2018-07-25 01:59:25 +02:00
|
|
|
/*#######################################################
|
|
|
|
*
|
|
|
|
* Maintained by Gregor Santner, 2017-
|
|
|
|
* https://gsantner.net/
|
|
|
|
*
|
|
|
|
* License: Apache 2.0
|
|
|
|
* https://github.com/gsantner/opoc/#licensing
|
|
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
#########################################################*/
|
|
|
|
package net.gsantner.opoc.ui;
|
|
|
|
|
|
|
|
import android.app.Activity;
|
2019-11-20 00:34:10 +01:00
|
|
|
import android.content.res.ColorStateList;
|
|
|
|
import android.graphics.Color;
|
2018-10-01 21:12:17 +02:00
|
|
|
import android.graphics.Typeface;
|
2019-07-26 03:19:28 +02:00
|
|
|
import android.os.AsyncTask;
|
2019-11-20 00:34:10 +01:00
|
|
|
import android.os.Build;
|
2018-07-25 01:59:25 +02:00
|
|
|
import android.support.annotation.ColorInt;
|
|
|
|
import android.support.annotation.NonNull;
|
|
|
|
import android.support.annotation.Nullable;
|
|
|
|
import android.support.annotation.StringRes;
|
2019-07-26 03:19:28 +02:00
|
|
|
import android.support.design.widget.Snackbar;
|
2018-07-25 01:59:25 +02:00
|
|
|
import android.support.v7.app.AlertDialog;
|
|
|
|
import android.support.v7.widget.AppCompatEditText;
|
|
|
|
import android.text.Editable;
|
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.text.TextWatcher;
|
2019-11-20 00:34:10 +01:00
|
|
|
import android.view.Gravity;
|
2018-07-25 01:59:25 +02:00
|
|
|
import android.view.KeyEvent;
|
|
|
|
import android.view.View;
|
|
|
|
import android.view.ViewGroup;
|
2019-07-26 03:19:28 +02:00
|
|
|
import android.view.Window;
|
2018-10-01 21:12:17 +02:00
|
|
|
import android.view.WindowManager;
|
2018-07-25 01:59:25 +02:00
|
|
|
import android.widget.ArrayAdapter;
|
|
|
|
import android.widget.Filter;
|
|
|
|
import android.widget.LinearLayout;
|
|
|
|
import android.widget.ListView;
|
|
|
|
import android.widget.TextView;
|
|
|
|
|
2019-07-26 03:19:28 +02:00
|
|
|
import net.gsantner.opoc.util.ActivityUtils;
|
2018-07-25 01:59:25 +02:00
|
|
|
import net.gsantner.opoc.util.Callback;
|
|
|
|
import net.gsantner.opoc.util.ContextUtils;
|
|
|
|
|
2019-07-26 03:19:28 +02:00
|
|
|
import org.apache.commons.io.FileUtils;
|
|
|
|
import org.apache.commons.io.filefilter.IOFileFilter;
|
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
import java.lang.ref.WeakReference;
|
2018-07-25 01:59:25 +02:00
|
|
|
import java.util.ArrayList;
|
2019-07-26 03:19:28 +02:00
|
|
|
import java.util.Iterator;
|
2018-07-25 01:59:25 +02:00
|
|
|
import java.util.List;
|
|
|
|
import java.util.Locale;
|
2019-07-26 03:19:28 +02:00
|
|
|
import java.util.regex.Pattern;
|
2018-07-25 01:59:25 +02:00
|
|
|
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
|
|
public class SearchOrCustomTextDialog {
|
|
|
|
|
|
|
|
public static class DialogOptions {
|
|
|
|
public Callback.a1<String> callback;
|
|
|
|
public List<? extends CharSequence> data = new ArrayList<>();
|
|
|
|
public List<? extends CharSequence> highlightData = new ArrayList<>();
|
2019-11-20 00:34:10 +01:00
|
|
|
public List<Integer> iconsForData = new ArrayList<>();
|
2018-07-25 01:59:25 +02:00
|
|
|
public String messageText = "";
|
|
|
|
public boolean isSearchEnabled = true;
|
|
|
|
public boolean isDarkDialog = false;
|
2019-11-20 00:34:10 +01:00
|
|
|
public int dialogWidthDp = WindowManager.LayoutParams.MATCH_PARENT;
|
|
|
|
public int dialogHeightDp = WindowManager.LayoutParams.WRAP_CONTENT;
|
|
|
|
public int gravity = Gravity.NO_GRAVITY;
|
|
|
|
public int searchInputType = 0;
|
2018-07-25 01:59:25 +02:00
|
|
|
|
|
|
|
@ColorInt
|
|
|
|
public int textColor = 0xFF000000;
|
|
|
|
@ColorInt
|
|
|
|
public int highlightColor = 0xFF00FF00;
|
|
|
|
@StringRes
|
|
|
|
public int cancelButtonText = android.R.string.cancel;
|
|
|
|
@StringRes
|
|
|
|
public int okButtonText = android.R.string.ok;
|
|
|
|
@StringRes
|
|
|
|
public int titleText = android.R.string.untitled;
|
|
|
|
@StringRes
|
|
|
|
public int searchHintText = android.R.string.search_go;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void showMultiChoiceDialogWithSearchFilterUI(final Activity activity, final DialogOptions dopt) {
|
|
|
|
final List<CharSequence> allItems = new ArrayList<>(dopt.data);
|
|
|
|
final List<CharSequence> filteredItems = new ArrayList<>(allItems);
|
|
|
|
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity, dopt.isDarkDialog
|
|
|
|
? android.support.v7.appcompat.R.style.Theme_AppCompat_Dialog
|
|
|
|
: android.support.v7.appcompat.R.style.Theme_AppCompat_Light_Dialog
|
|
|
|
);
|
|
|
|
|
|
|
|
final ArrayAdapter<CharSequence> listAdapter = new ArrayAdapter<CharSequence>(activity, android.R.layout.simple_list_item_1, filteredItems) {
|
|
|
|
@NonNull
|
|
|
|
@Override
|
|
|
|
public View getView(int pos, @Nullable View convertView, @NonNull ViewGroup parent) {
|
|
|
|
TextView textView = (TextView) super.getView(pos, convertView, parent);
|
|
|
|
String text = textView.getText().toString();
|
|
|
|
|
2019-11-20 00:34:10 +01:00
|
|
|
int posInOriginalList = dopt.data.indexOf(text);
|
|
|
|
if (posInOriginalList >= 0 && dopt.iconsForData != null && posInOriginalList < dopt.iconsForData.size() && dopt.iconsForData.get(posInOriginalList) != 0) {
|
|
|
|
textView.setCompoundDrawablesWithIntrinsicBounds(dopt.iconsForData.get(posInOriginalList), 0, 0, 0);
|
|
|
|
textView.setCompoundDrawablePadding(32);
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
|
|
textView.setCompoundDrawableTintList(ColorStateList.valueOf(dopt.isDarkDialog ? Color.WHITE : Color.BLACK));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
|
|
|
}
|
|
|
|
|
2018-10-01 21:12:17 +02:00
|
|
|
boolean hl = dopt.highlightData.contains(text);
|
|
|
|
textView.setTextColor(hl ? dopt.highlightColor : dopt.textColor);
|
|
|
|
textView.setTypeface(null, hl ? Typeface.BOLD : Typeface.NORMAL);
|
|
|
|
|
2018-07-25 01:59:25 +02:00
|
|
|
return textView;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Filter getFilter() {
|
|
|
|
return new Filter() {
|
|
|
|
@SuppressWarnings("unchecked")
|
|
|
|
@Override
|
|
|
|
protected void publishResults(final CharSequence constraint, final FilterResults results) {
|
|
|
|
filteredItems.clear();
|
|
|
|
filteredItems.addAll((List<String>) results.values);
|
|
|
|
notifyDataSetChanged();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected FilterResults performFiltering(final CharSequence constraint) {
|
|
|
|
final FilterResults res = new FilterResults();
|
|
|
|
final ArrayList<CharSequence> resList = new ArrayList<>();
|
|
|
|
final String fil = constraint.toString();
|
|
|
|
|
|
|
|
for (final CharSequence str : allItems) {
|
|
|
|
if ("".equals(fil) || str.toString().toLowerCase(Locale.getDefault()).contains(fil.toLowerCase(Locale.getDefault()))) {
|
|
|
|
resList.add(str);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
res.values = resList;
|
|
|
|
res.count = resList.size();
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
final AppCompatEditText searchEditText = new AppCompatEditText(activity);
|
|
|
|
searchEditText.setSingleLine(true);
|
|
|
|
searchEditText.setMaxLines(1);
|
|
|
|
searchEditText.setTextColor(dopt.textColor);
|
|
|
|
searchEditText.setHintTextColor((dopt.textColor & 0x00FFFFFF) | 0x99000000);
|
|
|
|
searchEditText.setHint(dopt.searchHintText);
|
2019-11-20 00:34:10 +01:00
|
|
|
searchEditText.setInputType(dopt.searchInputType == 0 ? searchEditText.getInputType() : dopt.searchInputType);
|
2018-07-25 01:59:25 +02:00
|
|
|
|
|
|
|
searchEditText.addTextChangedListener(new TextWatcher() {
|
|
|
|
@Override
|
|
|
|
public void afterTextChanged(final Editable arg0) {
|
|
|
|
listAdapter.getFilter().filter(searchEditText.getText());
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onTextChanged(final CharSequence arg0, final int arg1, final int arg2, final int arg3) {
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void beforeTextChanged(final CharSequence arg0, final int arg1, final int arg2, final int arg3) {
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
final ListView listView = new ListView(activity);
|
|
|
|
final LinearLayout linearLayout = new LinearLayout(activity);
|
|
|
|
listView.setAdapter(listAdapter);
|
2019-07-26 03:19:28 +02:00
|
|
|
listView.setVisibility(dopt.data != null && !dopt.data.isEmpty() ? View.VISIBLE : View.GONE);
|
2018-07-25 01:59:25 +02:00
|
|
|
linearLayout.setOrientation(LinearLayout.VERTICAL);
|
|
|
|
if (dopt.isSearchEnabled) {
|
|
|
|
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
2019-08-24 14:54:25 +02:00
|
|
|
int px = (int) (new ContextUtils(listView.getContext()).convertDpToPx(8));
|
2018-07-25 01:59:25 +02:00
|
|
|
lp.setMargins(px, px / 2, px, px / 2);
|
|
|
|
linearLayout.addView(searchEditText, lp);
|
|
|
|
}
|
|
|
|
final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0);
|
|
|
|
layoutParams.weight = 1;
|
|
|
|
linearLayout.addView(listView, layoutParams);
|
|
|
|
if (!TextUtils.isEmpty(dopt.messageText)) {
|
|
|
|
dialogBuilder.setMessage(dopt.messageText);
|
|
|
|
}
|
|
|
|
dialogBuilder.setView(linearLayout)
|
|
|
|
.setOnCancelListener(null)
|
2019-07-26 03:19:28 +02:00
|
|
|
.setNegativeButton(dopt.cancelButtonText, (dialogInterface, i) -> dialogInterface.dismiss());
|
2019-11-20 00:34:10 +01:00
|
|
|
if (dopt.titleText != 0) {
|
|
|
|
dialogBuilder.setTitle(dopt.titleText);
|
|
|
|
}
|
2018-07-25 01:59:25 +02:00
|
|
|
if (dopt.isSearchEnabled) {
|
|
|
|
dialogBuilder.setPositiveButton(dopt.okButtonText, (dialogInterface, i) -> {
|
|
|
|
dialogInterface.dismiss();
|
|
|
|
if (dopt.callback != null && !TextUtils.isEmpty(searchEditText.getText().toString())) {
|
|
|
|
dopt.callback.callback(searchEditText.getText().toString());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
final AlertDialog dialog = dialogBuilder.create();
|
|
|
|
listView.setOnItemClickListener((parent, view, position, id) -> {
|
|
|
|
dialog.dismiss();
|
|
|
|
if (dopt.callback != null) {
|
|
|
|
dopt.callback.callback(filteredItems.get(position).toString());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
searchEditText.setOnKeyListener((keyView, keyCode, keyEvent) -> {
|
|
|
|
if ((keyEvent.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
|
|
|
|
dialog.dismiss();
|
|
|
|
if (dopt.callback != null && !TextUtils.isEmpty(searchEditText.getText().toString())) {
|
|
|
|
dopt.callback.callback(searchEditText.getText().toString());
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
2019-07-26 03:19:28 +02:00
|
|
|
|
|
|
|
Window w;
|
|
|
|
if ((w = dialog.getWindow()) != null && dopt.isSearchEnabled) {
|
|
|
|
w.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
2018-10-01 21:12:17 +02:00
|
|
|
}
|
2018-07-25 01:59:25 +02:00
|
|
|
dialog.show();
|
2019-07-26 03:19:28 +02:00
|
|
|
if ((w = dialog.getWindow()) != null) {
|
2019-11-20 00:34:10 +01:00
|
|
|
int ds_w = dopt.dialogWidthDp < 100 ? dopt.dialogWidthDp : ((int) (dopt.dialogWidthDp * activity.getResources().getDisplayMetrics().density));
|
|
|
|
int ds_h = dopt.dialogHeightDp < 100 ? dopt.dialogHeightDp : ((int) (dopt.dialogHeightDp * activity.getResources().getDisplayMetrics().density));
|
|
|
|
w.setLayout(ds_w, ds_h);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((w = dialog.getWindow()) != null && dopt.gravity != Gravity.NO_GRAVITY) {
|
|
|
|
WindowManager.LayoutParams wlp = w.getAttributes();
|
|
|
|
wlp.gravity = dopt.gravity;
|
|
|
|
w.setAttributes(wlp);
|
2019-07-26 03:19:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (dopt.isSearchEnabled) {
|
|
|
|
searchEditText.requestFocus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static SearchFilesTask recursiveFileSearch(Activity activity, File searchDir, String query, Callback.a1<List<String>> callback) {
|
|
|
|
query = query.replaceAll("(?<![.])[*]", ".*");
|
|
|
|
SearchFilesTask task = new SearchFilesTask(activity, searchDir, query, callback, query.startsWith("^") || query.contains("*"));
|
|
|
|
task.execute();
|
|
|
|
return task;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static class SearchFilesTask extends AsyncTask<Void, File, List<String>> implements IOFileFilter {
|
|
|
|
private final Callback.a1<List<String>> _callback;
|
|
|
|
private final File _searchDir;
|
|
|
|
private final String _query;
|
|
|
|
private final boolean _isRegex;
|
|
|
|
private final WeakReference<Activity> _activityRef;
|
|
|
|
|
|
|
|
private final Pattern _regex;
|
|
|
|
private Snackbar _snackBar;
|
|
|
|
|
|
|
|
public SearchFilesTask(Activity activity, File searchDir, String query, Callback.a1<List<String>> callback, boolean isRegex) {
|
|
|
|
_searchDir = searchDir;
|
|
|
|
_query = isRegex ? query : query.toLowerCase();
|
|
|
|
_callback = callback;
|
|
|
|
_isRegex = isRegex;
|
|
|
|
_regex = isRegex ? Pattern.compile(_query) : null;
|
|
|
|
_activityRef = new WeakReference<>(activity);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Called for both, file and folder filter
|
|
|
|
@Override
|
|
|
|
public boolean accept(File file) {
|
|
|
|
return isMatching(file, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not called
|
|
|
|
@Override
|
|
|
|
public boolean accept(File dir, String name) {
|
|
|
|
return isMatching(new File(dir, name), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// In iterateFilesAndDirs, subdirs are only scanned when returning true on it
|
|
|
|
// But those dirs will also occur in iterator
|
|
|
|
// Hence call this aagain with alwaysMatchDir=false
|
|
|
|
public boolean isMatching(File file, boolean alwaysMatchDir) {
|
|
|
|
if (file.isDirectory()) {
|
|
|
|
// Do never scan .git directories, lots of files, lots of time
|
|
|
|
if (file.getName().equals(".git")) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (alwaysMatchDir) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
String name = file.getName();
|
|
|
|
file = file.getParentFile();
|
|
|
|
return _isRegex ? _regex.matcher(name).matches() : name.toLowerCase().contains(_query);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onPreExecute() {
|
|
|
|
super.onPreExecute();
|
|
|
|
if (_activityRef.get() != null) {
|
|
|
|
_snackBar = Snackbar.make(_activityRef.get().findViewById(android.R.id.content), _query + "...", Snackbar.LENGTH_INDEFINITE);
|
|
|
|
_snackBar.setAction(android.R.string.cancel, (v) -> {
|
|
|
|
_snackBar.dismiss();
|
|
|
|
cancel(true);
|
|
|
|
}).show();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected List<String> doInBackground(Void... voidp) {
|
|
|
|
List<String> ret = new ArrayList<>();
|
|
|
|
|
|
|
|
boolean first = true;
|
2019-08-24 14:54:25 +02:00
|
|
|
Iterator<File> iter = null;
|
|
|
|
try {
|
|
|
|
iter = FileUtils.iterateFilesAndDirs(_searchDir, this, this);
|
|
|
|
} catch (Exception ex) {
|
|
|
|
// Iterator may throw an error at creation
|
|
|
|
return ret;
|
|
|
|
}
|
2019-07-26 03:19:28 +02:00
|
|
|
while (iter.hasNext() && !isCancelled()) {
|
|
|
|
File f = iter.next();
|
|
|
|
if (first) {
|
|
|
|
first = false;
|
|
|
|
if (f.equals(_searchDir)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (f.isFile() || (f.isDirectory() && isMatching(f, false))) {
|
|
|
|
ret.add(f.getAbsolutePath().replace(_searchDir.getAbsolutePath() + "/", ""));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onPostExecute(List<String> ret) {
|
|
|
|
super.onPostExecute(ret);
|
|
|
|
if (_snackBar != null) {
|
|
|
|
_snackBar.dismiss();
|
|
|
|
}
|
|
|
|
if (_callback != null) {
|
|
|
|
try {
|
|
|
|
_callback.callback(ret);
|
|
|
|
} catch (Exception ignored) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
new ActivityUtils(_activityRef.get()).hideSoftKeyboard().freeContextRef();
|
|
|
|
}
|
2018-07-25 01:59:25 +02:00
|
|
|
}
|
|
|
|
}
|