#550 In-note-search doesn't jump to occurrence of searchstring

Divide et impera
This commit is contained in:
stefan-niedermann 2020-01-23 11:16:40 +01:00
parent 2b9cfc2704
commit 610eab570d
4 changed files with 230 additions and 206 deletions

View file

@ -10,24 +10,16 @@ import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.text.Layout;
import android.text.SpannableString;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.ShareActionProvider;
import androidx.core.view.MenuItemCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -45,7 +37,6 @@ import it.niedermann.owncloud.notes.model.CloudNote;
import it.niedermann.owncloud.notes.model.DBNote;
import it.niedermann.owncloud.notes.model.LocalAccount;
import it.niedermann.owncloud.notes.persistence.NoteSQLiteOpenHelper;
import it.niedermann.owncloud.notes.util.DisplayUtils;
import it.niedermann.owncloud.notes.util.ICallback;
import static androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported;
@ -62,11 +53,6 @@ public abstract class BaseNoteFragment extends Fragment implements CategoryDialo
private static final String SAVEDKEY_NOTE = "note";
private static final String SAVEDKEY_ORIGINAL_NOTE = "original_note";
protected SearchView searchView;
protected MenuItem searchMenuItem;
protected String searchQuery = null;
private LocalAccount localAccount;
protected DBNote note;
@ -75,21 +61,7 @@ public abstract class BaseNoteFragment extends Fragment implements CategoryDialo
protected NoteSQLiteOpenHelper db;
private NoteFragmentListener listener;
private TextView activeTextView;
private boolean isNew = true;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
searchQuery = savedInstanceState.getString("searchQuery", "");
}
}
void setActiveTextView(TextView textView) {
activeTextView = textView;
}
boolean isNew = true;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -167,18 +139,6 @@ public abstract class BaseNoteFragment extends Fragment implements CategoryDialo
saveNote(null);
outState.putSerializable(SAVEDKEY_NOTE, note);
outState.putSerializable(SAVEDKEY_ORIGINAL_NOTE, originalNote);
if (searchView != null && !TextUtils.isEmpty(searchView.getQuery().toString())) {
outState.putString("searchQuery", searchView.getQuery().toString());
}
}
private void colorWithText(String newText) {
if (activeTextView != null && ViewCompat.isAttachedToWindow(activeTextView)) {
activeTextView.setText(DisplayUtils.searchAndColor(activeTextView.getText().toString(), new SpannableString
(activeTextView.getText()), newText, getResources().getColor(R.color.primary)),
TextView.BufferType.SPANNABLE);
}
}
@Override
@ -190,9 +150,6 @@ public abstract class BaseNoteFragment extends Fragment implements CategoryDialo
}
}
private int currentOccurrence = 1;
private int occurrenceCount = 0;
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
super.onPrepareOptionsMenu(menu);
@ -200,166 +157,6 @@ public abstract class BaseNoteFragment extends Fragment implements CategoryDialo
prepareFavoriteOption(itemFavorite);
menu.findItem(R.id.menu_delete).setVisible(!isNew);
searchMenuItem = menu.findItem(R.id.search);
searchView = (SearchView) searchMenuItem.getActionView();
if (!TextUtils.isEmpty(searchQuery) && isNew) {
searchMenuItem.expandActionView();
searchView.setQuery(searchQuery, true);
searchView.clearFocus();
} else {
searchMenuItem.collapseActionView();
}
final LinearLayout searchEditFrame = searchView.findViewById(R.id
.search_edit_frame);
searchEditFrame.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
int oldVisibility = -1;
@Override
public void onGlobalLayout() {
int currentVisibility = searchEditFrame.getVisibility();
if (currentVisibility != oldVisibility) {
if (currentVisibility != View.VISIBLE) {
colorWithText("");
searchQuery = "";
hideSearchFabs();
} else {
showSearchFabs();
}
oldVisibility = currentVisibility;
}
}
});
FloatingActionButton next = getSearchNextButton();
FloatingActionButton prev = getSearchPrevButton();
if (next != null) {
next.setOnClickListener(v -> {
currentOccurrence++;
jumpToOccurrence();
});
}
if (prev != null) {
prev.setOnClickListener(v -> {
currentOccurrence--;
jumpToOccurrence();
});
}
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
currentOccurrence++;
jumpToOccurrence();
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
searchQuery = newText;
colorWithText(newText);
occurrenceCount = countOccurrences(getContent(), searchQuery);
if(occurrenceCount > 1) {
showSearchFabs();
} else {
hideSearchFabs();
}
currentOccurrence = 1;
jumpToOccurrence();
return true;
}
});
}
private void hideSearchFabs() {
FloatingActionButton next = getSearchNextButton();
FloatingActionButton prev = getSearchPrevButton();
if (prev != null) {
prev.hide();
}
if (next != null) {
next.hide();
}
}
private void showSearchFabs() {
FloatingActionButton next = getSearchNextButton();
FloatingActionButton prev = getSearchPrevButton();
if (prev != null) {
prev.show();
}
if (next != null) {
next.show();
}
}
private void jumpToOccurrence() {
if (searchQuery == null || searchQuery.isEmpty()) {
// No search term
return;
}
if (currentOccurrence < 1) {
// if currentOccurrence is lower than 1, jump to last occurrence
currentOccurrence = occurrenceCount;
jumpToOccurrence();
return;
}
String currentContent = getContent().toLowerCase();
int indexOfNewText = indexOfNth(currentContent, searchQuery.toLowerCase(), 0, currentOccurrence);
if (indexOfNewText <= 0) {
// Search term is not n times in text
// Go back to first search result
if (currentOccurrence != 1) {
currentOccurrence = 1;
jumpToOccurrence();
}
return;
}
String textUntilFirstOccurrence = currentContent.substring(0, indexOfNewText);
int numberLine = getLayout().getLineForOffset(textUntilFirstOccurrence.length());
if (numberLine >= 0) {
getScrollView().smoothScrollTo(0, getLayout().getLineTop(numberLine));
}
}
private static int indexOfNth(String input, String value, int startIndex, int nth) {
if (nth < 1)
throw new IllegalArgumentException("Param 'nth' must be greater than 0!");
if (nth == 1)
return input.indexOf(value, startIndex);
int idx = input.indexOf(value, startIndex);
if (idx == -1)
return -1;
return indexOfNth(input, value, idx + 1, --nth);
}
private static int countOccurrences(String haystack, String needle) {
if(haystack == null || haystack.isEmpty() || needle == null || needle.isEmpty()) {
return 0;
}
haystack = haystack.toLowerCase();
needle = needle.toLowerCase();
int lastIndex = 0;
int count = 0;
while (lastIndex != -1) {
lastIndex = haystack.indexOf(needle, lastIndex);
if (lastIndex != -1) {
count++;
lastIndex += needle.length();
}
}
return count;
}
protected abstract ScrollView getScrollView();

