Merge pull request #338 from SimpleMobileTools/search

Search
This commit is contained in:
Tibor Kaputa 2020-03-25 20:34:09 +01:00 committed by GitHub
commit 8bdac0e761
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 331 additions and 37 deletions

View File

@ -57,10 +57,10 @@ android {
} }
dependencies { dependencies {
implementation 'com.simplemobiletools:commons:5.22.19' implementation 'com.simplemobiletools:commons:5.23.7'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
kapt 'androidx.room:room-compiler:2.2.2' kapt 'androidx.room:room-compiler:2.2.4'
implementation 'androidx.room:room-runtime:2.2.2' implementation 'androidx.room:room-runtime:2.2.4'
annotationProcessor 'androidx.room:room-compiler:2.2.2' annotationProcessor 'androidx.room:room-compiler:2.2.4'
} }

View File

@ -3,13 +3,20 @@ package com.simplemobiletools.notes.pro.activities
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.Spannable
import android.text.SpannableString
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.util.TypedValue import android.util.TypedValue
import android.view.ActionMode import android.view.ActionMode
import android.view.Gravity import android.view.Gravity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.core.graphics.ColorUtils
import com.simplemobiletools.commons.dialogs.ConfirmationAdvancedDialog import com.simplemobiletools.commons.dialogs.ConfirmationAdvancedDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.dialogs.RadioGroupDialog
@ -26,9 +33,14 @@ import com.simplemobiletools.notes.pro.adapters.NotesPagerAdapter
import com.simplemobiletools.notes.pro.databases.NotesDatabase import com.simplemobiletools.notes.pro.databases.NotesDatabase
import com.simplemobiletools.notes.pro.dialogs.* import com.simplemobiletools.notes.pro.dialogs.*
import com.simplemobiletools.notes.pro.extensions.* import com.simplemobiletools.notes.pro.extensions.*
import com.simplemobiletools.notes.pro.helpers.* import com.simplemobiletools.notes.pro.fragments.TextFragment
import com.simplemobiletools.notes.pro.helpers.MIME_TEXT_PLAIN
import com.simplemobiletools.notes.pro.helpers.NoteType
import com.simplemobiletools.notes.pro.helpers.NotesHelper
import com.simplemobiletools.notes.pro.helpers.OPEN_NOTE_ID
import com.simplemobiletools.notes.pro.models.Note import com.simplemobiletools.notes.pro.models.Note
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.search_item.*
import java.io.File import java.io.File
import java.nio.charset.Charset import java.nio.charset.Charset
@ -47,6 +59,9 @@ class MainActivity : SimpleActivity() {
private var showSaveButton = false private var showSaveButton = false
private var showUndoButton = false private var showUndoButton = false
private var showRedoButton = false private var showRedoButton = false
private var searchIndex = 0
private var searchMatches = emptyList<Int>()
private var searchIsActive = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -65,7 +80,164 @@ class MainActivity : SimpleActivity() {
} }
wasInit = true wasInit = true
checkAppOnSDCard() checkAppOnSDCard()
searchListeners()
}
private fun searchListeners() {
search_query.onTextChangeListener { query ->
currentNotesView()?.let { noteView ->
currentTextFragment?.removeTextWatcher()
searchClearSpans(noteView.text)
if (query.isNotBlank() && query.length > 1) {
searchMatches = searchMatches(query, noteView.value)
searchHighLightText(noteView, query)
}
currentTextFragment?.setTextWatcher()
if (searchMatches.isNotEmpty()) {
noteView.requestFocus()
noteView.setSelection(searchMatches.getOrNull(searchIndex) ?: 0)
}
search_query.postDelayed({
search_query.requestFocus()
}, 50)
}
}
search_previous.setOnClickListener {
currentNotesView()?.let { noteView ->
if (searchIndex > 0) {
searchIndex--
}
else {
searchIndex = searchMatches.lastIndex
}
selectMatch(noteView)
}
}
search_next.setOnClickListener {
currentNotesView()?.let { noteView ->
if (searchIndex < searchMatches.lastIndex) {
searchIndex++
} else {
searchIndex = 0
}
selectMatch(noteView)
}
}
search_clear.setOnClickListener {
searchHide()
}
view_pager.onPageChangeListener {
currentTextFragment?.removeTextWatcher()
currentNotesView()?.let { noteView ->
searchClearSpans(noteView.text)
}
searchHide()
currentTextFragment?.setTextWatcher()
}
search_query.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
search_next.performClick()
return@OnEditorActionListener true
}
false
})
}
private val currentTextFragment: TextFragment? get() = mAdapter?.textFragment(view_pager.currentItem)
private fun searchClearSpans(editable: Editable) {
val spans = editable.getSpans(0, editable.length, Any::class.java)
for (span in spans) {
if (span is BackgroundColorSpan) {
editable.removeSpan(span)
}
}
}
private fun selectMatch(noteView: MyEditText) {
if (searchMatches.isNotEmpty()) {
noteView.requestFocus()
noteView.setSelection(searchMatches.getOrNull(searchIndex) ?: 0)
} else
hideKeyboard()
}
private fun searchMatches(textToHighlight: String, content: String): ArrayList<Int> {
val indexes = arrayListOf<Int>()
var indexOf = content.indexOf(textToHighlight, 0, ignoreCase = true)
var offset = 0
while (offset < content.length && indexOf != -1) {
indexOf = content.indexOf(textToHighlight, offset, ignoreCase = true)
if (indexOf == -1) {
break
} else {
indexes.add(indexOf)
}
offset = indexOf + 1
}
return indexes
}
private fun searchHighLightText(view: MyEditText, highlightText: String) {
val content = view.text.toString()
var indexOf = content.indexOf(highlightText, 0, true)
val wordToSpan = SpannableString(view.text)
var offset = 0
while (offset < content.length && indexOf != -1) {
indexOf = content.indexOf(highlightText, offset, true)
if (indexOf == -1) {
break
} else {
val spanBgColor = BackgroundColorSpan(ColorUtils.setAlphaComponent(config.primaryColor, 128))
val spanFlag = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
wordToSpan.setSpan(spanBgColor, indexOf, indexOf + highlightText.length, spanFlag)
view.setText(wordToSpan, TextView.BufferType.SPANNABLE)
}
offset = indexOf + 1
}
}
private fun searchShow() {
searchIsActive = true
search_root.beVisible()
showKeyboard(search_query)
currentNotesView()?.let { noteView ->
noteView.requestFocus()
noteView.setSelection(0)
}
search_query.postDelayed({
search_query.requestFocus()
}, 250)
}
private fun searchHide() {
search_query.text?.clear()
searchIsActive = false
search_root.beGone()
} }
override fun onResume() { override fun onResume() {
@ -82,6 +254,12 @@ class MainActivity : SimpleActivity() {
setTextColor(config.textColor) setTextColor(config.textColor)
} }
updateTextColors(view_pager) updateTextColors(view_pager)
val contrastColor = config.primaryColor.getContrastColor()
search_root.setBackgroundColor(config.primaryColor)
search_previous.applyColorFilter(contrastColor)
search_next.applyColorFilter(contrastColor)
search_clear.applyColorFilter(contrastColor)
} }
override fun onPause() { override fun onPause() {
@ -114,6 +292,7 @@ class MainActivity : SimpleActivity() {
findItem(R.id.open_note).isVisible = shouldBeVisible findItem(R.id.open_note).isVisible = shouldBeVisible
findItem(R.id.delete_note).isVisible = shouldBeVisible findItem(R.id.delete_note).isVisible = shouldBeVisible
findItem(R.id.export_all_notes).isVisible = shouldBeVisible findItem(R.id.export_all_notes).isVisible = shouldBeVisible
findItem(R.id.open_search).isVisible = currentItemIsCheckList.not()
saveNoteButton = findItem(R.id.save_note) saveNoteButton = findItem(R.id.save_note)
saveNoteButton!!.isVisible = !config.autosaveNotes && showSaveButton && mCurrentNote.type == NoteType.TYPE_TEXT.value saveNoteButton!!.isVisible = !config.autosaveNotes && showSaveButton && mCurrentNote.type == NoteType.TYPE_TEXT.value
@ -129,6 +308,7 @@ class MainActivity : SimpleActivity() {
} }
when (item.itemId) { when (item.itemId) {
R.id.open_search -> searchShow()
R.id.open_note -> displayOpenNoteDialog() R.id.open_note -> displayOpenNoteDialog()
R.id.save_note -> saveNote() R.id.save_note -> saveNote()
R.id.undo -> undo() R.id.undo -> undo()
@ -176,6 +356,8 @@ class MainActivity : SimpleActivity() {
} }
super.onBackPressed() super.onBackPressed()
} }
} else if (searchIsActive) {
searchHide()
} else { } else {
super.onBackPressed() super.onBackPressed()
} }
@ -188,6 +370,8 @@ class MainActivity : SimpleActivity() {
checkIntents(intent) checkIntents(intent)
} }
private val currentItemIsCheckList get() = mAdapter?.isChecklistFragment(view_pager.currentItem) ?: false
private fun checkIntents(intent: Intent) { private fun checkIntents(intent: Intent) {
intent.apply { intent.apply {
if (action == Intent.ACTION_SEND && type == MIME_TEXT_PLAIN) { if (action == Intent.ACTION_SEND && type == MIME_TEXT_PLAIN) {
@ -627,7 +811,8 @@ class MainActivity : SimpleActivity() {
private fun saveCurrentNote(force: Boolean) { private fun saveCurrentNote(force: Boolean) {
getPagerAdapter().saveCurrentNote(view_pager.currentItem, force) getPagerAdapter().saveCurrentNote(view_pager.currentItem, force)
if (mCurrentNote.type == NoteType.TYPE_CHECKLIST.value) { if (mCurrentNote.type == NoteType.TYPE_CHECKLIST.value) {
mCurrentNote.value = getPagerAdapter().getNoteChecklistItems(view_pager.currentItem) ?: "" mCurrentNote.value = getPagerAdapter().getNoteChecklistItems(view_pager.currentItem)
?: ""
} }
} }
@ -730,26 +915,28 @@ class MainActivity : SimpleActivity() {
} }
fun currentNoteTextChanged(newText: String, showUndo: Boolean, showRedo: Boolean) { fun currentNoteTextChanged(newText: String, showUndo: Boolean, showRedo: Boolean) {
var shouldRecreateMenu = false if (searchIsActive.not()) {
if (showUndo != showUndoButton) { var shouldRecreateMenu = false
showUndoButton = showUndo if (showUndo != showUndoButton) {
shouldRecreateMenu = true showUndoButton = showUndo
}
if (showRedo != showRedoButton) {
showRedoButton = showRedo
shouldRecreateMenu = true
}
if (!config.autosaveNotes) {
showSaveButton = newText != mCurrentNote.value
if (showSaveButton != saveNoteButton?.isVisible) {
shouldRecreateMenu = true shouldRecreateMenu = true
} }
}
if (shouldRecreateMenu) { if (showRedo != showRedoButton) {
invalidateOptionsMenu() showRedoButton = showRedo
shouldRecreateMenu = true
}
if (!config.autosaveNotes) {
showSaveButton = newText != mCurrentNote.value
if (showSaveButton != saveNoteButton?.isVisible) {
shouldRecreateMenu = true
}
}
if (shouldRecreateMenu) {
invalidateOptionsMenu()
}
} }
} }

View File

@ -45,6 +45,10 @@ class NotesPagerAdapter(fm: FragmentManager, val notes: List<Note>, val activity
} }
} }
fun isChecklistFragment(position: Int): Boolean = (fragments[position] is ChecklistFragment)
fun textFragment(position: Int): TextFragment? = (fragments[position] as? TextFragment)
fun getCurrentNotesView(position: Int) = (fragments[position] as? TextFragment)?.getNotesView() fun getCurrentNotesView(position: Int) = (fragments[position] as? TextFragment)?.getNotesView()
fun getCurrentNoteViewText(position: Int) = (fragments[position] as? TextFragment)?.getCurrentNoteViewText() fun getCurrentNoteViewText(position: Int) = (fragments[position] as? TextFragment)?.getCurrentNoteViewText()

View File

@ -4,6 +4,7 @@ import android.appwidget.AppWidgetManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.ContextCompat
import com.simplemobiletools.notes.pro.databases.NotesDatabase import com.simplemobiletools.notes.pro.databases.NotesDatabase
import com.simplemobiletools.notes.pro.helpers.Config import com.simplemobiletools.notes.pro.helpers.Config
import com.simplemobiletools.notes.pro.helpers.MyWidgetProvider import com.simplemobiletools.notes.pro.helpers.MyWidgetProvider
@ -26,3 +27,5 @@ fun Context.updateWidgets() {
} }
} }
} }
fun Context.color(id: Int) = ContextCompat.getColor(this, id)

