選択してコピー画面にテキスト欄の検索機能を追加
This commit is contained in:
parent
75e6001fcd
commit
1ccd05e08a
@ -4,9 +4,16 @@ 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
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccount
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.databinding.ActTextBinding
|
||||
@ -19,12 +26,17 @@ import jp.juggler.subwaytooter.util.CustomShareTarget
|
||||
import jp.juggler.subwaytooter.util.TootTextEncoder
|
||||
import jp.juggler.subwaytooter.util.copyToClipboard
|
||||
import jp.juggler.util.*
|
||||
import jp.juggler.util.coroutine.AppDispatchers
|
||||
import jp.juggler.util.coroutine.launchAndShowError
|
||||
import jp.juggler.util.coroutine.launchMain
|
||||
import jp.juggler.util.data.*
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.log.showToast
|
||||
import jp.juggler.util.ui.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ActText : AppCompatActivity() {
|
||||
|
||||
@ -60,6 +72,8 @@ class ActText : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
class SearchHilightSpan(color: Int) : BackgroundColorSpan(color)
|
||||
|
||||
private var account: SavedAccount? = null
|
||||
|
||||
private val views by lazy {
|
||||
@ -79,54 +93,45 @@ class ActText : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.btnCopy -> selection.copyToClipboard(this)
|
||||
private val searchTextChannel = Channel<Long>(capacity = Channel.CONFLATED)
|
||||
|
||||
R.id.btnSearch -> search()
|
||||
private var searchResult: List<Int> = emptyList()
|
||||
private var searchKeywordLength = 0
|
||||
|
||||
R.id.btnSend -> send()
|
||||
|
||||
R.id.btnMuteWord -> muteWord()
|
||||
|
||||
R.id.btnTranslate -> CustomShare.invokeText(
|
||||
CustomShareTarget.Translate,
|
||||
this,
|
||||
selection,
|
||||
)
|
||||
|
||||
// MSP検索ボタン -> searchToot(RESULT_SEARCH_MSP)
|
||||
// R.id.btnSearchTS -> searchToot(RESULT_SEARCH_TS)
|
||||
|
||||
R.id.btnSearchNotestock -> searchToot(RESULT_SEARCH_NOTESTOCK)
|
||||
|
||||
R.id.btnKeywordFilter -> keywordFilter()
|
||||
|
||||
R.id.btnHighlight -> highlight()
|
||||
|
||||
// If we got here, the user's action was not recognized.
|
||||
// Invoke the superclass to handle it.
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.act_text, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
App1.setActivityTheme(this)
|
||||
|
||||
initUI()
|
||||
setContentView(views.root)
|
||||
setSupportActionBar(views.toolbar)
|
||||
setNavigationBack(views.toolbar)
|
||||
fixHorizontalMargin(views.etText)
|
||||
views.etSearch.addTextChangedListener { postSearchText() }
|
||||
views.btnSearchClear.setOnClickListener { views.etSearch.setText("") }
|
||||
views.btnSearchPrev.setOnClickListener { searchPrev() }
|
||||
views.btnSearchNext.setOnClickListener { searchNext() }
|
||||
|
||||
lifecycleScope.launch {
|
||||
while (true) {
|
||||
try {
|
||||
searchTextChannel.receive()
|
||||
searchTextImpl()
|
||||
} catch (ex: Throwable) {
|
||||
if (ex is CancellationException) break
|
||||
log.e(ex, "searchTextChannel failed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launchAndShowError {
|
||||
account = intent.long(EXTRA_ACCOUNT_DB_ID)
|
||||
?.let { daoSavedAccount.loadAccount(it) }
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
views.llSearchResult.gone()
|
||||
|
||||
val sv = intent.string(EXTRA_TEXT) ?: ""
|
||||
val contentStart = intent.int(EXTRA_CONTENT_START) ?: 0
|
||||
val contentEnd = intent.int(EXTRA_CONTENT_END) ?: sv.length
|
||||
@ -143,11 +148,38 @@ class ActText : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun initUI() {
|
||||
setContentView(views.root)
|
||||
setSupportActionBar(views.toolbar)
|
||||
setNavigationBack(views.toolbar)
|
||||
fixHorizontalMargin(views.etText)
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
postSearchText()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.act_text, menu)
|
||||
super.onCreateOptionsMenu(menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.miCopy -> selection.copyToClipboard(this)
|
||||
R.id.miSearch -> search()
|
||||
R.id.miSend -> send()
|
||||
R.id.miMuteWord -> muteWord()
|
||||
R.id.miSearchNotestock -> searchToot(RESULT_SEARCH_NOTESTOCK)
|
||||
R.id.miKeywordFilter -> keywordFilter()
|
||||
R.id.miHighlight -> highlight()
|
||||
// MSP検索ボタン -> searchToot(RESULT_SEARCH_MSP)
|
||||
// R.id.btnSearchTS -> searchToot(RESULT_SEARCH_TS)
|
||||
|
||||
R.id.miTranslate -> CustomShare.invokeText(
|
||||
CustomShareTarget.Translate,
|
||||
this,
|
||||
selection,
|
||||
)
|
||||
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun send() {
|
||||
@ -230,4 +262,122 @@ class ActText : AppCompatActivity() {
|
||||
startActivity(ActHighlightWordEdit.createIntent(this, it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun postSearchText() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
searchTextChannel.send(SystemClock.elapsedRealtime())
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "postSearchText failed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
var nextStart = 0
|
||||
while (nextStart < content.length) {
|
||||
val pos = content.indexOf(
|
||||
keyword,
|
||||
startIndex = nextStart,
|
||||
ignoreCase = true
|
||||
)
|
||||
if (pos == -1) break
|
||||
add(pos)
|
||||
nextStart = pos + keyword.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.searchResult = searchResult
|
||||
this.searchKeywordLength = keyword.length
|
||||
|
||||
views.btnSearchClear.isEnabledAlpha = keyword.isNotEmpty()
|
||||
views.btnSearchPrev.isEnabledAlpha = searchResult.isNotEmpty()
|
||||
views.btnSearchNext.isEnabledAlpha = searchResult.isNotEmpty()
|
||||
when {
|
||||
searchResult.isEmpty() -> {
|
||||
views.llSearchResult.gone()
|
||||
searchHighlight(null)
|
||||
}
|
||||
|
||||
else -> searchNext(byTextUpdate=true)
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
} ?: searchResult.firstOrNull()
|
||||
searchJump(newPos)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "searchNext failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchPrev() {
|
||||
try {
|
||||
val curPos = views.etText.selectionStart.takeIf { it >= 0 }
|
||||
?: views.etText.text?.length
|
||||
?: return
|
||||
val newPos = searchResult.findLast { it < curPos }
|
||||
?: searchResult.lastOrNull()
|
||||
searchJump(newPos)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "searchPrev failed.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchJump(newPos: Int?) {
|
||||
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),
|
||||
)
|
||||
searchResult.indexOf(newPos).takeIf { it >= 0 }?.plus(1)
|
||||
}
|
||||
}
|
||||
views.llSearchResult.visible()
|
||||
views.tvSearchCount.text = getString(
|
||||
R.string.search_result,
|
||||
idx?.toString() ?: "?",
|
||||
searchResult.size
|
||||
)
|
||||
searchHighlight(newPos)
|
||||
}
|
||||
|
||||
private fun searchHighlight(newPos: Int?) {
|
||||
views.etText.text?.let { e ->
|
||||
for (span in e.getSpans(0, e.length, SearchHilightSpan::class.java)) {
|
||||
try {
|
||||
e.removeSpan(span)
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
}
|
||||
for (pos in searchResult) {
|
||||
val attrId = when (newPos) {
|
||||
pos -> R.attr.colorSearchFormBackground
|
||||
else -> R.attr.colorButtonBgCw
|
||||
}
|
||||
e.setSpan(
|
||||
SearchHilightSpan(attrColor(attrId)),
|
||||
pos,
|
||||
pos + searchKeywordLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
@ -10,8 +11,79 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@drawable/action_bar_bg"
|
||||
app:navigationIcon="?attr/homeAsUpIndicator"
|
||||
android:elevation="4dp" />
|
||||
android:elevation="4dp"
|
||||
app:navigationIcon="?attr/homeAsUpIndicator" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSearchFormBackground"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:baselineAligned="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<jp.juggler.subwaytooter.view.MyEditText
|
||||
android:id="@+id/etSearch"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/search"
|
||||
android:inputType="text" />
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSearchClear"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/clear"
|
||||
android:src="@drawable/ic_close"
|
||||
app:tint="?attr/colorTextContent" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llSearchResult"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:baselineAligned="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSearchCount"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
tools:text="12/345" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSearchPrev"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/previous"
|
||||
android:src="@drawable/ic_arrow_drop_up"
|
||||
app:tint="?attr/colorTextContent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnSearchNext"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription="@string/next"
|
||||
android:src="@drawable/ic_arrow_drop_down"
|
||||
app:tint="?attr/colorTextContent" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<jp.juggler.subwaytooter.view.MyEditText
|
||||
android:id="@+id/etText"
|
||||
|
@ -3,47 +3,47 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/btnCopy"
|
||||
android:id="@+id/miCopy"
|
||||
android:title="@string/copy_st"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/btnSend"
|
||||
android:id="@+id/miSend"
|
||||
android:title="@string/send"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/btnSearch"
|
||||
android:id="@+id/miSearch"
|
||||
android:title="@string/search_web"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<!-- <item-->
|
||||
<!-- android:id="@+id/btnSearchTS"-->
|
||||
<!-- android:title="@string/toot_search_ts"-->
|
||||
<!-- app:showAsAction="never" />-->
|
||||
<!-- <item-->
|
||||
<!-- android:id="@+id/btnSearchTS"-->
|
||||
<!-- android:title="@string/toot_search_ts"-->
|
||||
<!-- app:showAsAction="never" />-->
|
||||
|
||||
<item
|
||||
android:id="@+id/btnSearchNotestock"
|
||||
android:id="@+id/miSearchNotestock"
|
||||
android:title="@string/toot_search_notestock"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/btnTranslate"
|
||||
android:id="@+id/miTranslate"
|
||||
android:title="@string/translate"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/btnMuteWord"
|
||||
android:id="@+id/miMuteWord"
|
||||
android:title="@string/mute_word"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/btnKeywordFilter"
|
||||
android:id="@+id/miKeywordFilter"
|
||||
android:title="@string/keyword_filter"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/btnHighlight"
|
||||
android:id="@+id/miHighlight"
|
||||
android:title="@string/highlight_word"
|
||||
app:showAsAction="never" />
|
||||
|
||||
|
@ -1295,4 +1295,5 @@
|
||||
<string name="save_to_local_folder">Save to local folder</string>
|
||||
<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>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user