View file

@ -39,7 +39,7 @@ import it.niedermann.owncloud.notes.util.MarkDownUtil;
import it.niedermann.owncloud.notes.util.NotesTextWatcher;
import it.niedermann.owncloud.notes.util.StyleCallback;
public class NoteEditFragment extends BaseNoteFragment {
public class NoteEditFragment extends SearchableBaseNoteFragment {
private static final String LOG_TAG_AUTOSAVE = "AutoSave";

View file

@ -38,7 +38,7 @@ import it.niedermann.owncloud.notes.util.ICallback;
import it.niedermann.owncloud.notes.util.MarkDownUtil;
import it.niedermann.owncloud.notes.util.NoteLinksUtils;
public class NotePreviewFragment extends BaseNoteFragment {
public class NotePreviewFragment extends SearchableBaseNoteFragment {
private String changedText;

View file

@ -0,0 +1,227 @@
package it.niedermann.owncloud.notes.android.fragment;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.ViewCompat;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.util.DisplayUtils;
public abstract class SearchableBaseNoteFragment extends BaseNoteFragment {
private TextView activeTextView;
private int currentOccurrence = 1;
private int occurrenceCount = 0;
private SearchView searchView;
private String searchQuery = null;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
searchQuery = savedInstanceState.getString("searchQuery", "");
}
}
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
super.onPrepareOptionsMenu(menu);
MenuItem searchMenuItem = menu.findItem(R.id.search);
searchView = (SearchView) searchMenuItem.getActionView();
if (!TextUtils.isEmpty(searchQuery) && isNew) {
searchMenuItem.expandActionView();
searchView.setQuery(searchQuery, true);
searchView.clearFocus();
} else {
searchMenuItem.collapseActionView();
}
final LinearLayout searchEditFrame = searchView.findViewById(R.id
.search_edit_frame);
searchEditFrame.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
int oldVisibility = -1;
@Override
public void onGlobalLayout() {
int currentVisibility = searchEditFrame.getVisibility();
if (currentVisibility != oldVisibility) {
if (currentVisibility != View.VISIBLE) {
colorWithText("");
searchQuery = "";
hideSearchFabs();
} else {
showSearchFabs();
}
oldVisibility = currentVisibility;
}
}
});
FloatingActionButton next = getSearchNextButton();
FloatingActionButton prev = getSearchPrevButton();
if (next != null) {
next.setOnClickListener(v -> {
currentOccurrence++;
jumpToOccurrence();
});
}
if (prev != null) {
prev.setOnClickListener(v -> {
currentOccurrence--;
jumpToOccurrence();
});
}
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
currentOccurrence++;
jumpToOccurrence();
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
searchQuery = newText;
colorWithText(newText);
occurrenceCount = countOccurrences(getContent(), searchQuery);
if (occurrenceCount > 1) {
showSearchFabs();
} else {
hideSearchFabs();
}
currentOccurrence = 1;
jumpToOccurrence();
return true;
}
});
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (searchView != null && !TextUtils.isEmpty(searchView.getQuery().toString())) {
outState.putString("searchQuery", searchView.getQuery().toString());
}
}
void setActiveTextView(TextView textView) {
activeTextView = textView;
}
private void colorWithText(String newText) {
if (activeTextView != null && ViewCompat.isAttachedToWindow(activeTextView)) {
activeTextView.setText(DisplayUtils.searchAndColor(activeTextView.getText().toString(), new SpannableString
(activeTextView.getText()), newText, getResources().getColor(R.color.primary)),
TextView.BufferType.SPANNABLE);
}
}
private void showSearchFabs() {
FloatingActionButton next = getSearchNextButton();
FloatingActionButton prev = getSearchPrevButton();
if (prev != null) {
prev.show();
}
if (next != null) {
next.show();
}
}
private void hideSearchFabs() {
FloatingActionButton next = getSearchNextButton();
FloatingActionButton prev = getSearchPrevButton();
if (prev != null) {
prev.hide();
}
if (next != null) {
next.hide();
}
}
private void jumpToOccurrence() {
if (searchQuery == null || searchQuery.isEmpty()) {
// No search term
return;
}
if (currentOccurrence < 1) {
// if currentOccurrence is lower than 1, jump to last occurrence
currentOccurrence = occurrenceCount;
jumpToOccurrence();
return;
}
String currentContent = getContent().toLowerCase();
int indexOfNewText = indexOfNth(currentContent, searchQuery.toLowerCase(), 0, currentOccurrence);
if (indexOfNewText <= 0) {
// Search term is not n times in text
// Go back to first search result
if (currentOccurrence != 1) {
currentOccurrence = 1;
jumpToOccurrence();
}
return;
}
String textUntilFirstOccurrence = currentContent.substring(0, indexOfNewText);
int numberLine = getLayout().getLineForOffset(textUntilFirstOccurrence.length());
if (numberLine >= 0) {
getScrollView().smoothScrollTo(0, getLayout().getLineTop(numberLine));
}
}
private static int indexOfNth(String input, String value, int startIndex, int nth) {
if (nth < 1)
throw new IllegalArgumentException("Param 'nth' must be greater than 0!");
if (nth == 1)
return input.indexOf(value, startIndex);
int idx = input.indexOf(value, startIndex);
if (idx == -1)
return -1;
return indexOfNth(input, value, idx + 1, --nth);
}
private static int countOccurrences(String haystack, String needle) {
if (haystack == null || haystack.isEmpty() || needle == null || needle.isEmpty()) {
return 0;
}
haystack = haystack.toLowerCase();
needle = needle.toLowerCase();
int lastIndex = 0;
int count = 0;
while (lastIndex != -1) {
lastIndex = haystack.indexOf(needle, lastIndex);
if (lastIndex != -1) {
count++;
lastIndex += needle.length();
}
}
return count;
}
}