Improved text search

This commit is contained in:
M66B 2021-07-01 16:35:20 +02:00
parent ef71112629
commit b6b1b2a582
5 changed files with 172 additions and 147 deletions

View File

@ -55,18 +55,14 @@ import android.os.Parcelable;
import android.provider.CalendarContract; import android.provider.CalendarContract;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.provider.Settings; import android.provider.Settings;
import android.text.Editable;
import android.text.Html; import android.text.Html;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.text.method.ArrowKeyMovementMethod; import android.text.method.ArrowKeyMovementMethod;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.text.style.BackgroundColorSpan;
import android.text.style.DynamicDrawableSpan; import android.text.style.DynamicDrawableSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan; import android.text.style.ImageSpan;
@ -75,7 +71,6 @@ import android.text.style.StyleSpan;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.util.Pair; import android.util.Pair;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.Gravity;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@ -93,7 +88,6 @@ import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.inputmethod.InputMethodManager;
import android.view.textclassifier.ConversationAction; import android.view.textclassifier.ConversationAction;
import android.view.textclassifier.ConversationActions; import android.view.textclassifier.ConversationActions;
import android.webkit.WebSettings; import android.webkit.WebSettings;
@ -106,7 +100,6 @@ import android.widget.CompoundButton;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -196,7 +189,7 @@ import biweekly.property.RawProperty;
import biweekly.util.ICalDate; import biweekly.util.ICalDate;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static androidx.webkit.WebSettingsCompat.FORCE_DARK_OFF; import static androidx.webkit.WebSettingsCompat.FORCE_DARK_OFF;
import static androidx.webkit.WebSettingsCompat.FORCE_DARK_ON; import static androidx.webkit.WebSettingsCompat.FORCE_DARK_ON;
@ -289,7 +282,6 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
private boolean gotoTop = false; private boolean gotoTop = false;
private Integer gotoPos = null; private Integer gotoPos = null;
private boolean firstClick = false; private boolean firstClick = false;
private int searchResult = 0;
private AsyncPagedListDiffer<TupleMessageEx> differ; private AsyncPagedListDiffer<TupleMessageEx> differ;
private Map<Long, Integer> keyPosition = new HashMap<>(); private Map<Long, Integer> keyPosition = new HashMap<>();
private Map<Integer, Long> positionKey = new HashMap<>(); private Map<Integer, Long> positionKey = new HashMap<>();
@ -2043,6 +2035,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
tvBody.setTag(message.id); tvBody.setTag(message.id);
tvBody.setText(null); tvBody.setText(null);
} }
properties.endSearch();
clearActions(); clearActions();
ibSeenBottom.setImageResource(message.ui_seen ibSeenBottom.setImageResource(message.ui_seen
@ -4867,108 +4860,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
} }
private void onSearchText(TupleMessageEx message) { private void onSearchText(TupleMessageEx message) {
LayoutInflater inflater = LayoutInflater.from(context); properties.startSearch(tvBody);
View dview = inflater.inflate(R.layout.popup_search_in_text, null, false);
EditText etSearch = dview.findViewById(R.id.etSearch);
ImageButton ibNext = dview.findViewById(R.id.ibNext);
etSearch.setText(null);
ibNext.setEnabled(false);
etSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// Do nothing
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
searchResult = find(s.toString(), 1);
ibNext.setEnabled(searchResult > 0);
}
@Override
public void afterTextChanged(Editable s) {
// Do nothing
}
});
ibNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
searchResult = find(etSearch.getText().toString(), ++searchResult);
}
});
PopupWindow pw = new PopupWindow(dview, WRAP_CONTENT, WRAP_CONTENT);
pw.setFocusable(true);
pw.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
SpannableString ss = new SpannableString(tvBody.getText());
for (BackgroundColorSpan span : ss.getSpans(0, ss.length(), BackgroundColorSpan.class))
ss.removeSpan(span);
tvBody.setText(ss);
}
});
pw.showAtLocation(parentFragment.getView(), Gravity.TOP | Gravity.END, 0, 0);
final InputMethodManager imm =
(InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null)
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
}
private int find(String query, int result) {
query = query.toLowerCase();
SpannableString ss = new SpannableString(tvBody.getText());
for (BackgroundColorSpan span : ss.getSpans(0, ss.length(), BackgroundColorSpan.class))
ss.removeSpan(span);
int p = -1;
String text = tvBody.getText().toString().toLowerCase();
for (int i = 0; i < result; i++)
p = (p < 0 ? text.indexOf(query) : text.indexOf(query, p + 1));
if (p < 0 && result > 1) {
result = 1;
p = text.indexOf(query);
}
if (p < 0)
result = 0;
final int pos = p;
if (pos > 0) {
int color = Helper.resolveColor(context, R.attr.colorHighlight);
ss.setSpan(new BackgroundColorSpan(color), pos, pos + query.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tvBody.setText(ss);
final int apos = getAdapterPosition();
tvBody.post(new Runnable() {
@Override
public void run() {
try {
int line = tvBody.getLayout().getLineForOffset(pos);
int y = tvBody.getLayout().getLineTop(line);
int dy = Helper.dp2pixels(context, 48);
Rect rect = new Rect();
tvBody.getDrawingRect(rect);
((ViewGroup) itemView).offsetDescendantRectToMyCoords(tvBody, rect);
properties.scrollTo(apos, rect.top + y - dy);
} catch (Throwable ex) {
Log.e(ex);
}
}
});
} else
tvBody.setText(ss, TextView.BufferType.SPANNABLE);
return result;
} }
private void onMenuCreateRule(TupleMessageEx message) { private void onMenuCreateRule(TupleMessageEx message) {
@ -6496,6 +6388,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
void reply(TupleMessageEx message, String selected, View anchor); void reply(TupleMessageEx message, String selected, View anchor);
void startSearch(TextView view);
void endSearch();
void lock(long id); void lock(long id);
void refresh(); void refresh();
@ -7040,9 +6936,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
super.onStart(); super.onStart();
Dialog dialog = getDialog(); Dialog dialog = getDialog();
if (dialog != null) if (dialog != null)
dialog.getWindow().setLayout( dialog.getWindow().setLayout(MATCH_PARENT, MATCH_PARENT);
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
} }
@Override @Override

View File

@ -59,10 +59,14 @@ import android.provider.ContactsContract;
import android.provider.Settings; import android.provider.Settings;
import android.security.KeyChain; import android.security.KeyChain;
import android.security.KeyChainException; import android.security.KeyChainException;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan; import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
@ -82,6 +86,8 @@ import android.view.ViewGroup;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.view.animation.TranslateAnimation; import android.view.animation.TranslateAnimation;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
@ -243,6 +249,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
private ImageButton ibHintSupport; private ImageButton ibHintSupport;
private ImageButton ibHintSwipe; private ImageButton ibHintSwipe;
private ImageButton ibHintSelect; private ImageButton ibHintSelect;
private TextViewAutoCompleteAction etSearch;
private TextView tvNoEmail; private TextView tvNoEmail;
private TextView tvNoEmailHint; private TextView tvNoEmailHint;
private FixedRecyclerView rvMessage; private FixedRecyclerView rvMessage;
@ -283,6 +290,9 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
private BoundaryCallbackMessages.SearchCriteria criteria = null; private BoundaryCallbackMessages.SearchCriteria criteria = null;
private boolean pane; private boolean pane;
private int searchIndex = 0;
private TextView searchView = null;
private WebView printWebView = null; private WebView printWebView = null;
private OpenPgpServiceConnection pgpService; private OpenPgpServiceConnection pgpService;
@ -462,6 +472,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
ibHintSupport = view.findViewById(R.id.ibHintSupport); ibHintSupport = view.findViewById(R.id.ibHintSupport);
ibHintSwipe = view.findViewById(R.id.ibHintSwipe); ibHintSwipe = view.findViewById(R.id.ibHintSwipe);
ibHintSelect = view.findViewById(R.id.ibHintSelect); ibHintSelect = view.findViewById(R.id.ibHintSelect);
etSearch = view.findViewById(R.id.etSearch);
tvNoEmail = view.findViewById(R.id.tvNoEmail); tvNoEmail = view.findViewById(R.id.tvNoEmail);
tvNoEmailHint = view.findViewById(R.id.tvNoEmailHint); tvNoEmailHint = view.findViewById(R.id.tvNoEmailHint);
rvMessage = view.findViewById(R.id.rvMessage); rvMessage = view.findViewById(R.id.rvMessage);
@ -537,6 +548,49 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
} }
}); });
etSearch.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus)
endSearch();
}
});
etSearch.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
endSearch();
return true;
} else
return false;
}
});
etSearch.setActionRunnable(new Runnable() {
@Override
public void run() {
performSearch(true);
}
});
etSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// Do nothing
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
performSearch(false);
}
@Override
public void afterTextChanged(Editable s) {
// Do nothing
}
});
rvMessage.setHasFixedSize(false); rvMessage.setHasFixedSize(false);
int threads = prefs.getInt("query_threads", 4); int threads = prefs.getInt("query_threads", 4);
@ -1147,6 +1201,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
FragmentDialogTheme.setBackground(getContext(), view, false); FragmentDialogTheme.setBackground(getContext(), view, false);
tvNoEmail.setVisibility(View.GONE); tvNoEmail.setVisibility(View.GONE);
tvNoEmailHint.setVisibility(View.GONE); tvNoEmailHint.setVisibility(View.GONE);
etSearch.setVisibility(View.GONE);
sbThread.setVisibility(View.GONE); sbThread.setVisibility(View.GONE);
ibDown.setVisibility(View.GONE); ibDown.setVisibility(View.GONE);
ibUp.setVisibility(View.GONE); ibUp.setVisibility(View.GONE);
@ -1857,6 +1912,14 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
onReply(message, selected, anchor); onReply(message, selected, anchor);
} }
public void startSearch(TextView view) {
FragmentMessages.this.startSearch(view);
}
public void endSearch() {
FragmentMessages.this.endSearch();
}
@Override @Override
public void lock(long id) { public void lock(long id) {
Bundle args = new Bundle(); Bundle args = new Bundle();
@ -5784,6 +5847,89 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
} }
} }
private void startSearch(TextView view) {
searchView = view;
etSearch.setText(null);
etSearch.setVisibility(View.VISIBLE);
etSearch.requestFocus();
InputMethodManager imm =
(InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null)
imm.showSoftInput(etSearch, InputMethodManager.SHOW_IMPLICIT);
}
private void endSearch() {
Helper.hideKeyboard(etSearch);
etSearch.setVisibility(View.GONE);
clearSearch();
searchView = null;
}
private void performSearch(boolean next) {
clearSearch();
if (searchView == null)
return;
searchIndex = (next ? searchIndex + 1 : 1);
String query = etSearch.getText().toString().toLowerCase();
String text = searchView.getText().toString().toLowerCase();
int pos = -1;
for (int i = 0; i < searchIndex; i++)
pos = (pos < 0 ? text.indexOf(query) : text.indexOf(query, pos + 1));
// Wrap around
if (pos < 0 && searchIndex > 1) {
searchIndex = 1;
pos = text.indexOf(query);
}
// Scroll to found text
if (pos >= 0) {
int color = Helper.resolveColor(searchView.getContext(), R.attr.colorHighlight);
SpannableString ss = new SpannableString(searchView.getText());
ss.setSpan(new BackgroundColorSpan(color),
pos, pos + query.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | Spannable.SPAN_COMPOSING);
searchView.setText(ss);
int line = searchView.getLayout().getLineForOffset(pos);
int y = searchView.getLayout().getLineTop(line);
int dy = searchView.getContext().getResources()
.getDimensionPixelSize(R.dimen.search_in_text_margin);
View itemView = rvMessage.findContainingItemView(searchView);
if (itemView != null) {
Rect rect = new Rect();
searchView.getDrawingRect(rect);
RecyclerView.ViewHolder holder = rvMessage.getChildViewHolder(itemView);
((ViewGroup) itemView).offsetDescendantRectToMyCoords(searchView, rect);
iProperties.scrollTo(holder.getAdapterPosition(), rect.top + y - dy);
}
}
boolean hasNext = (pos >= 0 &&
(text.indexOf(query) != pos ||
text.indexOf(query, pos + 1) >= 0));
etSearch.setActionEnabled(hasNext);
}
private void clearSearch() {
if (searchView == null)
return;
SpannableString ss = new SpannableString(searchView.getText());
for (BackgroundColorSpan span : ss.getSpans(0, ss.length(), BackgroundColorSpan.class))
if ((ss.getSpanFlags(span) & Spannable.SPAN_COMPOSING) != 0)
ss.removeSpan(span);
searchView.setText(ss);
}
private ActivityBase.IKeyPressedListener onBackPressedListener = new ActivityBase.IKeyPressedListener() { private ActivityBase.IKeyPressedListener onBackPressedListener = new ActivityBase.IKeyPressedListener() {
@Override @Override
public boolean onKeyPressed(KeyEvent event) { public boolean onKeyPressed(KeyEvent event) {

View File

@ -135,6 +135,22 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvHintSelect" /> app:layout_constraintTop_toBottomOf="@id/tvHintSelect" />
<eu.faircode.email.TextViewAutoCompleteAction
android:id="@+id/etSearch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorSeparator"
android:completionThreshold="2"
android:hint="@string/title_search_for_hint"
android:imeOptions="actionDone"
android:inputType="text"
android:maxLines="1"
android:padding="6dp"
app:end_drawable="@drawable/twotone_fast_forward_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorHintSelect" />
<eu.faircode.email.ViewTextDelayed <eu.faircode.email.ViewTextDelayed
android:id="@+id/tvNoEmail" android:id="@+id/tvNoEmail"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -173,7 +189,7 @@
app:layout_constraintBottom_toTopOf="@+id/sbThread" app:layout_constraintBottom_toTopOf="@+id/sbThread"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorHintSelect" /> app:layout_constraintTop_toBottomOf="@id/etSearch" />
<View <View
android:id="@+id/vwAnchor" android:id="@+id/vwAnchor"

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorSeparator"
android:padding="12dp">
<eu.faircode.email.EditTextPlain
android:id="@+id/etSearch"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:inputType="text"
android:text="Search"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<requestFocus />
</eu.faircode.email.EditTextPlain>
<ImageButton
android:id="@+id/ibNext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="@id/etSearch"
app:layout_constraintStart_toEndOf="@+id/etSearch"
app:layout_constraintTop_toTopOf="@+id/etSearch"
app:srcCompat="@drawable/twotone_fast_forward_24" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -11,4 +11,5 @@
<dimen name="quote_gap_size">6dp</dimen> <dimen name="quote_gap_size">6dp</dimen>
<dimen name="quote_stripe_width">3dp</dimen> <dimen name="quote_stripe_width">3dp</dimen>
<dimen name="line_dash_length">3dp</dimen> <dimen name="line_dash_length">3dp</dimen>
<dimen name="search_in_text_margin">48dp</dimen>
</resources> </resources>