「選択してコピー」の検索に正規表現トグルを追加

This commit is contained in:
tateisu 2023-05-06 21:33:24 +09:00
parent 705aa157e6
commit afe1a01995
3 changed files with 83 additions and 49 deletions

View File

@ -4,8 +4,6 @@ import android.app.SearchManager
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.text.Spanned
import android.text.style.BackgroundColorSpan
@ -72,33 +70,19 @@ class ActText : AppCompatActivity() {
}
}
class SearchResultSpan(color: Int) : BackgroundColorSpan(color)
private var account: SavedAccount? = null
private class SearchResultSpan(color: Int) : BackgroundColorSpan(color)
private val views by lazy {
ActTextBinding.inflate(layoutInflater)
}
private val selection: String
get() {
val et = views.etText
val s = et.selectionStart
val e = et.selectionEnd
val text = et.text.toString()
return if (s == e) {
text
} else {
text.substring(s, e)
}
}
private val searchTextChannel = Channel<Long>(capacity = Channel.CONFLATED)
private var searchResult: List<Int> = emptyList()
private var searchKeywordLength = 0
private var account: SavedAccount? = null
private val handler = Handler(Looper.getMainLooper())
private var searchResult: List<IntRange> = emptyList()
private var searchError: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -112,6 +96,7 @@ class ActText : AppCompatActivity() {
views.btnSearchClear.setOnClickListener { views.etSearch.setText("") }
views.btnSearchPrev.setOnClickListener { searchPrev() }
views.btnSearchNext.setOnClickListener { searchNext() }
views.btnToggleRegex.setOnCheckedChangeListener { _, _ -> postSearchText() }
lifecycleScope.launch {
while (true) {
@ -161,7 +146,7 @@ class ActText : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.miCopy -> selection.copyToClipboard(this)
R.id.miCopy -> selectionOrAll.copyToClipboard(this)
R.id.miSearch -> search()
R.id.miSend -> send()
R.id.miMuteWord -> muteWord()
@ -174,7 +159,7 @@ class ActText : AppCompatActivity() {
R.id.miTranslate -> CustomShare.invokeText(
CustomShareTarget.Translate,
this,
selection,
selectionOrAll,
)
else -> return super.onOptionsItemSelected(item)
@ -182,8 +167,23 @@ class ActText : AppCompatActivity() {
return true
}
/**
* 選択範囲またはテキスト全体
*/
private val selectionOrAll: String
get() {
val et = views.etText
val s = et.selectionStart
val e = et.selectionEnd
val text = et.text.toString()
return when (s) {
e -> text
else -> text.substring(s, e)
}
}
private fun send() {
selection.trim().notEmpty()?.let {
selectionOrAll.trim().notEmpty()?.let {
try {
val intent = Intent()
@ -199,7 +199,7 @@ class ActText : AppCompatActivity() {
}
private fun search() {
selection.trim().notEmpty()?.also {
selectionOrAll.trim().notEmpty()?.also {
try {
val intent = Intent(Intent.ACTION_WEB_SEARCH)
intent.putExtra(SearchManager.QUERY, it)
@ -214,7 +214,7 @@ class ActText : AppCompatActivity() {
}
private fun searchToot(@Suppress("SameParameterValue") resultCode: Int) {
selection.trim().notEmpty()?.let {
selectionOrAll.trim().notEmpty()?.let {
try {
val data = Intent()
data.putExtra(Intent.EXTRA_TEXT, it)
@ -229,7 +229,7 @@ class ActText : AppCompatActivity() {
private fun muteWord() {
launchAndShowError {
selection.trim().notEmpty()?.let {
selectionOrAll.trim().notEmpty()?.let {
daoMutedWord.save(it)
App1.getAppState(this@ActText).onMuteUpdated()
showToast(false, R.string.word_was_muted)
@ -238,7 +238,7 @@ class ActText : AppCompatActivity() {
}
private fun keywordFilter() {
selection.trim().notEmpty()?.let { text ->
selectionOrAll.trim().notEmpty()?.let { text ->
val account = this.account
if (account?.isPseudo == false && account.isMastodon) {
ActKeywordFilter.open(this, account, initialPhrase = text)
@ -258,7 +258,7 @@ class ActText : AppCompatActivity() {
}
private fun highlight() {
selection.trim().notEmpty()?.let {
selectionOrAll.trim().notEmpty()?.let {
startActivity(ActHighlightWordEdit.createIntent(this, it))
}
}
@ -276,11 +276,23 @@ class ActText : AppCompatActivity() {
private suspend fun searchTextImpl() {
val keyword = views.etSearch.text?.toString() ?: ""
val content = views.etText.text?.toString() ?: ""
val searchResult: List<Int> = withContext(AppDispatchers.IO) {
if (keyword.isEmpty()) {
emptyList()
} else {
buildList {
val useRegex = views.btnToggleRegex.isChecked
val searchResult: List<IntRange> = withContext(AppDispatchers.IO) {
buildList {
searchError = null
if (keyword.isEmpty()) {
// nothing to do.
} else if (useRegex) {
try {
val re = keyword.toRegex(RegexOption.IGNORE_CASE)
re.findAll(content).forEach { mr ->
add(mr.range)
}
} catch (ex: Throwable) {
log.e(ex, "search error.")
searchError = ex.message
}
} else {
var nextStart = 0
while (nextStart < content.length) {
val pos = content.indexOf(
@ -289,14 +301,14 @@ class ActText : AppCompatActivity() {
ignoreCase = true
)
if (pos == -1) break
add(pos)
nextStart = pos + keyword.length
val end = pos + keyword.length
add(pos until end)
nextStart = end
}
}
}
}
this.searchResult = searchResult
this.searchKeywordLength = keyword.length
views.btnSearchClear.isEnabledAlpha = keyword.isNotEmpty()
views.btnSearchPrev.isEnabledAlpha = searchResult.isNotEmpty()
@ -307,16 +319,16 @@ class ActText : AppCompatActivity() {
searchHighlight(null)
}
else -> searchNext(byTextUpdate=true)
else -> searchNext(byTextUpdate = true)
}
}
private fun searchNext(byTextUpdate: Boolean=false) {
private fun searchNext(byTextUpdate: Boolean = false) {
try {
val curPos = views.etText.selectionStart
val newPos = when {
byTextUpdate -> searchResult.find { it >= curPos }
else -> searchResult.find { it > curPos }
byTextUpdate -> searchResult.find { it.first >= curPos }
else -> searchResult.find { it.first > curPos }
} ?: searchResult.firstOrNull()
searchJump(newPos)
} catch (ex: Throwable) {
@ -329,7 +341,7 @@ class ActText : AppCompatActivity() {
val curPos = views.etText.selectionStart.takeIf { it >= 0 }
?: views.etText.text?.length
?: return
val newPos = searchResult.findLast { it < curPos }
val newPos = searchResult.findLast { it.first < curPos }
?: searchResult.lastOrNull()
searchJump(newPos)
} catch (ex: Throwable) {
@ -337,14 +349,14 @@ class ActText : AppCompatActivity() {
}
}
private fun searchJump(newPos: Int?) {
private fun searchJump(newPos: IntRange?) {
val idx = when (newPos) {
null -> null
else -> {
val end = views.etText.text?.length ?: 0
views.etText.setSelection(
newPos.clip(0, end),
(newPos + searchKeywordLength).clip(0, end),
newPos.first.clip(0, end),
(newPos.last + 1).clip(0, end),
)
searchResult.indexOf(newPos).takeIf { it >= 0 }?.plus(1)
}
@ -358,7 +370,8 @@ class ActText : AppCompatActivity() {
searchHighlight(newPos)
}
private fun searchHighlight(newPos: Int?) {
private fun searchHighlight(newPos: IntRange?) {
views.tvSearchError.vg(!searchError.isNullOrBlank())?.text = searchError
views.etText.text?.let { e ->
for (span in e.getSpans(0, e.length, SearchResultSpan::class.java)) {
try {
@ -373,8 +386,8 @@ class ActText : AppCompatActivity() {
}
e.setSpan(
SearchResultSpan(attrColor(attrId)),
pos,
pos + searchKeywordLength,
pos.first,
pos.last + 1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
)
}

View File

@ -37,17 +37,37 @@
android:hint="@string/search"
android:inputType="text" />
<ToggleButton
android:id="@+id/btnToggleRegex"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:disabledAlpha="0.3"
android:elevation="0dp"
android:minHeight="48dp"
android:minWidth="48dp"
android:stateListAnimator="@null"
android:textOff="@string/toggle_regexp"
android:textOn="@string/toggle_regexp"
tools:isChecked="true" />
<ImageButton
android:id="@+id/btnSearchClear"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="4dp"
android:layout_marginStart="2dp"
android:contentDescription="@string/clear"
android:src="@drawable/ic_close"
app:tint="?attr/colorTextContent" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tvSearchError"
android:textColor="?attr/colorRegexFilterError"
/>
<LinearLayout
android:id="@+id/llSearchResult"
android:layout_width="match_parent"

View File

@ -1296,4 +1296,5 @@
<string name="app_data_export_import">Export/Import app data</string>
<string name="translate_or_custom_share">Translate/Custom share buttons</string>
<string name="search_result" translatable="false">%1$s/%2$d</string>
<string name="toggle_regexp">.+\?</string>
</resources>