From b8f359c3e55570fabc1d5fffadfaa74afe5c1b10 Mon Sep 17 00:00:00 2001 From: Eric House Date: Wed, 5 Aug 2020 09:25:33 -0700 Subject: [PATCH] add filtering to wordlist browser Add a basic regular expression engine to the dictiter, and to the UI add the ability to filter for "starts with", "contains" and "ends with", which translate into ANDed RE_*, _*RE_* and _*RE, respectively (with _ standing for blank/wildcard). The engine's tightly integrated with the next/prevWord() functions for greatest possible speed, but unless there's no pattern does slow things down a bit (especially when "ENDS WITH" is used.) The full engine is not exposed (users can't provide raw REs), and while the parser will accept nesting (e.g. ([AB]_*[CD]){2,5} to mean words from 2-5 tiles long starting with A or B and ending with C or D) the engine can't handle it. Which is why filtering for word length is handled separately from REs (but also tightly integrated.) Users can enter strings that don't map to tiles. They now get an error. It made sense for the error alert to have a "Show tiles" button, so there's now a dialog listing all the tiles in a wordlist, something the browser has needed all along. --- .../org/eehouse/android/xw4/BoardCanvas.java | 7 +- .../java/org/eehouse/android/xw4/DBUtils.java | 89 - .../org/eehouse/android/xw4/DelegateBase.java | 5 + .../android/xw4/DictBrowseDelegate.java | 635 ++++-- .../org/eehouse/android/xw4/DlgDelegate.java | 14 +- .../eehouse/android/xw4/DlgDelegateAlert.java | 4 +- .../java/org/eehouse/android/xw4/DlgID.java | 1 + .../org/eehouse/android/xw4/DlgState.java | 19 +- .../org/eehouse/android/xw4/EditWClear.java | 21 +- .../android/xw4/ExpandImageButton.java | 63 + .../android/xw4/GameConfigDelegate.java | 19 +- .../org/eehouse/android/xw4/GameListItem.java | 34 +- .../eehouse/android/xw4/LabeledSpinner.java | 63 + .../org/eehouse/android/xw4/PatTableRow.java | 88 + .../android/xw4/StudyListDelegate.java | 6 +- .../org/eehouse/android/xw4/TwoStrsItem.java | 1 - .../java/org/eehouse/android/xw4/Utils.java | 16 + .../org/eehouse/android/xw4/jni/XwJNI.java | 148 +- .../app/src/main/res/layout/dict_browser.xml | 193 +- .../app/src/main/res/layout/game_config.xml | 28 +- .../src/main/res/layout/game_list_item.xml | 9 +- .../app/src/main/res/layout/player_edit.xml | 16 +- .../app/src/main/res/layout/studylist.xml | 42 +- .../app/src/main/res/layout/tiles_row.xml | 16 + .../app/src/main/res/layout/tiles_table.xml | 35 + .../src/main/res/menu/dict_browse_menu.xml | 8 + .../app/src/main/res/values/common_rsrc.xml | 1 + .../app/src/main/res/values/strings.xml | 81 +- .../app/src/main/res/values/styles.xml | 28 + xwords4/android/jni/andutils.c | 5 +- xwords4/android/jni/utilwrapper.c | 6 +- xwords4/android/jni/xwjni.c | 428 ++-- xwords4/android/res_src/values-ca/strings.xml | 2 +- xwords4/android/res_src/values-de/strings.xml | 2 +- xwords4/android/res_src/values-fr/strings.xml | 3 +- xwords4/android/res_src/values-ja/strings.xml | 2 +- .../android/res_src/values-nb-rNO/strings.xml | 2 +- xwords4/android/res_src/values-nl/strings.xml | 3 +- xwords4/android/res_src/values-pl/strings.xml | 2 +- xwords4/android/res_src/values-sk/strings.xml | 6 - xwords4/common/board.c | 10 +- xwords4/common/boarddrw.c | 2 +- xwords4/common/comtypes.h | 2 +- xwords4/common/dictiter.c | 1743 ++++++++++++++--- xwords4/common/dictiter.h | 72 +- xwords4/common/dictmgr.c | 6 +- xwords4/common/dictmgr.h | 2 +- xwords4/common/dictnry.c | 90 +- xwords4/common/dictnry.h | 20 +- xwords4/common/engine.c | 2 +- xwords4/common/engine.h | 2 +- xwords4/common/model.c | 40 +- xwords4/common/model.h | 10 +- xwords4/common/modelp.h | 2 +- xwords4/common/mscore.c | 12 +- xwords4/common/pool.c | 2 +- xwords4/common/pool.h | 2 +- xwords4/common/server.c | 18 +- xwords4/common/tray.c | 4 +- xwords4/dawg/Catalan/Makefile.DISC2 | 2 +- xwords4/dawg/Makefile.langcommon | 5 +- xwords4/linux/Makefile | 1 + xwords4/linux/linuxmain.c | 278 ++- xwords4/linux/main.h | 12 + xwords4/linux/mqttcon.c | 31 +- xwords4/linux/scripts/regex-test.py | 92 + 66 files changed, 3446 insertions(+), 1167 deletions(-) create mode 100644 xwords4/android/app/src/main/java/org/eehouse/android/xw4/ExpandImageButton.java create mode 100644 xwords4/android/app/src/main/java/org/eehouse/android/xw4/LabeledSpinner.java create mode 100644 xwords4/android/app/src/main/java/org/eehouse/android/xw4/PatTableRow.java create mode 100644 xwords4/android/app/src/main/res/layout/tiles_row.xml create mode 100644 xwords4/android/app/src/main/res/layout/tiles_table.xml create mode 100644 xwords4/android/app/src/main/res/menu/dict_browse_menu.xml create mode 100755 xwords4/linux/scripts/regex-test.py diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardCanvas.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardCanvas.java index c8a23f33c..f526baf94 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardCanvas.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardCanvas.java @@ -146,7 +146,6 @@ public class BoardCanvas extends Canvas implements DrawCtx { m_context = context; m_activity = activity; m_jniThread = jniThread; - m_dict = new DictWrapper(); m_hasSmallScreen = Utils.hasSmallScreen( m_context ); @@ -583,7 +582,7 @@ public class BoardCanvas extends Canvas implements DrawCtx { @Override public void dictChanged( final long newPtr ) { - long curPtr = m_dict.getDictPtr(); + long curPtr = null == m_dict ? 0 : m_dict.getDictPtr(); boolean doPost = false; if ( curPtr != newPtr ) { if ( 0 == newPtr ) { @@ -595,7 +594,9 @@ public class BoardCanvas extends Canvas implements DrawCtx { m_dictChars = null; doPost = true; } - m_dict.release(); + if ( null != m_dict ) { + m_dict.release(); + } m_dict = new DictWrapper( newPtr ); } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java index da5b5cacc..4212ff183 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java @@ -122,15 +122,6 @@ public class DBUtils { int ts; } - public static class DictBrowseState { - public int m_minShown; - public int m_maxShown; - public int m_pos; - public int m_top; - public String m_prefix; - public int[] m_counts; - } - public static GameSummary getSummary( Context context, GameLock lock ) { @@ -1951,86 +1942,6 @@ public class DBUtils { return success; } - ///////////////////////////////////////////////////////////////// - // DictsDB stuff - ///////////////////////////////////////////////////////////////// - public static DictBrowseState dictsGetOffset( Context context, String name, - DictLoc loc ) - { - Assert.assertTrue( DictLoc.UNKNOWN != loc ); - DictBrowseState result = null; - String[] columns = { DBHelper.ITERPOS, DBHelper.ITERTOP, - DBHelper.ITERMIN, DBHelper.ITERMAX, - DBHelper.WORDCOUNTS, DBHelper.ITERPREFIX }; - String selection = - String.format( NAMELOC_FMT, DBHelper.DICTNAME, - name, DBHelper.LOC, loc.ordinal() ); - initDB( context ); - synchronized( s_dbHelper ) { - Cursor cursor = query( TABLE_NAMES.DICTBROWSE, columns, selection ); - if ( 1 >= cursor.getCount() && cursor.moveToFirst() ) { - result = new DictBrowseState(); - result.m_pos = cursor.getInt( cursor - .getColumnIndex(DBHelper.ITERPOS)); - result.m_top = cursor.getInt( cursor - .getColumnIndex(DBHelper.ITERTOP)); - result.m_minShown = - cursor.getInt( cursor - .getColumnIndex(DBHelper.ITERMIN)); - result.m_maxShown = - cursor.getInt( cursor - .getColumnIndex(DBHelper.ITERMAX)); - result.m_prefix = - cursor.getString( cursor - .getColumnIndex(DBHelper.ITERPREFIX)); - String counts = - cursor.getString( cursor.getColumnIndex(DBHelper.WORDCOUNTS)); - if ( null != counts ) { - String[] nums = TextUtils.split( counts, ":" ); - int[] ints = new int[nums.length]; - for ( int ii = 0; ii < nums.length; ++ii ) { - ints[ii] = Integer.parseInt( nums[ii] ); - } - result.m_counts = ints; - } - } - cursor.close(); - } - return result; - } - - public static void dictsSetOffset( Context context, String name, - DictLoc loc, DictBrowseState state ) - { - Assert.assertTrue( DictLoc.UNKNOWN != loc ); - String selection = - String.format( NAMELOC_FMT, DBHelper.DICTNAME, - name, DBHelper.LOC, loc.ordinal() ); - ContentValues values = new ContentValues(); - values.put( DBHelper.ITERPOS, state.m_pos ); - values.put( DBHelper.ITERTOP, state.m_top ); - values.put( DBHelper.ITERMIN, state.m_minShown ); - values.put( DBHelper.ITERMAX, state.m_maxShown ); - values.put( DBHelper.ITERPREFIX, state.m_prefix ); - if ( null != state.m_counts ) { - String[] nums = new String[state.m_counts.length]; - for ( int ii = 0; ii < nums.length; ++ii ) { - nums[ii] = String.format( "%d", state.m_counts[ii] ); - } - values.put( DBHelper.WORDCOUNTS, TextUtils.join( ":", nums ) ); - } - - initDB( context ); - synchronized( s_dbHelper ) { - int result = update( TABLE_NAMES.DICTBROWSE, values, selection ); - if ( 0 == result ) { - values.put( DBHelper.DICTNAME, name ); - values.put( DBHelper.LOC, loc.ordinal() ); - insert( TABLE_NAMES.DICTBROWSE, values ); - } - } - } - // Called from jni public static String dictsGetMD5Sum( Context context, String name ) { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DelegateBase.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DelegateBase.java index c4b03c067..1a878056b 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DelegateBase.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DelegateBase.java @@ -592,6 +592,11 @@ public class DelegateBase implements DlgClickNotify, m_dlgDelegate.startProgress( titleID, msg, null ); } + protected void startProgress( String title, String msg ) + { + m_dlgDelegate.startProgress( title, msg, null ); + } + protected void startProgress( int titleID, int msgID, OnCancelListener lstnr ) { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictBrowseDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictBrowseDelegate.java index 61449c5b1..6ead3d02d 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictBrowseDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DictBrowseDelegate.java @@ -25,9 +25,10 @@ import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; +import android.text.TextUtils; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView.OnItemSelectedListener; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.BaseAdapter; @@ -39,39 +40,99 @@ import android.widget.SectionIndexer; import android.widget.Spinner; import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; import org.eehouse.android.xw4.DlgDelegate.Action; +import org.eehouse.android.xw4.ExpandImageButton.ExpandChangeListener; +import org.eehouse.android.xw4.jni.DictInfo; import org.eehouse.android.xw4.jni.JNIUtilsImpl; +import org.eehouse.android.xw4.jni.XwJNI.DictWrapper; +import org.eehouse.android.xw4.jni.XwJNI.IterWrapper; +import org.eehouse.android.xw4.jni.XwJNI.PatDesc; import org.eehouse.android.xw4.jni.XwJNI; import java.util.Arrays; +import java.io.Serializable; public class DictBrowseDelegate extends DelegateBase - implements View.OnClickListener, OnItemSelectedListener { + implements View.OnClickListener { private static final String TAG = DictBrowseDelegate.class.getSimpleName(); private static final String DELIM = "."; + private static final boolean SHOW_NUM = false; private static final String DICT_NAME = "DICT_NAME"; private static final String DICT_LOC = "DICT_LOC"; private static final int MIN_LEN = 2; + private static final int MAX_LEN = 15; + + // Struct to show both what user's configuring AND what's been + // successfully fed to create the current iterator. The config setting + // become the filter params when the user presses the Apply Filter button + // and corrects any tile problems. + private static class DictBrowseState implements Serializable { + public int m_chosenMin, m_chosenMax; + public int m_passedMin, m_passedMax; + public int m_pos; + public int m_top; + public PatDesc[] m_pats; + public int[] m_counts; + public boolean m_expanded; + + public DictBrowseState() + { + m_chosenMin = MIN_LEN; + m_chosenMax = MAX_LEN; + m_pats = new PatDesc[3]; + for ( int ii = 0; ii < m_pats.length; ++ii ) { + m_pats[ii] = new PatDesc(); + } + } + + private void onFilterAccepted( DictWrapper dict, String delim ) + { + m_passedMin = m_chosenMin; + m_passedMax = m_chosenMax; + + for ( PatDesc desc : m_pats ) { + String str = XwJNI.dict_tilesToStr( dict, desc.tilePat, delim ); + desc.strPat = str; + } + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder("{pats:["); + for ( PatDesc pd : m_pats ) { + sb.append(pd).append(","); + } + sb.append("],"); + sb.append( "passedMin:").append(m_passedMin).append(",") + .append( "passedMax:").append(m_passedMax).append(",") + .append( "chosenMin:").append(m_chosenMin).append(",") + .append( "chosenMax:").append(m_chosenMax).append(",") + ; + sb.append("}"); + return sb.toString(); + } + } private Activity m_activity; - private long m_dictClosure = 0L; private int m_lang; private String m_name; private DictUtils.DictLoc m_loc; - private DBUtils.DictBrowseState m_browseState; + private DictBrowseState m_browseState; private int m_minAvail; private int m_maxAvail; private ListView m_list; - - -// - Steps to reproduce the problem: -// Create ListView, set custom adapter which implements ListAdapter and -// SectionIndexer but do not extends BaseAdapter. Enable fast scroll in -// layout. This will effect in ClassCastException. - + private IterWrapper m_diClosure; + private DictWrapper m_dict; + private DictInfo mDictInfo; + private PatTableRow m_rows[] = { null, null, null }; + private Spinner m_spinnerMin; + private Spinner m_spinnerMax; private class DictListAdapter extends BaseAdapter implements SectionIndexer { @@ -84,23 +145,19 @@ public class DictBrowseDelegate extends DelegateBase { super(); - XwJNI.di_setMinMax( m_dictClosure, m_browseState.m_minShown, - m_browseState.m_maxShown ); - m_nWords = XwJNI.di_wordCount( m_dictClosure ); - - int format = m_browseState.m_minShown == m_browseState.m_maxShown ? - R.string.dict_browse_title1_fmt : R.string.dict_browse_title_fmt; - setTitle( getString( format, m_name, m_nWords, - m_browseState.m_minShown, - m_browseState.m_maxShown )); + m_nWords = XwJNI.di_wordCount( m_diClosure ); + Log.d( TAG, "making DictListAdapter; have %d words", m_nWords ); } public Object getItem( int position ) { TextView text = (TextView) inflate( android.R.layout.simple_list_item_1 ); - String str = XwJNI.di_nthWord( m_dictClosure, position, null ); + String str = XwJNI.di_nthWord( m_diClosure, position, null ); if ( null != str ) { + if ( SHOW_NUM ) { + str = String.format( "%1$5d %2$s", position, str ); + } text.setText( str ); text.setOnClickListener( DictBrowseDelegate.this ); } @@ -114,7 +171,7 @@ public class DictBrowseDelegate extends DelegateBase public long getItemId( int position ) { return position; } public int getCount() { - Assert.assertTrue( 0 != m_dictClosure ); + Assert.assertTrueNR( m_nWords == XwJNI.di_wordCount( m_diClosure ) ); return m_nWords; } @@ -142,15 +199,16 @@ public class DictBrowseDelegate extends DelegateBase @Override public Object[] getSections() { - m_prefixes = XwJNI.di_getPrefixes( m_dictClosure ); - m_indices = XwJNI.di_getIndices( m_dictClosure ); + m_prefixes = XwJNI.di_getPrefixes( m_diClosure ); + m_indices = XwJNI.di_getIndices( m_diClosure ); return m_prefixes; } } - protected DictBrowseDelegate( Delegator delegator, Bundle savedInstanceState ) + protected DictBrowseDelegate( Delegator delegator, Bundle sis ) { - super( delegator, savedInstanceState, R.layout.dict_browser ); + super( delegator, sis, R.layout.dict_browser, + R.menu.dict_browse_menu ); m_activity = delegator.getActivity(); } @@ -168,71 +226,63 @@ public class DictBrowseDelegate extends DelegateBase DictUtils.DictLoc.values()[args.getInt( DICT_LOC, 0 )]; m_lang = DictLangCache.getDictLangCode( m_activity, name ); - String[] names = { name }; - DictUtils.DictPairs pairs = DictUtils.openDicts( m_activity, names ); - m_dictClosure = XwJNI.di_init( pairs.m_bytes[0], - name, pairs.m_paths[0] ); + findTableRows(); + m_spinnerMin = ((LabeledSpinner)findViewById( R.id.spinner_min )) + .getSpinner(); + m_spinnerMax = ((LabeledSpinner)findViewById( R.id.spinner_max )) + .getSpinner(); - String desc = XwJNI.di_getDesc( m_dictClosure ); - Log.d( TAG, "got desc: %s", desc ); + loadBrowseState(); + + String[] names = { m_name }; + DictUtils.DictPairs pairs = DictUtils.openDicts( m_activity, names ); + Assert.assertNotNull( m_browseState ); + m_dict = XwJNI.makeDict( pairs.m_bytes[0], m_name, pairs.m_paths[0] ); + + mDictInfo = new DictInfo(); + XwJNI.dict_getInfo( m_dict, false, mDictInfo ); + setTitle( getString( R.string.dict_browse_title_fmt, m_name, mDictInfo.wordCount ) ); + + ExpandImageButton eib = (ExpandImageButton)findViewById( R.id.expander ); + eib.setOnExpandChangedListener( new ExpandChangeListener() { + @Override + public void expandedChanged( boolean nowExpanded ) + { + m_browseState.m_expanded = nowExpanded; + setShowConfig(); + } + } ) + .setExpanded( m_browseState.m_expanded ); + + String desc = XwJNI.dict_getDesc( m_dict ); if ( null != desc ) { TextView view = (TextView)findViewById( R.id.desc ); - Assert.assertNotNull( view ); view.setVisibility( View.VISIBLE ); view.setText( desc ); } - m_browseState = DBUtils.dictsGetOffset( m_activity, name, m_loc ); - boolean newState = null == m_browseState; - if ( newState ) { - m_browseState = new DBUtils.DictBrowseState(); - m_browseState.m_pos = 0; - m_browseState.m_top = 0; - } - if ( null == m_browseState.m_counts ) { - m_browseState.m_counts = XwJNI.di_getCounts( m_dictClosure ); + int[] ids = { R.id.button_useconfig, R.id.button_addBlank, }; + for ( int id : ids ) { + findViewById( id ).setOnClickListener(this); } - if ( null == m_browseState.m_counts ) { - // empty dict? Just close down for now. Later if - // this is extended to include tile info -- it should - // be -- then use an empty list elem and disable - // search/minmax stuff. - String msg = getString( R.string.alert_empty_dict_fmt, name ); - makeOkOnlyBuilder(msg).setAction(Action.FINISH_ACTION).show(); - } else { - figureMinMax( m_browseState.m_counts ); - if ( newState ) { - m_browseState.m_minShown = m_minAvail; - m_browseState.m_maxShown = m_maxAvail; - } - - Button button = (Button)findViewById( R.id.search_button ); - button.setOnClickListener( new View.OnClickListener() { - public void onClick( View view ) - { - findButtonClicked(); - } - } ); - - setUpSpinners(); - - initList(); - } + setShowConfig(); + replaceIter( true ); } } // init protected void onPause() { - if ( null != m_browseState // already saved? - && null != m_list ) { // there are words? (don't NPE on empty dict) - m_browseState.m_pos = m_list.getFirstVisiblePosition(); - View view = m_list.getChildAt( 0 ); - m_browseState.m_top = (view == null) ? 0 : view.getTop(); - m_browseState.m_prefix = getFindText(); - DBUtils.dictsSetOffset( m_activity, m_name, m_loc, m_browseState ); - m_browseState = null; - } + scrapeBrowseState(); + storeBrowseState(); + // if ( null != m_browseState ) { + // if ( null != m_list ) { // there are words? (don't NPE on empty dict) + // m_browseState.m_pos = m_list.getFirstVisiblePosition(); + // View view = m_list.getChildAt( 0 ); + // m_browseState.m_top = (view == null) ? 0 : view.getTop(); + // } + // storeBrowseState(); + // } super.onPause(); } @@ -240,30 +290,12 @@ public class DictBrowseDelegate extends DelegateBase protected void onResume() { super.onResume(); - if ( null == m_browseState ) { - m_browseState = DBUtils.dictsGetOffset( m_activity, m_name, m_loc ); - } - setFindText( m_browseState.m_prefix ); - } - - @Override - protected void onDestroy() - { - XwJNI.di_destroy( m_dictClosure ); - m_dictClosure = 0; - } - - // Just in case onDestroy didn't get called.... - @Override - public void finalize() - { - Assert.assertTrueNR( m_dictClosure == 0 ); - XwJNI.di_destroy( m_dictClosure ); - try { - super.finalize(); - } catch ( java.lang.Throwable err ){ - Log.i( TAG, "%s", err.toString() ); - } + loadBrowseState(); + // if ( null == m_browseState ) { + // m_browseState = DBUtils.dictsGetOffset( m_activity, m_name, m_loc ); + // here + // } + setFindPats( m_browseState.m_pats ); } @Override @@ -276,7 +308,7 @@ public class DictBrowseDelegate extends DelegateBase final byte[][] choices = (byte[][])params[0]; final String[] strs = new String[choices.length]; for ( int ii = 0; ii < choices.length; ++ii ) { - strs[ii] = XwJNI.di_tilesToStr( m_dictClosure, choices[ii], DELIM ); + strs[ii] = XwJNI.dict_tilesToStr( m_dict, choices[ii], DELIM ); } final int[] chosen = {0}; dialog = makeAlertBuilder() @@ -293,14 +325,26 @@ public class DictBrowseDelegate extends DelegateBase public void onClick( DialogInterface dialog, int which ) { if ( 0 <= chosen[0] ) { - byte[][] theOne = {choices[chosen[0]]}; - showPrefix( theOne, DELIM ); + Assert.failDbg(); } } } ) .setTitle( R.string.pick_tiles_title ) .create(); break; + case SHOW_TILES: + String info = (String)params[0]; + View tilesView = inflate( R.layout.tiles_table ); + addTileRows( tilesView, info ); + + String langName = DictLangCache.getLangName( m_activity, m_lang ); + String title = getString( R.string.show_tiles_title_fmt, langName ); + dialog = makeAlertBuilder() + .setView( tilesView ) + .setPositiveButton( android.R.string.ok, null ) + .setTitle( title ) + .create(); + break; default: dialog = super.makeDialog( alert, params ); break; @@ -308,48 +352,42 @@ public class DictBrowseDelegate extends DelegateBase return dialog; } + @Override + public boolean onOptionsItemSelected( MenuItem item ) + { + boolean handled = true; + + switch ( item.getItemId() ) { + case R.id.dicts_showtiles: + showTiles(); + break; + default: + handled = false; + } + return handled; + } + ////////////////////////////////////////////////// // View.OnClickListener interface ////////////////////////////////////////////////// @Override public void onClick( View view ) { - TextView text = (TextView)view; - String[] words = { text.getText().toString() }; - launchLookup( words, m_lang, true ); - } - - ////////////////////////////////////////////////// - // AdapterView.OnItemSelectedListener interface - ////////////////////////////////////////////////// - @Override - public void onItemSelected( AdapterView parent, View view, - int position, long id ) - { - TextView text = (TextView)view; - // null text seems to have generated at least one google play report - if ( null != text && null != m_browseState ) { - int newval = Integer.parseInt( text.getText().toString() ); - switch ( parent.getId() ) { - case R.id.wordlen_min: - if ( newval != m_browseState.m_minShown ) { - setMinMax( newval, m_browseState.m_maxShown ); - } - break; - case R.id.wordlen_max: - if ( newval != m_browseState.m_maxShown ) { - setMinMax( m_browseState.m_minShown, newval ); - } - break; - } + switch ( view.getId() ) { + case R.id.button_useconfig: + useButtonClicked(); + break; + case R.id.button_addBlank: + addBlankButtonClicked(); + break; + default: + TextView text = (TextView)view; + String[] words = { text.getText().toString() }; + launchLookup( words, m_lang, true ); + break; } } - @Override - public void onNothingSelected( AdapterView parent ) - { - } - ////////////////////////////////////////////////// // DlgDelegate.DlgClickNotify interface ////////////////////////////////////////////////// @@ -362,6 +400,9 @@ public class DictBrowseDelegate extends DelegateBase handled = true; finish(); break; + case SHOW_TILES: + showTiles(); + break; default: handled = super.onPosButton( action, params ); break; @@ -369,70 +410,209 @@ public class DictBrowseDelegate extends DelegateBase return handled; } - private void findButtonClicked() + private void scrapeBrowseState() { - String text = getFindText(); - if ( null != text && 0 < text.length() ) { - m_browseState.m_prefix = text; + Assert.assertTrueNR( null != m_browseState ); + m_browseState.m_chosenMin = MIN_LEN + m_spinnerMin.getSelectedItemPosition(); + m_browseState.m_chosenMax = MIN_LEN + m_spinnerMax.getSelectedItemPosition(); + if ( null != m_list ) { // there are words? (don't NPE on empty dict) + m_browseState.m_pos = m_list.getFirstVisiblePosition(); + View view = m_list.getChildAt( 0 ); + m_browseState.m_top = (view == null) ? 0 : view.getTop(); + } - byte[][] choices = XwJNI.di_strToTiles( m_dictClosure, text ); - if ( null == choices || 0 == choices.length ) { - String msg = getString( R.string.no_tiles_exist, text, m_name ); - makeOkOnlyBuilder( msg ).show(); - } else if ( 1 == choices.length || !XwJNI.di_hasDuplicates(m_dictClosure) ) { - showPrefix( choices, null ); - } else { - showDialogFragment( DlgID.CHOOSE_TILES, (Object)choices ); + // Get the strings (not bytes) from the rows + for ( int ii = 0; ii < m_rows.length; ++ii ) { + m_rows[ii].getToDesc(m_browseState.m_pats[ii]); + // .updateFrom( desc ); + } + } + + private static final int[] sTileRowIDs = {R.id.face, R.id.count, R.id.value }; + private void addTileRows( View view, String info ) + { + ViewGroup table = view.findViewById( R.id.table ); + if ( null != table ) { + String[] tiles = TextUtils.split( info, "\n" ); + for ( String row : tiles ) { + String[] fields = TextUtils.split( row, "\t" ); + if ( 3 == fields.length ) { + ViewGroup rowView = (ViewGroup)inflate( R.layout.tiles_row ); + for ( int ii = 0; ii < sTileRowIDs.length; ++ii ) { + TextView tv = (TextView)rowView.findViewById( sTileRowIDs[ii] ); + tv.setText( fields[ii] ); + } + table.addView( rowView ); + } } } } - private String getFindText() + private void showTiles() { - EditWClear edit = (EditWClear)findViewById( R.id.word_edit ); - return edit.getText().toString(); + String info = XwJNI.getTilesInfo( m_dict ); + showDialogFragment( DlgID.SHOW_TILES, info ); } - private void setFindText( String text ) + private String m_stateKey = null; + private String getStateKey() { - EditWClear edit = (EditWClear)findViewById( R.id.word_edit ); - edit.setText( text ); + if ( null == m_stateKey ) { + m_stateKey = String.format( "KEY_%s_%d", m_name, m_loc.ordinal() ); + } + return m_stateKey; } - private void showPrefix( byte[][] prefix, String delim ) + private void findTableRows() { - if ( null != prefix && 0 < prefix.length && 0 < prefix[0].length ) { - int pos = XwJNI.di_getStartsWith( m_dictClosure, prefix ); - if ( 0 <= pos ) { - m_list.setSelection( pos ); - } else { - String text = XwJNI.di_tilesToStr( m_dictClosure, prefix[0], delim ); - DbgUtils.showf( m_activity, R.string.dict_browse_nowords_fmt, - m_name, text ); + ViewGroup table = (ViewGroup)findViewById( R.id.config ); + int count = table.getChildCount(); + int nFound = 0; + for ( int ii = 0; ii < count && nFound < m_rows.length; ++ii ) { + View child = table.getChildAt( ii ); + if ( child instanceof PatTableRow ) { + m_rows[nFound++] = (PatTableRow)child; } } + Assert.assertTrueNR( nFound == m_rows.length ); + } + + private void loadBrowseState() + { + boolean newState = false; + if ( null == m_browseState ) { + Serializable obj = DBUtils.getSerializableFor( m_activity, getStateKey() ); + if ( null != obj && obj instanceof DictBrowseState ) { + m_browseState = (DictBrowseState)obj; + if ( null == m_browseState.m_pats ) { + m_browseState = null; + } + } + if ( null == m_browseState ) { + m_browseState = new DictBrowseState(); + } + } + Log.d( TAG, "loadBrowseState() => %s", m_browseState ); + } + + private void storeBrowseState() + { + if ( null != m_browseState ) { + DBUtils.setSerializableFor( m_activity, getStateKey(), m_browseState ); + } } + private void useButtonClicked() + { + scrapeBrowseState(); + Log.d( TAG, "useButtonClicked(): m_browseState: %s", m_browseState ); + + boolean pending = false; + + if ( m_browseState.m_chosenMin > m_browseState.m_chosenMax ) { + pending = true; + makeOkOnlyBuilder( R.string.error_min_gt_max ).show(); + } + + PatDesc[] pats = m_browseState.m_pats; + for ( int ii = 0; ii < pats.length && !pending; ++ii ) { + String strPat = pats[ii].strPat; + if ( null != strPat && 0 < strPat.length() ) { + byte[][] choices = XwJNI.dict_strToTiles( m_dict, strPat ); + if ( null == choices || 0 == choices.length ) { + String langName = DictLangCache.getLangName( m_activity, m_lang ); + String msg = getString( R.string.no_tiles_exist, strPat, langName ); + makeOkOnlyBuilder( msg ) + .setActionPair( Action.SHOW_TILES, R.string.show_tiles_button ) + .show(); + pending = true; + } else if ( 1 == choices.length + || !XwJNI.dict_hasDuplicates( m_dict ) ) { + pats[ii].tilePat = choices[0]; + } else { + showDialogFragment( DlgID.CHOOSE_TILES, (Object)choices ); + pending = true; + } + } else { + pats[ii].tilePat = null; + } + } + + if ( !pending ) { + storeBrowseState(); + replaceIter( false ); + } + } + + private void addBlankButtonClicked() + { + boolean handled = false; + for ( PatTableRow row : m_rows ) { + handled = handled || row.addBlankToFocussed( "_" ); + } + if ( !handled ) { + makeNotAgainBuilder( R.string.blank_button_expl, + R.string.key_na_addBlankButton ) + .setTitle(null) + .show(); + } + } + + private void setShowConfig() + { + findViewById(R.id.config).setVisibility( m_browseState.m_expanded + ? View.VISIBLE : View.GONE ); + } + + private void setFindPats( PatDesc[] descs ) + { + if ( null != descs && descs.length == m_rows.length ) { + for ( int ii = 0; ii < m_rows.length; ++ii ) { + m_rows[ii].setFromDesc( descs[ii] ); + } + } + setUpSpinners(); + } + + private String formatPats( PatDesc[] pats, String delim ) + { + Assert.assertTrueNR( null != m_diClosure ); + List strs = new ArrayList<>(); + for ( int ii = 0; ii < pats.length; ++ii ) { + PatDesc desc = pats[ii]; + String str = desc.strPat; + if ( null == str && (ii == 0 || ii == pats.length - 1) ) { + str = ""; + } + if ( null != str ) { + strs.add(str); + } + } + String result = TextUtils.join( "…", strs ); + // Log.d( TAG, "formatPats() => %s", result ); + return result; + } + private void setMinMax( int min, int max ) { // I can't make a second call to setListAdapter() work, nor does // notifyDataSetChanged do anything toward refreshing the // adapter/making it recognize a changed dataset. So, as a // workaround, relaunch the activity with different parameters. - if ( m_browseState.m_minShown != min || - m_browseState.m_maxShown != max ) { + // if ( m_browseState.m_minShown != min || + // m_browseState.m_maxShown != max ) { - m_browseState.m_pos = 0; - m_browseState.m_top = 0; - m_browseState.m_minShown = min; - m_browseState.m_maxShown = max; - m_browseState.m_prefix = getFindText(); - DBUtils.dictsSetOffset( m_activity, m_name, m_loc, m_browseState ); + // m_browseState.m_pos = 0; + // m_browseState.m_top = 0; + // m_browseState.m_minShown = min; + // m_browseState.m_maxShown = max; + // m_browseState.m_pats = getFindText(); + // DBUtils.dictsSetOffset( m_activity, m_name, m_loc, m_browseState ); - setUpSpinners(); + // setUpSpinners(); - initList(); - } + // initList(); + // } } private void figureMinMax( int[] counts ) @@ -448,55 +628,106 @@ public class DictBrowseDelegate extends DelegateBase } } - private void makeSpinnerAdapter( int resID, int min, int max, int cur ) + private String[] m_nums; + private void makeSpinnerAdapter( Spinner spinner, int curVal ) { - Spinner spinner = (Spinner)findViewById( resID ); - Assert.assertTrue( min <= max ); - - int sel = -1; - String[] nums = new String[max - min + 1]; - for ( int ii = 0; ii < nums.length; ++ii ) { - int val = min + ii; - if ( val == cur ) { - sel = ii; - } - nums[ii] = String.format( "%d", min + ii ); - } ArrayAdapter adapter = new ArrayAdapter( m_activity, - //android.R.layout.simple_spinner_dropdown_item, android.R.layout.simple_spinner_item, - nums ); + m_nums ); adapter.setDropDownViewResource( android.R.layout. simple_spinner_dropdown_item ); spinner.setAdapter( adapter ); - spinner.setSelection( sel ); - spinner.setOnItemSelectedListener( this ); + spinner.setSelection( curVal - MIN_LEN ); + // spinner.setOnItemSelectedListener( this ); } private void setUpSpinners() { - // Min and max-length spinners. To avoid empty lists, - // don't allow min to exceed max. Do that by making the - // current max the largest min allowed, and the current - // min the smallest max allowed. - makeSpinnerAdapter( R.id.wordlen_min, m_minAvail, - m_browseState.m_maxShown, m_browseState.m_minShown ); - makeSpinnerAdapter( R.id.wordlen_max, m_browseState.m_minShown, - m_maxAvail, m_browseState.m_maxShown ); + if ( null == m_nums ) { + m_nums = new String[MAX_LEN - MIN_LEN + 1]; + for ( int ii = MIN_LEN; ii <= MAX_LEN; ++ii ) { + m_nums[ii - MIN_LEN] = String.format( "%d", ii ); + } + } + + makeSpinnerAdapter( m_spinnerMin, m_browseState.m_chosenMin ); + makeSpinnerAdapter( m_spinnerMax, m_browseState.m_chosenMax ); } - private void initList() + private FrameLayout removeList() { + m_list = null; FrameLayout parent = (FrameLayout)findViewById(R.id.list_container); parent.removeAllViews(); + return parent; + } + + private void replaceIter( boolean useOldVals ) + { + Assert.assertNotNull( m_browseState ); + Assert.assertNotNull( m_dict ); + int min = useOldVals ? m_browseState.m_passedMin : m_browseState.m_chosenMin; + int max = useOldVals ? m_browseState.m_passedMax : m_browseState.m_chosenMax; + + String title = getString( R.string.filter_title_fmt, m_name ); + String msg = getString( R.string.filter_progress_fmt, mDictInfo.wordCount ); + startProgress( title, msg ); + + XwJNI.di_init( m_dict, m_browseState.m_pats, min, max, + new XwJNI.DictIterProcs() { + @Override + public void onIterReady( final IterWrapper wrapper ) + { + runOnUiThread( new Runnable() { + @Override + public void run() { + stopProgress(); + + m_browseState.onFilterAccepted( m_dict, null ); + initList( wrapper ); + setFindPats( m_browseState.m_pats ); + } + } ); + } + } ); + } + + private void initList( IterWrapper newIter ) + { + FrameLayout parent = removeList(); + m_list = (ListView)inflate( R.layout.dict_browser_list ); - m_list.setAdapter( new DictListAdapter() ); + + Assert.assertNotNull( m_browseState ); + Assert.assertNotNull( m_dict ); + m_diClosure = newIter; + + DictListAdapter dla = new DictListAdapter(); + + m_list.setAdapter( dla ); m_list.setFastScrollEnabled( true ); m_list.setSelectionFromTop( m_browseState.m_pos, m_browseState.m_top ); parent.addView( m_list ); + + updateFilterString(); + } + + private void updateFilterString() + { + PatDesc[] pats = m_browseState.m_pats; + Assert.assertNotNull( pats ); + String summary; + String pat = formatPats( pats, null ); + int nWords = XwJNI.di_wordCount( m_diClosure ); + int[] minMax = XwJNI.di_getMinMax( m_diClosure ); + summary = getString( R.string.filter_sum_pat_fmt, pat, + minMax[0], minMax[1], + nWords ); + TextView tv = (TextView)findViewById( R.id.filter_summary ); + tv.setText( summary ); } private static void launch( Delegator delegator, Bundle bundle ) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java index 27fff5d07..347d8d835 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java @@ -90,6 +90,7 @@ public class DlgDelegate { DELETE_DICT_ACTION, UPDATE_DICTS_ACTION, MOVE_CONFIRMED, + SHOW_TILES, // Game configs LOCKED_CHANGE_ACTION, @@ -198,7 +199,13 @@ public class DlgDelegate { Builder setTitle( int strID ) { - mState.setTitle( strID ); + mState.setTitle( getString(strID) ); + return this; + } + + Builder setTitle( String str ) + { + mState.setTitle( str ); return this; } @@ -407,6 +414,11 @@ public class DlgDelegate { public void startProgress( int titleID, String msg, OnCancelListener lstnr ) { String title = getString( titleID ); + startProgress( title, msg, lstnr ); + } + + public void startProgress( String title, String msg, OnCancelListener lstnr ) + { m_progress = ProgressDialog.show( m_activity, title, msg, true, true ); if ( null != lstnr ) { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegateAlert.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegateAlert.java index eee10f207..c1ffe9df9 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegateAlert.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegateAlert.java @@ -116,8 +116,8 @@ public class DlgDelegateAlert extends XWDialogFragment { AlertDialog.Builder builder = LocUtils.makeAlertBuilder( context ); - if ( 0 != state.m_titleId ) { - builder.setTitle( state.m_titleId ); + if ( null != state.m_title ) { + builder.setTitle( state.m_title ); } populateBuilder( context, state, builder ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java index 31d99b6fd..be256ba2d 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgID.java @@ -69,6 +69,7 @@ public enum DlgID { , GAMES_LIST_NAME_REMATCH , ASK_DUP_PAUSE , CHOOSE_TILES + , SHOW_TILES ; private boolean m_addToStack; diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgState.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgState.java index a0f3a4360..8ad4e2795 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgState.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgState.java @@ -46,7 +46,7 @@ public class DlgState implements Parcelable { public int m_prefsNAKey; // These can't be serialized!!!! public Object[] m_params; - public int m_titleId; + public String m_title; public DlgState( DlgID dlgID ) { @@ -79,8 +79,8 @@ public class DlgState implements Parcelable { { m_posButton = id; return this; } public DlgState setNegButton( int id ) { m_negButton = id; return this; } - public DlgState setTitle( int id ) - { m_titleId = id; return this; } + public DlgState setTitle( String title ) + { m_title = title; return this; } @Override public String toString() @@ -103,7 +103,7 @@ public class DlgState implements Parcelable { .append(", pair ").append(m_pair) .append(", pos: ").append(m_posButton) .append(", neg: ").append(m_negButton) - .append(", title: ").append(m_titleId) + .append(", title: ").append(m_title) .append(", params: [").append(params) .append("]}") .toString(); @@ -124,14 +124,15 @@ public class DlgState implements Parcelable { DlgState other = (DlgState)it; result = other != null && m_id.equals(other.m_id) - && ((null == m_msg) ? (null == other.m_msg) : m_msg.equals(other.m_msg)) + && TextUtils.equals( m_msg, other.m_msg) && m_posButton == other.m_posButton && m_negButton == other.m_negButton && m_action == other.m_action && ((null == m_pair) ? (null == other.m_pair) : m_pair.equals(other.m_pair)) && m_prefsNAKey == other.m_prefsNAKey && Arrays.deepEquals( m_params, other.m_params ) - && m_titleId == other.m_titleId; + && TextUtils.equals( m_title,other.m_title) + ; } } else { result = super.equals( it ); @@ -164,7 +165,7 @@ public class DlgState implements Parcelable { out.writeInt( m_negButton ); out.writeInt( null == m_action ? -1 : m_action.ordinal() ); out.writeInt( m_prefsNAKey ); - out.writeInt( m_titleId ); + out.writeString( m_title ); out.writeString( m_msg ); out.writeSerializable( m_params ); out.writeSerializable( m_pair ); @@ -196,7 +197,7 @@ public class DlgState implements Parcelable { int tmp = in.readInt(); Action action = 0 > tmp ? null : Action.values()[tmp]; int prefsKey = in.readInt(); - int titleId = in.readInt(); + String title = in.readString(); String msg = in.readString(); Object[] params = (Object[])in.readSerializable(); ActionPair pair = (ActionPair)in.readSerializable(); @@ -206,7 +207,7 @@ public class DlgState implements Parcelable { .setNegButton( negButton ) .setAction( action ) .setPrefsNAKey( prefsKey ) - .setTitle(titleId) + .setTitle(title) .setParams(params) .setActionPair(pair) ; diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/EditWClear.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/EditWClear.java index d94e754b8..8bb558844 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/EditWClear.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/EditWClear.java @@ -19,9 +19,10 @@ package org.eehouse.android.xw4; -import android.widget.SearchView; import android.content.Context; import android.util.AttributeSet; +import android.widget.EditText; +import android.widget.SearchView; import java.util.HashSet; import java.util.Set; @@ -31,6 +32,7 @@ public class EditWClear extends SearchView private static final String TAG = EditWClear.class.getSimpleName(); private Set mWatchers; + private EditText mEdit; public interface TextWatcher { void onTextChanged( String newText ); @@ -41,6 +43,12 @@ public class EditWClear extends SearchView super( context, as ); } + @Override + protected void onFinishInflate() + { + mEdit = (EditText)Utils.getChildInstanceOf( this, EditText.class ); + } + synchronized void addTextChangedListener( TextWatcher proc ) { if ( null == mWatchers ) { @@ -60,6 +68,17 @@ public class EditWClear extends SearchView return super.getQuery(); } + void insertBlank( String blank ) + { + // I'm not confident I'll always be able to get the edittext, so to be + // safe.... + if ( null == mEdit ) { + setQuery( getQuery() + blank, false ); + } else { + mEdit.getText().insert(mEdit.getSelectionStart(), blank ); + } + } + // from SearchView.OnQueryTextListener @Override public synchronized boolean onQueryTextChange( String newText ) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ExpandImageButton.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ExpandImageButton.java new file mode 100644 index 000000000..17454a59d --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ExpandImageButton.java @@ -0,0 +1,63 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2009 - 2020 by Eric House (xwords@eehouse.org). All rights + * reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.eehouse.android.xw4; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; + +public class ExpandImageButton extends ImageButton { + private boolean m_expanded; + + public interface ExpandChangeListener { + public void expandedChanged( boolean nowExpanded ); + } + + public ExpandImageButton( Context context, AttributeSet as ) + { + super( context, as ); + } + + public ExpandImageButton setExpanded( boolean expanded ) + { + m_expanded = expanded; + + setImageResource( expanded ? + R.drawable.expander_ic_maximized : + R.drawable.expander_ic_minimized); + return this; + } + + public ExpandImageButton setOnExpandChangedListener( final ExpandChangeListener listener ) + { + setOnClickListener( new View.OnClickListener() { + @Override + public void onClick( View view ) + { + m_expanded = ! m_expanded; + setExpanded( m_expanded ); + listener.expandedChanged( m_expanded ); + } + } ); + return this; + } +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java index f505bada5..8b406b7ec 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java @@ -377,8 +377,8 @@ public class GameConfigDelegate extends DelegateBase } else { dictLabel.setVisibility( View.GONE ); } - m_playerDictSpinner = (Spinner) - playerView.findViewById( R.id.dict_spinner ); + m_playerDictSpinner = ((LabeledSpinner)playerView.findViewById( R.id.player_dict_spinner )) + .getSpinner(); if ( localOnlyGame() ) { configDictSpinner( m_playerDictSpinner, m_gi.dictLang, m_gi.dictName(lp) ); } else { @@ -429,11 +429,6 @@ public class GameConfigDelegate extends DelegateBase lp.password = Utils.getText( dialog, R.id.password_edit ); if ( localOnlyGame() ) { - { - Spinner spinner = - (Spinner)((Dialog)di).findViewById( R.id.dict_spinner ); - Assert.assertTrue( m_playerDictSpinner == spinner ); - } int position = m_playerDictSpinner.getSelectedItemPosition(); SpinnerAdapter adapter = m_playerDictSpinner.getAdapter(); @@ -475,9 +470,13 @@ public class GameConfigDelegate extends DelegateBase findViewById( R.id.play_button ).setOnClickListener( this ); m_playerLayout = (LinearLayout)findViewById( R.id.player_list ); - m_phoniesSpinner = (Spinner)findViewById( R.id.phonies_spinner ); - m_boardsizeSpinner = (Spinner)findViewById( R.id.boardsize_spinner ); - m_smartnessSpinner = (Spinner)findViewById( R.id.smart_robot ); + + m_phoniesSpinner = ((LabeledSpinner)findViewById( R.id.phonies_spinner )) + .getSpinner(); + m_boardsizeSpinner = ((LabeledSpinner)findViewById( R.id.boardsize_spinner )) + .getSpinner(); + m_smartnessSpinner = ((LabeledSpinner)findViewById( R.id.smart_robot )) + .getSpinner(); m_connLabel = (TextView)findViewById( R.id.conns_label ); } // init diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java index 3c539a9b5..2cb36a142 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java @@ -43,7 +43,8 @@ import java.util.HashSet; import java.util.concurrent.LinkedBlockingQueue; public class GameListItem extends LinearLayout - implements View.OnClickListener, SelectableItem.LongClickHandler { + implements View.OnClickListener, SelectableItem.LongClickHandler, + ExpandImageButton.ExpandChangeListener { private static final String TAG = GameListItem.class.getSimpleName(); private static final int SUMMARY_WAIT_MSECS = 1000; @@ -67,7 +68,7 @@ public class GameListItem extends LinearLayout private boolean m_expanded, m_haveTurn, m_haveTurnLocal; private long m_lastMoveTime; - private ImageButton m_expandButton; + private ExpandImageButton m_expandButton; private Handler m_handler; private GameSummary m_summary; private SelectableItem m_cb; @@ -165,15 +166,6 @@ public class GameListItem extends LinearLayout { int id = view.getId(); switch ( id ) { - case R.id.expander: - m_expanded = !m_expanded; - DBUtils.setExpanded( m_rowid, m_expanded ); - - makeThumbnailIf( m_expanded ); - - showHide(); - break; - case R.id.view_loaded: toggleSelected(); break; @@ -189,12 +181,24 @@ public class GameListItem extends LinearLayout } } + // ExpandImageButton.ExpandChangeListener + @Override + public void expandedChanged( boolean nowExpanded ) + { + m_expanded = nowExpanded; + DBUtils.setExpanded( m_rowid, m_expanded ); + + makeThumbnailIf( m_expanded ); + + showHide(); + } + private void findViews() { m_hideable = (LinearLayout)findViewById( R.id.hideable ); m_name = (ExpiringTextView)findViewById( R.id.game_name ); - m_expandButton = (ImageButton)findViewById( R.id.expander ); - m_expandButton.setOnClickListener( this ); + m_expandButton = (ExpandImageButton)findViewById( R.id.expander ); + m_expandButton.setOnExpandChangedListener( this ); m_viewUnloaded = (TextView)findViewById( R.id.view_unloaded ); m_viewLoaded = findViewById( R.id.view_loaded ); m_viewLoaded.setOnClickListener( this ); @@ -225,9 +229,7 @@ public class GameListItem extends LinearLayout private void showHide() { - m_expandButton.setImageResource( m_expanded ? - R.drawable.expander_ic_maximized : - R.drawable.expander_ic_minimized); + m_expandButton.setExpanded( m_expanded ); m_hideable.setVisibility( m_expanded? View.VISIBLE : View.GONE ); int vis = m_expanded && XWPrefs.getThumbEnabled( m_context ) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/LabeledSpinner.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/LabeledSpinner.java new file mode 100644 index 000000000..f5de92fdd --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/LabeledSpinner.java @@ -0,0 +1,63 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2020 by Eric House (xwords@eehouse.org). All + * rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.eehouse.android.xw4; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + + +/** + * This class's purpose is to link a spinner with a textview that's its label + * such that clicking on the label is the same as clicking on the spinner. + */ + +public class LabeledSpinner extends LinearLayout { + private Spinner mSpinner; + + public LabeledSpinner( Context context, AttributeSet as ) { + super( context, as ); + } + + @Override + protected void onFinishInflate() + { + mSpinner = (Spinner)Utils.getChildInstanceOf( this, Spinner.class ); + + TextView tv = (TextView)Utils.getChildInstanceOf( this, TextView.class ); + tv.setOnClickListener( new OnClickListener() { + @Override + public void onClick( View target ) + { + mSpinner.performClick(); + } + } ); + } + + public Spinner getSpinner() + { + Assert.assertNotNull( mSpinner ); + return mSpinner; + } +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PatTableRow.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PatTableRow.java new file mode 100644 index 000000000..149a269f4 --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PatTableRow.java @@ -0,0 +1,88 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2020 by Eric House (xwords@eehouse.org). All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.eehouse.android.xw4; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.CheckBox; +import android.widget.TableRow; + +import org.eehouse.android.xw4.jni.XwJNI.PatDesc; + +public class PatTableRow extends TableRow { + private static final String TAG = PatTableRow.class.getSimpleName(); + private EditWClear mEdit; + private CheckBox mCheck; + + public PatTableRow( Context context, AttributeSet as ) + { + super( context, as ); + } + + public void getToDesc( PatDesc out ) + { + getFields(); + + // PatDesc result = null; + String strPat = mEdit.getText().toString(); + out.strPat = strPat; + out.anyOrderOk = mCheck.isChecked(); + // if ( null != strPat && 0 < strPat.length() ) { + // result = new PatDesc(); + // result.strPat = strPat; + // result.anyOrderOk = mCheck.isChecked(); + // } + // return result; + } + + public void setFromDesc( PatDesc desc ) + { + getFields(); + + mEdit.setText(desc.strPat); + mCheck.setChecked(desc.anyOrderOk); + } + + public boolean addBlankToFocussed( String blank ) + { + getFields(); + + boolean handled = mEdit.hasFocus(); + if ( handled ) { + mEdit.insertBlank( blank ); + } + return handled; + } + + private void getFields() + { + for ( int ii = 0; + (null == mEdit || null == mCheck) && ii < getChildCount(); + ++ii ) { + View view = getChildAt( ii ); + if ( view instanceof EditWClear ) { + mEdit = (EditWClear)view; + } else if ( view instanceof CheckBox ) { + mCheck = (CheckBox)view; + } + } + } +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/StudyListDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/StudyListDelegate.java index 885451f5e..3d5b1a01b 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/StudyListDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/StudyListDelegate.java @@ -59,7 +59,7 @@ public class StudyListDelegate extends ListDelegateBase private Activity m_activity; private Spinner m_spinner; - private View m_pickView; // LinearLayout, actually + private LabeledSpinner m_pickView; private int[] m_langCodes; private String[] m_words; private Set m_checkeds; @@ -79,8 +79,8 @@ public class StudyListDelegate extends ListDelegateBase { m_list = (ListView)findViewById( android.R.id.list ); - m_spinner = (Spinner)findViewById( R.id.pick_lang_spinner ); - m_pickView = findViewById( R.id.pick_lang ); + m_pickView = (LabeledSpinner)findViewById( R.id.pick_lang ); + m_spinner = m_pickView.getSpinner(); m_checkeds = new HashSet<>(); m_words = new String[0]; diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/TwoStrsItem.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/TwoStrsItem.java index c2ebcd409..464fd59c3 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/TwoStrsItem.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/TwoStrsItem.java @@ -23,7 +23,6 @@ package org.eehouse.android.xw4; import android.content.Context; import android.util.AttributeSet; import android.view.View; -import android.widget.CheckBox; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.LinearLayout; import android.widget.TextView; diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java index 9a07c43b5..a0cdea535 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java @@ -48,6 +48,7 @@ import android.util.Base64; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; @@ -690,6 +691,21 @@ public class Utils { return Looper.getMainLooper().equals(Looper.myLooper()); } + public static View getChildInstanceOf( ViewGroup parent, Class clazz ) + { + View result = null; + for ( int ii = 0; null == result && ii < parent.getChildCount(); ++ii ) { + View child = parent.getChildAt( ii ); + if ( clazz.isInstance(child) ) { + result = child; + break; + } else if ( child instanceof ViewGroup ) { + result = getChildInstanceOf( (ViewGroup)child, clazz ); + } + } + return result; + } + // But see hexArray above private static final String HEX_CHARS = "0123456789ABCDEF"; private static char[] HEX_CHARS_ARRAY = HEX_CHARS.toCharArray(); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java index 3bac75628..a8a4c5660 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java @@ -22,6 +22,7 @@ package org.eehouse.android.xw4.jni; import android.graphics.Rect; +import java.io.Serializable; import java.util.Arrays; import org.eehouse.android.xw4.Assert; @@ -531,29 +532,130 @@ public class XwJNI { public static boolean dict_getInfo( byte[] dict, String name, String path, boolean check, DictInfo info ) { - return dict_getInfo( getJNI().m_ptrGlobals, dict, name, path, check, info ); + DictWrapper wrapper = makeDict( dict, name, path ); + return dict_getInfo( wrapper, check, info ); + } + + public static boolean dict_getInfo( DictWrapper dict, boolean check, DictInfo info ) + { + return dict_getInfo( getJNI().m_ptrGlobals, dict.getDictPtr(), + check, info ); + } + + public static String dict_getDesc( DictWrapper dict ) + { + return dict_getDesc( dict.getDictPtr() ); + } + + public static String dict_tilesToStr( DictWrapper dict, byte[] tiles, String delim ) + { + return dict_tilesToStr( dict.getDictPtr(), tiles, delim ); + } + + public static byte[][] dict_strToTiles( DictWrapper dict, String str ) + { + return dict_strToTiles( dict.getDictPtr(), str ); + } + + public static boolean dict_hasDuplicates( DictWrapper dict ) + { + return dict_hasDuplicates( dict.getDictPtr() ); + } + + public static String getTilesInfo( DictWrapper dict ) + { + return dict_getTilesInfo( getJNI().m_ptrGlobals, dict.getDictPtr() ); } public static native int dict_getTileValue( long dictPtr, int tile ); // Dict iterator public final static int MAX_COLS_DICT = 15; // from dictiter.h - public static long di_init( byte[] dict, String name, String path ) + public static DictWrapper makeDict( byte[] bytes, String name, String path ) { - return di_init( getJNI().m_ptrGlobals, dict, name, path ); + long dict = dict_make( getJNI().m_ptrGlobals, bytes, name, path ); + return new DictWrapper( dict ); } - public static native void di_setMinMax( long closure, int min, int max ); - public static native void di_destroy( long closure ); - public static native int di_wordCount( long closure ); - public static native int[] di_getCounts( long closure ); - public static native String di_nthWord( long closure, int nn, String delim ); - public static native String[] di_getPrefixes( long closure ); - public static native int[] di_getIndices( long closure ); - public static native byte[][] di_strToTiles( long closure, String str ); - public static native int di_getStartsWith( long closure, byte[][] prefix ); - public static native String di_getDesc( long closure ); - public static native String di_tilesToStr( long closure, byte[] tiles, String delim ); - public static native boolean di_hasDuplicates( long closure ); + + public static class PatDesc implements Serializable { + public String strPat; + public byte[] tilePat; + public boolean anyOrderOk; + + @Override + public String toString() + { + return String.format( "{str: %s; nTiles: %d; anyOrderOk: %b}", + strPat, null == tilePat ? 0 : tilePat.length, + anyOrderOk ); + } + } + + public static class IterWrapper { + private long iterRef; + + private IterWrapper(long ref) { this.iterRef = ref; } + + private long getRef() { return this.iterRef; } + + @Override + public void finalize() throws java.lang.Throwable + { + di_destroy( iterRef ); + super.finalize(); + } + } + + public interface DictIterProcs { + void onIterReady( IterWrapper iterRef ); + } + + public static void di_init( DictWrapper dict, final PatDesc[] pats, + final int minLen, final int maxLen, + final DictIterProcs callback ) + { + final long jniState = getJNI().m_ptrGlobals; + final long dictPtr = dict.getDictPtr(); + new Thread( new Runnable() { + @Override + public void run() { + long iterPtr = di_init( jniState, dictPtr, pats, + minLen, maxLen ); + callback.onIterReady( new IterWrapper(iterPtr) ); + } + } ).start(); + } + + public static int di_wordCount( IterWrapper iter ) + { + return di_wordCount( iter.getRef() ); + } + + public static String di_nthWord( IterWrapper iter, int nn, String delim ) + { + return di_nthWord( iter.getRef(), nn, delim ); + } + + public static int[] di_getMinMax( IterWrapper iter ) { + return di_getMinMax( iter.getRef() ); + } + + public static String[] di_getPrefixes( IterWrapper iter ) + { + return di_getPrefixes( iter.getRef() ); + } + + public static int[] di_getIndices( IterWrapper iter ) + { + return di_getIndices( iter.getRef() ); + } + + private static native void di_destroy( long closure ); + private static native int di_wordCount( long closure ); + private static native String di_nthWord( long closure, int nn, String delim ); + private static native int[] di_getMinMax( long closure ); + private static native String[] di_getPrefixes( long closure ); + private static native int[] di_getIndices( long closure ); // Private methods -- called only here private static native long initGlobals( DUtilCtxt dutil, JNIUtils jniu ); @@ -575,14 +677,18 @@ public class XwJNI { byte[] stream ); private static native long initGameJNI( long jniState, int seed ); private static native void envDone( long globals ); + private static native long dict_make( long jniState, byte[] dict, String name, String path ); private static native void dict_ref( long dictPtr ); private static native void dict_unref( long dictPtr ); - private static native boolean dict_getInfo( long jniState, byte[] dict, - String name, String path, - boolean check, - DictInfo info ); - private static native long di_init( long jniState, byte[] dict, - String name, String path ); + private static native byte[][] dict_strToTiles( long dictPtr, String str ); + private static native String dict_tilesToStr( long dictPtr, byte[] tiles, String delim ); + private static native boolean dict_hasDuplicates( long dictPtr ); + private static native String dict_getTilesInfo( long jniState, long dictPtr ); + private static native boolean dict_getInfo( long jniState, long dictPtr, + boolean check, DictInfo info ); + private static native String dict_getDesc( long dictPtr ); + private static native long di_init( long jniState, long dictPtr, + PatDesc[] pats, int minLen, int maxLen ); private static native byte[][] smsproto_prepOutbound( long jniState, SMS_CMD cmd, int gameID, byte[] buf, diff --git a/xwords4/android/app/src/main/res/layout/dict_browser.xml b/xwords4/android/app/src/main/res/layout/dict_browser.xml index 58f1a6e30..b539b801f 100644 --- a/xwords4/android/app/src/main/res/layout/dict_browser.xml +++ b/xwords4/android/app/src/main/res/layout/dict_browser.xml @@ -1,8 +1,9 @@ - - + + + + + + - - -