diff --git a/DroidFishApp/src/main/java/org/petero/droidfish/activities/EditPGN.java b/DroidFishApp/src/main/java/org/petero/droidfish/activities/EditPGN.java index a3c93e6..a52e23a 100644 --- a/DroidFishApp/src/main/java/org/petero/droidfish/activities/EditPGN.java +++ b/DroidFishApp/src/main/java/org/petero/droidfish/activities/EditPGN.java @@ -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 aa = null; + private GameAdapter 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(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(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 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 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; + } } diff --git a/DroidFishApp/src/main/java/org/petero/droidfish/activities/GameAdapter.java b/DroidFishApp/src/main/java/org/petero/droidfish/activities/GameAdapter.java new file mode 100644 index 0000000..ba72574 --- /dev/null +++ b/DroidFishApp/src/main/java/org/petero/droidfish/activities/GameAdapter.java @@ -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 . +*/ + +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 in a ListView. + */ +public class GameAdapter extends BaseAdapter implements Filterable { + private ArrayList origValues; // Unfiltered values + private ArrayList 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 objects) { + origValues = objects; + values = objects; + inflater = LayoutInflater.from(context); + this.resource = resource; + } + + public ArrayList 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 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) 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 boolean matchItem(U item, String matchStr) { + return item.toString().toLowerCase().contains(matchStr); + } +}