diff --git a/app/src/main/java/net/bible/android/control/search/SearchControl.kt b/app/src/main/java/net/bible/android/control/search/SearchControl.kt index 5a343da122..225f5b8b24 100644 --- a/app/src/main/java/net/bible/android/control/search/SearchControl.kt +++ b/app/src/main/java/net/bible/android/control/search/SearchControl.kt @@ -99,10 +99,32 @@ class SearchControl @Inject constructor( "-" } - fun decorateSearchString(searchString: String, searchType: SearchType, bibleSection: SearchBibleSection, currentBookName: String?): String { - val cleanSearchString = cleanSearchString(searchString) + fun decorateSearchString(searchString: String, searchType: SearchType, bibleSection: SearchBibleSection, currentBookName: String?, + includeAllEndings: Boolean=false, fuzzySearchAccuracy: Double? = null, proximityWords: Int? = null, + strongs: Char? = null): String { + var cleanSearchString = cleanSearchString(searchString) var decorated: String + if (includeAllEndings || strongs != null || fuzzySearchAccuracy != null) { + var newSearchString ="" + val wordArray: List = cleanSearchString.split(" ") + wordArray.forEach { + var decoratedWord = it + if (includeAllEndings) decoratedWord += "* " + if (strongs != null) decoratedWord = "strong:$strongs$decoratedWord " + if (fuzzySearchAccuracy != null) { + val fuzzySearchAccuracyAdjusted = if (fuzzySearchAccuracy.equals(1.0)) 0.99 else fuzzySearchAccuracy + decoratedWord += "~%.2f ".format(fuzzySearchAccuracyAdjusted) + } + newSearchString += decoratedWord + } + cleanSearchString = newSearchString.trim() + } + + if (proximityWords != null) { + cleanSearchString = "\"" + cleanSearchString + "\"~" + proximityWords + } + // add search type (all/any/phrase) to search string decorated = searchType.decorate(cleanSearchString) diff --git a/app/src/main/java/net/bible/android/view/activity/search/Search.kt b/app/src/main/java/net/bible/android/view/activity/search/Search.kt index edf074fd6b..369e0ffd44 100644 --- a/app/src/main/java/net/bible/android/view/activity/search/Search.kt +++ b/app/src/main/java/net/bible/android/view/activity/search/Search.kt @@ -21,12 +21,16 @@ package net.bible.android.view.activity.search import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher import android.util.Log import android.view.MenuItem import android.view.View import android.view.inputmethod.EditorInfo +import android.widget.LinearLayout import android.widget.RadioButton import android.widget.RadioGroup +import android.widget.SeekBar import net.bible.android.activity.R import net.bible.android.activity.databinding.SearchBinding @@ -63,6 +67,31 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { /** get all, any, phrase query limitation */ + + private var fuzzySearchAccuracySelection: Int = 5 + set(value) { + binding.fuzzySearch.text = getString(R.string.search_fuzzy, "(~" + (value * 10).toString() + "%)") + binding.fuzzySearchAccuracy.setProgress(value) + binding.decoratedSearchString.text = decorateSearchString(binding.searchText.text.toString()) + field = value + } + + // I don't know how to do this properly. I want to update the value of proximityWordNumber whenever it is changed via buttons, text or initialisation + // But part of the update is to set the text value itself. This gets into an infinite loop because the text value change fires the update again. + // So i have broken it into two parts. One updates everything and one does not update the text field. + private var proximitySearchWordsSelection: Int = 10 + set(value) { + binding.proximityWordNumber.setText(value.toString()) + proximitySearchWordsSelectionFinal = value + binding.decoratedSearchString.text = decorateSearchString(binding.searchText.text.toString()) + field = value + } + private var proximitySearchWordsSelectionFinal: Int = 10 + set(value) { + binding.proximitySearch.text = getString(R.string.search_proximity, "(${value.toString()} ${getString(R.string.words)})") + field = value + } + private val searchType: SearchType get() { return when (wordsRadioSelection) { @@ -75,7 +104,6 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { } } } - /** get OT, NT, or all query limitation * * @return @@ -104,11 +132,19 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { CommonUtils.settings.setLong("search-last-used", System.currentTimeMillis()) buildActivityComponent().inject(this) + // set text for current bible book on appropriate radio button + val currentBookRadioButton = findViewById(R.id.searchCurrentBook) as RadioButton + + // set current book to default and allow override if saved - implies returning via Back button + currentBookName = searchControl.currentBookName + if (!searchControl.validateIndex(documentToSearch)) { Dialogs.instance.showErrorMsg(R.string.error_occurred) { finish() } } title = getString(R.string.search_in, documentToSearch!!.abbreviation) + + // Add all listeners binding.searchText.setOnEditorActionListener {v, actionId, event -> return@setOnEditorActionListener when (actionId) { EditorInfo.IME_ACTION_SEARCH -> { @@ -117,17 +153,108 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { } else -> false }} + binding.searchText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) {} + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + binding.decoratedSearchString.text = decorateSearchString(binding.searchText.text.toString()) + } + }) + val sectionRadioGroup = findViewById(R.id.bibleSectionGroup) as RadioGroup + sectionRadioGroup.setOnCheckedChangeListener { group, checkedId -> + sectionRadioSelection = checkedId + CommonUtils.settings.setInt("search_bible_section_group_prompt", checkedId) + enableSearchControls() + } + val wordsRadioGroup = findViewById(R.id.wordsGroup) as RadioGroup + wordsRadioGroup.setOnCheckedChangeListener { group, checkedId -> + wordsRadioSelection = checkedId + CommonUtils.settings.setInt("search_words_group_prompt", checkedId) + enableSearchControls() + } + binding.includeAllEndings.setOnClickListener { + CommonUtils.settings.setBoolean("search_include_all_endings", binding.includeAllEndings.isChecked) + if (binding.includeAllEndings.isChecked) { + binding.proximitySearch.isChecked = false + binding.strongsSearch.isChecked = false + binding.fuzzySearch.isChecked = false + } + enableSearchControls() + } + binding.rememberSearchText.setOnClickListener { + CommonUtils.settings.setBoolean("search_remember_search_text", binding.rememberSearchText.isChecked) + } + binding.fuzzySearch.setOnClickListener { + if (binding.fuzzySearch.isChecked) { + binding.proximitySearch.isChecked = false + binding.strongsSearch.isChecked = false + binding.includeAllEndings.isChecked = false + } + enableSearchControls() + } + binding.fuzzySearchAccuracy.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + fuzzySearchAccuracySelection = progress + } + override fun onStartTrackingTouch(seekBar: SeekBar?) {} + override fun onStopTrackingTouch(seekBar: SeekBar?) {} + }) + + binding.proximitySearch.setOnClickListener { + if (binding.proximitySearch.isChecked) { + binding.fuzzySearch.isChecked = false + binding.strongsSearch.isChecked = false + binding.includeAllEndings.isChecked = false + } + enableSearchControls() + } + binding.proximityButtonAdd.setOnClickListener {proximitySearchWordsSelection = adjustProximityWordNumber(1)} + binding.proximityButtonSubtract.setOnClickListener {proximitySearchWordsSelection = adjustProximityWordNumber(-1)} + binding.proximityWordNumber.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) {} + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {proximitySearchWordsSelectionFinal = if(s.isNullOrEmpty()) 1 else s.toString().toInt()} + }) + + binding.strongsSearch.setOnClickListener { + if (binding.strongsSearch.isChecked) { + binding.includeAllEndings.isChecked = false + binding.fuzzySearch.isChecked = false + binding.proximitySearch.isChecked = false + } + enableSearchControls() + } + binding.greekOrHebrewGroup.setOnCheckedChangeListener { group, checkedId -> + CommonUtils.settings.setInt("search_greek_or_hebrew", checkedId) + enableSearchControls() + } + binding.decoratedTextShow.setOnCheckedChangeListener { group, checkedId -> + CommonUtils.settings.setBoolean("search_show_decorated_text", checkedId) + enableSearchControls() + } binding.submit.setOnClickListener { onSearch() } - //searchText.setOnKeyListener(OnKeyListener { v, keyCode, event -> - // // If the event is a key-down event on the "enter" button - // if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { - // // Perform action on key press - // onSearch(null) - // return@OnKeyListener true - // } - // false - //}) + + // Initialise controls + binding.includeAllEndings.isChecked = CommonUtils.settings.getBoolean("search_include_all_endings", false) + binding.rememberSearchText.isChecked = CommonUtils.settings.getBoolean("search_remember_search_text", false) + + binding.proximitySearch.isChecked = CommonUtils.settings.getBoolean("search_proximity",false) + proximitySearchWordsSelection = CommonUtils.settings.getInt("search_proximity_words", 10) + + binding.wordsGroup.check(CommonUtils.settings.getInt("search_words_group_prompt", 0)) + wordsRadioSelection = binding.wordsGroup.checkedRadioButtonId + + binding.bibleSectionGroup.check(CommonUtils.settings.getInt("search_bible_section_group_prompt", 0)) + sectionRadioSelection = binding.bibleSectionGroup.checkedRadioButtonId + + binding.fuzzySearch.isChecked = CommonUtils.settings.getBoolean("search_fuzzy", false) + fuzzySearchAccuracySelection = CommonUtils.settings.getInt("search_fuzzy_accuracy", 5) + + binding.strongsSearch.isChecked = CommonUtils.settings.getBoolean("search_strongs",false) + binding.greekOrHebrewGroup.check(CommonUtils.settings.getInt("search_greek_or_hebrew",0)) + + binding.decoratedTextShow.isChecked = CommonUtils.settings.getBoolean("search_show_decorated_text",false) // pre-load search string if passed in val extras = intent.extras @@ -136,10 +263,10 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { if (StringUtils.isNotEmpty(text)) { binding.searchText.setText(text) } + } else { + if (binding.rememberSearchText.isChecked) binding.searchText.setText(CommonUtils.settings.getString("search_text", "")) } - val wordsRadioGroup = findViewById(R.id.wordsGroup) as RadioGroup - wordsRadioGroup.setOnCheckedChangeListener { group, checkedId -> wordsRadioSelection = checkedId } if (extras != null) { val wordsSelection = extras.getInt(WORDS_SELECTION_SAVE, -1) if (wordsSelection != -1) { @@ -147,8 +274,6 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { } } - val sectionRadioGroup = findViewById(R.id.bibleSectionGroup) as RadioGroup - sectionRadioGroup.setOnCheckedChangeListener { group, checkedId -> sectionRadioSelection = checkedId } if (extras != null) { val sectionSelection = extras.getInt(SECTION_SELECTION_SAVE, -1) if (sectionSelection != -1) { @@ -156,11 +281,6 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { } } - // set text for current bible book on appropriate radio button - val currentBookRadioButton = findViewById(R.id.searchCurrentBook) as RadioButton - - // set current book to default and allow override if saved - implies returning via Back button - currentBookName = searchControl.currentBookName if (extras != null) { val currentBibleBookSaved = extras.getString(CURRENT_BIBLE_BOOK_SAVE) if (currentBibleBookSaved != null) { @@ -169,9 +289,46 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { } currentBookRadioButton.text = currentBookName + binding.textClear.setOnClickListener({binding.searchText.setText("")}) + enableSearchControls() + Log.i(TAG, "Finished displaying Search view") } + private fun adjustProximityWordNumber(changeAmt: Int):Int { + var newProximityWordNumber = binding.proximityWordNumber.text.toString().toInt() + changeAmt + if (newProximityWordNumber < 1) newProximityWordNumber = 1 + return newProximityWordNumber + } + + fun enableSearchControls() { + + // Set control visibility + binding.fuzzySearchDetailsLayout.visibility = if (binding.fuzzySearch.isChecked) View.VISIBLE else View.GONE + binding.proximityDetailsLayout.visibility = if (binding.proximitySearch.isChecked) View.VISIBLE else View.GONE + binding.strongsDetailLayout.visibility = if (binding.strongsSearch.isChecked) View.VISIBLE else View.GONE + binding.decoratedSearchDetailLayout.visibility = if (binding.decoratedTextShow.isChecked) View.VISIBLE else View.GONE + + enableLayout(binding.fuzzySearchLayout, binding.allWords.id == wordsRadioSelection) + enableLayout(binding.fuzzySearchDetailsLayout, binding.fuzzySearch.isChecked && binding.fuzzySearch.isEnabled) + + enableLayout(binding.strongsDetailLayout, (binding.strongsSearch.isChecked && binding.strongsSearch.isEnabled)) + + binding.searchOldTestament.isEnabled = !binding.strongsSearch.isChecked || (binding.strongsSearch.isEnabled && binding.strongsSearch.isChecked && binding.searchHebrew.isChecked) + binding.searchNewTestament.isEnabled = !binding.strongsSearch.isChecked || (binding.strongsSearch.isEnabled && binding.strongsSearch.isChecked && binding.searchGreek.isChecked) + + binding.decoratedSearchString.text = decorateSearchString(binding.searchText.text.toString()) + } + + fun enableLayout(layout: LinearLayout, isEnabled: Boolean) { + for (i in 0 until layout.childCount) { + val view: View = layout.getChildAt(i) + if (view is LinearLayout) {enableLayout(view,isEnabled)} + view.isEnabled = isEnabled + } + layout.isEnabled = isEnabled + + } override fun onOptionsItemSelected(item: MenuItem): Boolean { when(item.itemId) { R.id.rebuildIndex -> { @@ -191,7 +348,6 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { binding.searchText.requestFocus() } - fun onRebuildIndex(v: View?) { startActivity(Intent(this, SearchIndex::class.java)) finish() @@ -204,6 +360,13 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { var text = binding.searchText.text.toString() if (!StringUtils.isEmpty(text)) { + CommonUtils.settings.setString("search_text", text) + CommonUtils.settings.setBoolean("search_fuzzy", binding.fuzzySearch.isChecked) + CommonUtils.settings.setInt("search_fuzzy_accuracy", fuzzySearchAccuracySelection) + CommonUtils.settings.setBoolean("search_proximity", binding.proximitySearch.isChecked) + CommonUtils.settings.setInt("search_proximity_words", proximitySearchWordsSelectionFinal) + CommonUtils.settings.setBoolean("search_strongs", binding.strongsSearch.isChecked) + // update current intent so search is restored if we return here via history/back // the current intent is saved by HistoryManager intent.putExtra(SEARCH_TEXT_SAVE, text) @@ -215,7 +378,7 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { Log.i(TAG, "Search text:$text") // specify search string and doc in new Intent; - // if doc is not specifed a, possibly invalid, doc may be used when returning to search via history list e.g. search bible, select dict, history list, search results + // if doc is not specifed a possibly invalid doc may be used when returning to search via history list e.g. search bible, select dict, history list, search results val intent = Intent(this, SearchResults::class.java) intent.putExtra(SearchControl.SEARCH_TEXT, text) val currentDocInitials = documentToSearch?.initials @@ -229,7 +392,11 @@ class Search : CustomTitlebarActivityBase(R.menu.search_actionbar_menu) { } private fun decorateSearchString(searchString: String): String { - return searchControl.decorateSearchString(searchString, searchType, bibleSection, currentBookName) + val fuzzyAccuracy = if (binding.fuzzySearch.isChecked && binding.fuzzySearch.isEnabled) fuzzySearchAccuracySelection.toDouble()/10 else null + val proximityWords = if (binding.proximitySearch.isChecked && binding.proximitySearch.isEnabled) proximitySearchWordsSelectionFinal else null + val strongs = if (binding.strongsSearch.isChecked && binding.strongsSearch.isEnabled) {if (binding.searchGreek.isChecked) 'G' else 'H'} else null + + return searchControl.decorateSearchString(searchString, searchType, bibleSection, currentBookName, binding.includeAllEndings.isChecked, fuzzyAccuracy, proximityWords, strongs) } companion object { diff --git a/app/src/main/java/net/bible/service/history/KeyHistoryItem.kt b/app/src/main/java/net/bible/service/history/KeyHistoryItem.kt index 79a1f7ff1f..8f9527c15e 100644 --- a/app/src/main/java/net/bible/service/history/KeyHistoryItem.kt +++ b/app/src/main/java/net/bible/service/history/KeyHistoryItem.kt @@ -45,7 +45,7 @@ class KeyHistoryItem( val desc = StringBuilder() try { val verseDesc = CommonUtils.getKeyDescription(key) - desc.append(verseDesc).append(" ").append(document.abbreviation) + desc.append(verseDesc).append(" ").append(document.abbreviation) } catch (e: Exception) { Log.e(TAG, "Error getting description", e) } diff --git a/app/src/main/res/layout/search.xml b/app/src/main/res/layout/search.xml index 48963f68ae..213c2a2e75 100644 --- a/app/src/main/res/layout/search.xml +++ b/app/src/main/res/layout/search.xml @@ -5,6 +5,7 @@ --> - - - - - + + - - + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +