/*####################################################### * * 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; import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.Typeface; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; import android.support.v7.app.AlertDialog; import android.support.v7.widget.AppCompatEditText; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.ArrayAdapter; import android.widget.Filter; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import net.gsantner.opoc.util.ActivityUtils; import net.gsantner.opoc.util.Callback; import net.gsantner.opoc.util.ContextUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.IOFileFilter; import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.regex.Pattern; @SuppressWarnings("WeakerAccess") public class SearchOrCustomTextDialog { public static class DialogOptions { public Callback.a1 callback; public List data = new ArrayList<>(); public List highlightData = new ArrayList<>(); public List iconsForData = new ArrayList<>(); public String messageText = ""; public boolean isSearchEnabled = true; public boolean isDarkDialog = false; public int dialogWidthDp = WindowManager.LayoutParams.MATCH_PARENT; public int dialogHeightDp = WindowManager.LayoutParams.WRAP_CONTENT; public int gravity = Gravity.NO_GRAVITY; public int searchInputType = 0; @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 allItems = new ArrayList<>(dopt.data); final List 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 listAdapter = new ArrayAdapter(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(); 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); } boolean hl = dopt.highlightData.contains(text); textView.setTextColor(hl ? dopt.highlightColor : dopt.textColor); textView.setTypeface(null, hl ? Typeface.BOLD : Typeface.NORMAL); 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) results.values); notifyDataSetChanged(); } @Override protected FilterResults performFiltering(final CharSequence constraint) { final FilterResults res = new FilterResults(); final ArrayList 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); searchEditText.setInputType(dopt.searchInputType == 0 ? searchEditText.getInputType() : dopt.searchInputType); 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); listView.setVisibility(dopt.data != null && !dopt.data.isEmpty() ? View.VISIBLE : View.GONE); linearLayout.setOrientation(LinearLayout.VERTICAL); if (dopt.isSearchEnabled) { LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); int px = (int) (new ContextUtils(listView.getContext()).convertDpToPx(8)); 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) .setNegativeButton(dopt.cancelButtonText, (dialogInterface, i) -> dialogInterface.dismiss()); if (dopt.titleText != 0) { dialogBuilder.setTitle(dopt.titleText); } 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; }); Window w; if ((w = dialog.getWindow()) != null && dopt.isSearchEnabled) { w.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); } dialog.show(); if ((w = dialog.getWindow()) != null) { 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); } if (dopt.isSearchEnabled) { searchEditText.requestFocus(); } } public static SearchFilesTask recursiveFileSearch(Activity activity, File searchDir, String query, Callback.a1> callback) { query = query.replaceAll("(?> implements IOFileFilter { private final Callback.a1> _callback; private final File _searchDir; private final String _query; private final boolean _isRegex; private final WeakReference _activityRef; private final Pattern _regex; private Snackbar _snackBar; public SearchFilesTask(Activity activity, File searchDir, String query, Callback.a1> 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 doInBackground(Void... voidp) { List ret = new ArrayList<>(); boolean first = true; Iterator iter = null; try { iter = FileUtils.iterateFilesAndDirs(_searchDir, this, this); } catch (Exception ex) { // Iterator may throw an error at creation return ret; } 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 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(); } } }