View File

@ -7,3 +7,4 @@ import com.simplemobiletools.notes.pro.helpers.Config
val Fragment.config: Config? get() = if (context != null) Config.newInstance(context!!) else null val Fragment.config: Config? get() = if (context != null) Config.newInstance(context!!) else null
val Fragment.requiredActivity: FragmentActivity get() = this.activity!! val Fragment.requiredActivity: FragmentActivity get() = this.activity!!

View File

@ -1,7 +1,12 @@
package com.simplemobiletools.notes.pro.extensions package com.simplemobiletools.notes.pro.extensions
import android.os.Build
import android.text.Html
import androidx.annotation.RequiresApi
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.helpers.isMarshmallowPlus
import com.simplemobiletools.commons.helpers.isNougatPlus
import com.simplemobiletools.notes.pro.models.ChecklistItem import com.simplemobiletools.notes.pro.models.ChecklistItem
fun String.parseChecklistItems(): ArrayList<ChecklistItem>? { fun String.parseChecklistItems(): ArrayList<ChecklistItem>? {
@ -14,3 +19,12 @@ fun String.parseChecklistItems(): ArrayList<ChecklistItem>? {
} }
return null return null
} }
@RequiresApi(Build.VERSION_CODES.N)
fun String.toHtml() =
if (isNougatPlus()) {
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
} else {
Html.fromHtml(this)
}

