mirror of
https://github.com/peterosterlund2/droidfish.git
synced 2024-11-23 11:31:33 +01:00
Improve behavior when searching/filtering PGN games
When loading a PGN game from a file a list is displayed with one item for each game in the file. Filtering of this list has been improved in several ways: * Made it possible to search for part of a word and to include space characters in the search. * While changing the search string keep the top list element unchanged when possible. * When using the "load from last file" action to go to the game list, start at the position in the list corresponding to the previously loaded game. * Make the "load next/previous game" actions load the correct game also when a filter is in effect. * Use correct text color after a game has been deleted from the game list.
This commit is contained in:
parent
aadae0674b
commit
5a0493a4e6
|
@ -36,7 +36,8 @@ import android.view.Menu;
|
|||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.Filter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
@ -62,10 +63,11 @@ public abstract class EditPGN extends ListActivity {
|
|||
private PGNFile pgnFile;
|
||||
private ProgressDialog progress;
|
||||
private GameInfo selectedGi = null;
|
||||
private ArrayAdapter<GameInfo> aa = null;
|
||||
private GameAdapter<GameInfo> aa = null;
|
||||
|
||||
private SharedPreferences settings;
|
||||
private int defaultItem = 0;
|
||||
private long defaultFilePos = 0;
|
||||
private long currentFilePos = 0;
|
||||
private String lastSearchString = "";
|
||||
private String lastFileName = "";
|
||||
private long lastModTime = -1;
|
||||
|
@ -86,14 +88,14 @@ public abstract class EditPGN extends ListActivity {
|
|||
Util.setFullScreenMode(this, settings);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
defaultItem = savedInstanceState.getInt("defaultItem");
|
||||
defaultFilePos = savedInstanceState.getLong("defaultFilePos");
|
||||
lastSearchString = savedInstanceState.getString("lastSearchString");
|
||||
if (lastSearchString == null) lastSearchString = "";
|
||||
lastFileName = savedInstanceState.getString("lastFileName");
|
||||
if (lastFileName == null) lastFileName = "";
|
||||
lastModTime = savedInstanceState.getLong("lastModTime");
|
||||
} else {
|
||||
defaultItem = settings.getInt("defaultItem", 0);
|
||||
defaultFilePos = settings.getLong("defaultFilePos", 0);
|
||||
lastSearchString = settings.getString("lastSearchString", "");
|
||||
lastFileName = settings.getString("lastFileName", "");
|
||||
lastModTime = settings.getLong("lastModTime", 0);
|
||||
|
@ -126,28 +128,37 @@ public abstract class EditPGN extends ListActivity {
|
|||
pgnFile = new PGNFile(fileName);
|
||||
loadGame = true;
|
||||
boolean next = action.equals("org.petero.droidfish.loadFileNextGame");
|
||||
final int loadItem = defaultItem + (next ? 1 : -1);
|
||||
if (loadItem < 0) {
|
||||
DroidFishApp.toast(R.string.no_prev_game, Toast.LENGTH_SHORT);
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
} else {
|
||||
workThread = new Thread(() -> {
|
||||
if (!readFile())
|
||||
return;
|
||||
runOnUiThread(() -> {
|
||||
if (loadItem >= gamesInFile.size()) {
|
||||
DroidFishApp.toast(R.string.no_next_game, Toast.LENGTH_SHORT);
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
} else {
|
||||
defaultItem = loadItem;
|
||||
sendBackResult(gamesInFile.get(loadItem));
|
||||
}
|
||||
});
|
||||
workThread = new Thread(() -> {
|
||||
if (!readFile())
|
||||
return;
|
||||
int itemNo = getItemNo(gamesInFile, defaultFilePos) + (next ? 1 : -1);
|
||||
if (next) {
|
||||
while (itemNo < gamesInFile.size() &&
|
||||
!GameAdapter.matchItem(gamesInFile.get(itemNo), lastSearchString))
|
||||
itemNo++;
|
||||
} else {
|
||||
while (itemNo >= 0 &&
|
||||
!GameAdapter.matchItem(gamesInFile.get(itemNo), lastSearchString))
|
||||
itemNo--;
|
||||
}
|
||||
final int loadItem = itemNo;
|
||||
runOnUiThread(() -> {
|
||||
if (loadItem < 0) {
|
||||
DroidFishApp.toast(R.string.no_prev_game, Toast.LENGTH_SHORT);
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
} else if (loadItem >= gamesInFile.size()) {
|
||||
DroidFishApp.toast(R.string.no_next_game, Toast.LENGTH_SHORT);
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
} else {
|
||||
GameInfo gi = gamesInFile.get(loadItem);
|
||||
defaultFilePos = gi.startPos;
|
||||
sendBackResult(gi);
|
||||
}
|
||||
});
|
||||
workThread.start();
|
||||
}
|
||||
});
|
||||
workThread.start();
|
||||
} else if ("org.petero.droidfish.saveFile".equals(action)) {
|
||||
loadGame = false;
|
||||
String token = i.getStringExtra("org.petero.droidfish.pgn");
|
||||
|
@ -191,7 +202,7 @@ public abstract class EditPGN extends ListActivity {
|
|||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("defaultItem", defaultItem);
|
||||
outState.putLong("defaultFilePos", defaultFilePos);
|
||||
outState.putString("lastSearchString", lastSearchString);
|
||||
outState.putString("lastFileName", lastFileName);
|
||||
outState.putLong("lastModTime", lastModTime);
|
||||
|
@ -200,7 +211,7 @@ public abstract class EditPGN extends ListActivity {
|
|||
@Override
|
||||
protected void onPause() {
|
||||
Editor editor = settings.edit();
|
||||
editor.putInt("defaultItem", defaultItem);
|
||||
editor.putLong("defaultFilePos", defaultFilePos);
|
||||
editor.putString("lastSearchString", lastSearchString);
|
||||
editor.putString("lastFileName", lastFileName);
|
||||
editor.putLong("lastModTime", lastModTime);
|
||||
|
@ -242,27 +253,18 @@ public abstract class EditPGN extends ListActivity {
|
|||
removeDialog(PROGRESS_DIALOG);
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.select_game);
|
||||
Util.overrideViewAttribs(findViewById(android.R.id.content));
|
||||
aa = new ArrayAdapter<GameInfo>(this, R.layout.select_game_list_item, gamesInFile) {
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
if (view instanceof TextView) {
|
||||
int fg = ColorTheme.instance().getColor(ColorTheme.FONT_FOREGROUND);
|
||||
((TextView) view).setTextColor(fg);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
};
|
||||
setListAdapter(aa);
|
||||
createAdapter();
|
||||
ListView lv = getListView();
|
||||
lv.setSelectionFromTop(defaultItem, 0);
|
||||
currentFilePos = defaultFilePos;
|
||||
int itemNo = getItemNo(gamesInFile, defaultFilePos);
|
||||
lv.setSelectionFromTop(itemNo, 0);
|
||||
lv.setFastScrollEnabled(true);
|
||||
lv.setOnItemClickListener((parent, view, pos, id) -> {
|
||||
selectedGi = aa.getItem(pos);
|
||||
if (selectedGi == null)
|
||||
return;
|
||||
if (loadGame) {
|
||||
defaultItem = pos;
|
||||
defaultFilePos = selectedGi.startPos;
|
||||
sendBackResult(selectedGi);
|
||||
} else {
|
||||
reShowDialog(SAVE_GAME_DIALOG);
|
||||
|
@ -274,6 +276,17 @@ public abstract class EditPGN extends ListActivity {
|
|||
reShowDialog(DELETE_GAME_DIALOG);
|
||||
return true;
|
||||
});
|
||||
lv.setOnScrollListener(new AbsListView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(AbsListView view, int scrollState) {
|
||||
}
|
||||
@Override
|
||||
public void onScroll(AbsListView view, int firstVisibleItem,
|
||||
int visibleItemCount, int totalItemCount) {
|
||||
if (visibleItemCount > 0)
|
||||
currentFilePos = aa.getItem(firstVisibleItem).startPos;
|
||||
}
|
||||
});
|
||||
|
||||
binding.selectGameFilter.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
|
@ -284,8 +297,9 @@ public abstract class EditPGN extends ListActivity {
|
|||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
aa.getFilter().filter(s);
|
||||
lastSearchString = s.toString();
|
||||
String fs = s.toString();
|
||||
setFilterString(fs);
|
||||
lastSearchString = fs;
|
||||
}
|
||||
});
|
||||
binding.selectGameFilter.setText(lastSearchString);
|
||||
|
@ -399,7 +413,7 @@ public abstract class EditPGN extends ListActivity {
|
|||
private boolean readFile() {
|
||||
String fileName = pgnFile.getName();
|
||||
if (!fileName.equals(lastFileName))
|
||||
defaultItem = 0;
|
||||
defaultFilePos = 0;
|
||||
long modTime = new File(fileName).lastModified();
|
||||
if (cacheValid && (modTime == lastModTime) && fileName.equals(lastFileName))
|
||||
return true;
|
||||
|
@ -444,14 +458,59 @@ public abstract class EditPGN extends ListActivity {
|
|||
if (pgnFile.deleteGame(gi, gamesInFile)) {
|
||||
ListView lv = getListView();
|
||||
int pos = lv.pointToPosition(0, 0);
|
||||
aa = new ArrayAdapter<>(this, R.layout.select_game_list_item, gamesInFile);
|
||||
setListAdapter(aa);
|
||||
createAdapter();
|
||||
String s = binding.selectGameFilter.getText().toString();
|
||||
aa.getFilter().filter(s);
|
||||
setFilterString(s);
|
||||
lv.setSelection(pos);
|
||||
// Update lastModTime, since current change has already been handled
|
||||
String fileName = pgnFile.getName();
|
||||
lastModTime = new File(fileName).lastModified();
|
||||
}
|
||||
}
|
||||
|
||||
private void createAdapter() {
|
||||
aa = new GameAdapter<GameInfo>(this, R.layout.select_game_list_item, gamesInFile) {
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
if (view instanceof TextView) {
|
||||
int fg = ColorTheme.instance().getColor(ColorTheme.FONT_FOREGROUND);
|
||||
((TextView) view).setTextColor(fg);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
};
|
||||
setListAdapter(aa);
|
||||
}
|
||||
|
||||
private void setFilterString(String s) {
|
||||
Filter.FilterListener listener = (count) -> {
|
||||
ArrayList<GameInfo> arr = aa.getValues();
|
||||
int itemNo = getItemNo(arr, currentFilePos);
|
||||
if (itemNo < 0)
|
||||
itemNo = 0;
|
||||
while (itemNo < arr.size() &&
|
||||
!GameAdapter.matchItem(arr.get(itemNo), lastSearchString))
|
||||
itemNo++;
|
||||
if (itemNo < arr.size())
|
||||
getListView().setSelectionFromTop(itemNo, 0);
|
||||
};
|
||||
aa.getFilter().filter(s, listener);
|
||||
}
|
||||
|
||||
/** Return index in "games" corresponding to a file position. */
|
||||
private static int getItemNo(ArrayList<GameInfo> games, long filePos) {
|
||||
int lo = -1;
|
||||
int hi = games.size();
|
||||
// games[lo].startPos <= filePos < games[hi].startPos
|
||||
while (hi - lo > 1) {
|
||||
int mid = (lo + hi) / 2;
|
||||
long val = games.get(mid).startPos;
|
||||
if (filePos < val)
|
||||
hi = mid;
|
||||
else
|
||||
lo = mid;
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
DroidFish - An Android chess program.
|
||||
Copyright (C) 2019 Peter Österlund, peterosterlund2@gmail.com
|
||||
|
||||
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 3 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.petero.droidfish.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* An adapter for displaying an ArrayList<GameInfo> in a ListView.
|
||||
*/
|
||||
public class GameAdapter<T> extends BaseAdapter implements Filterable {
|
||||
private ArrayList<T> origValues; // Unfiltered values
|
||||
private ArrayList<T> values; // Filtered values. Equal to origValues if no filter used
|
||||
private final LayoutInflater inflater;
|
||||
private int resource;
|
||||
private GameFilter filter; // Initialized at first use
|
||||
|
||||
public GameAdapter(Context context, int resource, ArrayList<T> objects) {
|
||||
origValues = objects;
|
||||
values = objects;
|
||||
inflater = LayoutInflater.from(context);
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
public ArrayList<T> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return values.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getItem(int position) {
|
||||
return values.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
TextView view;
|
||||
if (convertView == null)
|
||||
view = (TextView) inflater.inflate(resource, parent, false);
|
||||
else
|
||||
view = (TextView) convertView;
|
||||
view.setText(getItem(position).toString());
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
if (filter == null)
|
||||
filter = new GameFilter();
|
||||
return filter;
|
||||
}
|
||||
|
||||
private class GameFilter extends Filter {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence constraint) {
|
||||
FilterResults res = new FilterResults();
|
||||
if (constraint == null || constraint.length() == 0) {
|
||||
res.values = origValues;
|
||||
res.count = origValues.size();
|
||||
} else {
|
||||
String s = constraint.toString().toLowerCase();
|
||||
ArrayList<T> newValues = new ArrayList<>();
|
||||
for (T item : origValues)
|
||||
if (matchItem(item, s))
|
||||
newValues.add(item);
|
||||
res.values = newValues;
|
||||
res.count = newValues.size();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||
values = (ArrayList<T>) results.values;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/** Return true if matchStr matches item.
|
||||
* @param item The item to check. The toString() value converted to lowercase is used.
|
||||
* @param matchStr The match string. Must be lowercase. */
|
||||
static <U> boolean matchItem(U item, String matchStr) {
|
||||
return item.toString().toLowerCase().contains(matchStr);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user