View File

@ -77,7 +77,8 @@ class TextFragment : NoteFragment() {
if (config!!.autosaveNotes) { if (config!!.autosaveNotes) {
saveText(false) saveText(false)
} }
view.text_note_view.removeTextChangedListener(textWatcher)
removeTextWatcher()
} }
override fun setMenuVisibility(menuVisible: Boolean) { override fun setMenuVisibility(menuVisible: Boolean) {
@ -154,9 +155,12 @@ class TextFragment : NoteFragment() {
view.notes_counter.beGone() view.notes_counter.beGone()
} }
view.text_note_view.addTextChangedListener(textWatcher) setTextWatcher()
} }
fun setTextWatcher() = view.text_note_view.addTextChangedListener(textWatcher)
fun removeTextWatcher() = view.text_note_view.removeTextChangedListener(textWatcher)
fun updateNoteValue(value: String) { fun updateNoteValue(value: String) {
note?.value = value note?.value = value
} }

View File

@ -1,16 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.simplemobiletools.commons.views.MyViewPager <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/view_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager.widget.PagerTitleStrip <include layout="@layout/search_item" />
android:id="@+id/pager_title_strip"
<com.simplemobiletools.commons.views.MyViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:layout_gravity="top"
android:paddingLeft="@dimen/activity_margin"
android:paddingRight="@dimen/activity_margin"/>
</com.simplemobiletools.commons.views.MyViewPager> <androidx.viewpager.widget.PagerTitleStrip
android:id="@+id/pager_title_strip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingLeft="@dimen/activity_margin"
android:paddingRight="@dimen/activity_margin" />
</com.simplemobiletools.commons.views.MyViewPager>
</LinearLayout>

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/search_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/color_primary"
android:gravity="center_vertical"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="@dimen/medium_margin"
android:paddingBottom="@dimen/medium_margin">
<com.simplemobiletools.commons.views.MyEditText
android:id="@+id/search_query"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:layout_weight="1"
android:background="@null"
android:hint="@string/search"
android:maxLength="50"
android:maxLines="1"
android:singleLine="true"
android:textCursorDrawable="@null" />
<ImageView
android:id="@+id/search_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/smaller_margin"
android:background="?selectableItemBackgroundBorderless"
android:padding="@dimen/small_margin"
android:src="@drawable/ic_chevron_left_vector" />
<ImageView
android:id="@+id/search_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/smaller_margin"
android:background="?selectableItemBackgroundBorderless"
android:padding="@dimen/small_margin"
android:src="@drawable/ic_chevron_right_vector" />
<ImageView
android:id="@+id/search_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/medium_margin"
android:background="?selectableItemBackgroundBorderless"
android:padding="@dimen/small_margin"
android:src="@drawable/ic_cross_vector" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_gravity="bottom"
android:alpha="0.1"
android:background="@color/md_grey_white" />
</FrameLayout>

View File

@ -16,6 +16,11 @@
android:icon="@drawable/ic_redo_vector" android:icon="@drawable/ic_redo_vector"
android:title="@string/redo" android:title="@string/redo"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item
android:id="@+id/open_search"
android:icon="@drawable/ic_search_vector"
android:title="@string/search"
app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/open_note" android:id="@+id/open_note"
android:icon="@drawable/ic_folder_open_vector" android:icon="@drawable/ic_folder_open_vector"