2021-06-28 09:09:00 +02:00
|
|
|
package jp.juggler.subwaytooter.actpost
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
|
|
import android.content.SharedPreferences
|
|
|
|
import android.os.Handler
|
|
|
|
import android.text.*
|
|
|
|
import android.text.style.ForegroundColorSpan
|
|
|
|
import android.view.View
|
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
|
|
import jp.juggler.subwaytooter.App1
|
|
|
|
import jp.juggler.subwaytooter.R
|
|
|
|
import jp.juggler.subwaytooter.api.*
|
|
|
|
import jp.juggler.subwaytooter.api.entity.*
|
|
|
|
import jp.juggler.subwaytooter.dialog.ActionsDialog
|
|
|
|
import jp.juggler.subwaytooter.dialog.EmojiPicker
|
|
|
|
import jp.juggler.subwaytooter.dialog.EmojiPickerResult
|
|
|
|
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
|
|
|
import jp.juggler.subwaytooter.emoji.EmojiBase
|
|
|
|
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
|
2021-11-07 13:00:06 +01:00
|
|
|
import jp.juggler.subwaytooter.pref.PrefB
|
2021-06-21 05:03:09 +02:00
|
|
|
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
|
|
|
|
import jp.juggler.subwaytooter.table.AcctSet
|
|
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
|
|
|
import jp.juggler.subwaytooter.table.TagSet
|
2021-06-28 09:09:00 +02:00
|
|
|
import jp.juggler.subwaytooter.util.DecodeOptions
|
|
|
|
import jp.juggler.subwaytooter.util.EmojiDecoder
|
|
|
|
import jp.juggler.subwaytooter.util.PopupAutoCompleteAcct
|
2021-06-21 05:03:09 +02:00
|
|
|
import jp.juggler.subwaytooter.view.MyEditText
|
|
|
|
import jp.juggler.util.*
|
|
|
|
import java.util.*
|
|
|
|
import kotlin.math.min
|
|
|
|
|
2021-06-27 12:05:04 +02:00
|
|
|
// 入力補完機能
|
2021-06-21 05:03:09 +02:00
|
|
|
class CompletionHelper(
|
|
|
|
private val activity: AppCompatActivity,
|
|
|
|
private val pref: SharedPreferences,
|
|
|
|
private val handler: Handler,
|
|
|
|
) {
|
|
|
|
companion object {
|
|
|
|
private val log = LogCategory("CompletionHelper")
|
2021-11-21 07:38:25 +01:00
|
|
|
private val reCharsNotEmoji = "[^0-9A-Za-z_-]".asciiRegex()
|
2021-11-07 13:00:06 +01:00
|
|
|
|
|
|
|
// 無視するスパン
|
|
|
|
// ($を.に変換済)
|
|
|
|
val ignoreSpans = setOf(
|
|
|
|
"android.text.Selection.END",
|
|
|
|
"android.text.Selection.START",
|
|
|
|
"android.widget.Editor.SpanController",
|
|
|
|
"android.widget.TextView.ChangeWatcher",
|
2021-11-18 18:09:22 +01:00
|
|
|
"androidx.emoji2.text.SpannableBuilder.WatcherWrapper",
|
|
|
|
"androidx.emoji2.viewsintegration.EmojiKeyListener",
|
|
|
|
|
|
|
|
"android.text.DynamicLayout.ChangeWatcher",
|
|
|
|
"android.text.method.TextKeyListener",
|
|
|
|
"android.text.method.Touch.DragState",
|
2021-11-15 05:27:28 +01:00
|
|
|
"android.text.style.SpellCheckSpan",
|
2021-11-07 13:00:06 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
private val reRemoveSpan = """\Qandroid.text.style.\E.+Span""".toRegex()
|
2021-11-21 07:38:25 +01:00
|
|
|
|
|
|
|
private fun matchUserNameOrAsciiDomain(cp: Int): Boolean {
|
|
|
|
if (cp >= 0x7f) return false
|
|
|
|
val c = cp.toChar()
|
|
|
|
|
|
|
|
return '0' <= c && c <= '9' ||
|
|
|
|
'A' <= c && c <= 'Z' ||
|
|
|
|
'a' <= c && c <= 'z' ||
|
|
|
|
c == '_' || c == '-' || c == '.'
|
|
|
|
}
|
|
|
|
|
|
|
|
// Letter | Mark | Decimal_Number | Connector_Punctuation
|
|
|
|
private fun matchIdnWord(cp: Int) = when (Character.getType(cp).toByte()) {
|
|
|
|
// Letter
|
|
|
|
// LCはエイリアスなので文字から得られることはないはず
|
|
|
|
Character.UPPERCASE_LETTER,
|
|
|
|
Character.LOWERCASE_LETTER,
|
|
|
|
Character.TITLECASE_LETTER,
|
|
|
|
Character.MODIFIER_LETTER,
|
|
|
|
Character.OTHER_LETTER,
|
|
|
|
-> true
|
|
|
|
// Mark
|
|
|
|
Character.NON_SPACING_MARK,
|
|
|
|
Character.COMBINING_SPACING_MARK,
|
|
|
|
Character.ENCLOSING_MARK,
|
|
|
|
-> true
|
|
|
|
// Decimal_Number
|
|
|
|
Character.DECIMAL_DIGIT_NUMBER -> true
|
|
|
|
// Connector_Punctuation
|
|
|
|
Character.CONNECTOR_PUNCTUATION -> true
|
|
|
|
|
|
|
|
else -> false
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
}
|
|
|
|
|
2021-06-27 12:05:04 +02:00
|
|
|
interface Callback2 {
|
|
|
|
fun onTextUpdate()
|
|
|
|
fun canOpenPopup(): Boolean
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
|
|
private val pickerCaptionEmoji: String by lazy {
|
|
|
|
activity.getString(R.string.open_picker_emoji)
|
|
|
|
}
|
|
|
|
|
|
|
|
private var callback2: Callback2? = null
|
|
|
|
private var et: MyEditText? = null
|
|
|
|
private var popup: PopupAutoCompleteAcct? = null
|
|
|
|
private var formRoot: View? = null
|
|
|
|
private var bMainScreen: Boolean = false
|
|
|
|
|
|
|
|
private var accessInfo: SavedAccount? = null
|
|
|
|
|
2021-06-27 12:05:04 +02:00
|
|
|
private val onEmojiListLoad: (list: ArrayList<CustomEmoji>) -> Unit = {
|
|
|
|
if (popup?.isShowing == true) procTextChanged.run()
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
private val procTextChanged: Runnable = Runnable {
|
|
|
|
val et = this.et
|
|
|
|
if (et == null || et.selectionStart != et.selectionEnd || callback2?.canOpenPopup() != true) {
|
|
|
|
// EditTextを特定できない
|
|
|
|
// 範囲選択中
|
|
|
|
// 何らかの理由でポップアップが許可されていない
|
|
|
|
closeAcctPopup()
|
|
|
|
} else {
|
2021-06-21 05:03:09 +02:00
|
|
|
checkMention(et, et.text.toString())
|
|
|
|
}
|
2021-11-21 07:38:25 +01:00
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
private fun checkMention(et: MyEditText, src: String) {
|
|
|
|
// 選択範囲末尾からスキャン
|
2022-03-11 00:11:49 +01:00
|
|
|
var countAtmark = 0
|
2021-11-21 07:38:25 +01:00
|
|
|
var start: Int = -1
|
|
|
|
val end = et.selectionEnd
|
|
|
|
var i = end
|
|
|
|
while (i > 0) {
|
|
|
|
val cp = src.codePointBefore(i)
|
|
|
|
i -= Character.charCount(cp)
|
|
|
|
|
|
|
|
if (cp == '@'.code) {
|
|
|
|
start = i
|
2022-03-11 00:11:49 +01:00
|
|
|
if (++countAtmark >= 2) break else continue
|
|
|
|
} else if (countAtmark == 1) {
|
2021-11-21 07:38:25 +01:00
|
|
|
// @username@host の username部分はUnicodeを含まない
|
|
|
|
if (matchUserNameOrAsciiDomain(cp)) continue else break
|
2021-06-21 05:03:09 +02:00
|
|
|
} else {
|
2021-11-21 07:38:25 +01:00
|
|
|
// @username@host のhost 部分か、 @username のusername部分
|
|
|
|
// ここはUnicodeを含むかもしれない
|
|
|
|
if (matchUserNameOrAsciiDomain(cp) || matchIdnWord(cp)) continue else break
|
2021-06-21 05:03:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
if (start == -1) {
|
|
|
|
checkTag(et, src)
|
|
|
|
return
|
2021-06-21 05:03:09 +02:00
|
|
|
}
|
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
// 最低でも2文字ないと補完しない
|
|
|
|
if (end - start < 2) {
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
val limit = 100
|
|
|
|
val s = src.substring(start, end)
|
2022-03-11 00:11:49 +01:00
|
|
|
val acctList = AcctSet.searchPrefix(s, limit)
|
|
|
|
log.d("search for $s, result=${acctList.size}")
|
|
|
|
if (acctList.isEmpty()) {
|
2021-11-21 07:38:25 +01:00
|
|
|
closeAcctPopup()
|
|
|
|
} else {
|
2022-03-11 00:11:49 +01:00
|
|
|
openPopup()?.setList(et, start, end, acctList, null, null)
|
2021-11-21 07:38:25 +01:00
|
|
|
}
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
private fun checkTag(et: MyEditText, src: String) {
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
val end = et.selectionEnd
|
2022-03-11 00:11:49 +01:00
|
|
|
val lastSharp = src.lastIndexOf('#', end - 1)
|
|
|
|
if (lastSharp == -1 || end - lastSharp < 2) {
|
2021-11-21 07:38:25 +01:00
|
|
|
checkEmoji(et, src)
|
|
|
|
return
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2022-03-11 00:11:49 +01:00
|
|
|
val part = src.substring(lastSharp + 1, end)
|
2021-11-21 07:38:25 +01:00
|
|
|
if (!TootTag.isValid(part, accessInfo?.isMisskey == true)) {
|
|
|
|
checkEmoji(et, src)
|
|
|
|
return
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
val limit = 100
|
2022-03-11 00:11:49 +01:00
|
|
|
val s = src.substring(lastSharp + 1, end)
|
|
|
|
val tagList = TagSet.searchPrefix(s, limit)
|
|
|
|
log.d("search for $s, result=${tagList.size}")
|
|
|
|
if (tagList.isEmpty()) {
|
2021-11-21 07:38:25 +01:00
|
|
|
closeAcctPopup()
|
|
|
|
} else {
|
2022-03-11 00:11:49 +01:00
|
|
|
openPopup()?.setList(et, lastSharp, end, tagList, null, null)
|
2021-11-21 07:38:25 +01:00
|
|
|
}
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
private fun checkEmoji(et: MyEditText, src: String) {
|
|
|
|
val end = et.selectionEnd
|
2022-03-11 00:11:49 +01:00
|
|
|
val lastColon = src.lastIndexOf(':', end - 1)
|
|
|
|
if (lastColon == -1 || end - lastColon < 1) {
|
2021-11-21 07:38:25 +01:00
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2022-03-11 00:11:49 +01:00
|
|
|
if (!EmojiDecoder.canStartShortCode(src, lastColon)) {
|
2021-11-21 07:38:25 +01:00
|
|
|
// : の手前は始端か改行か空白でなければならない
|
|
|
|
log.d("checkEmoji: invalid character before shortcode.")
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2022-03-11 00:11:49 +01:00
|
|
|
val part = src.substring(lastColon + 1, end)
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
if (part.isEmpty()) {
|
|
|
|
// :を入力した直後は候補は0で、「閉じる」と「絵文字を選ぶ」だけが表示されたポップアップを出す
|
2021-06-21 05:03:09 +02:00
|
|
|
openPopup()?.setList(
|
2022-03-11 00:11:49 +01:00
|
|
|
et, lastColon, end, null, pickerCaptionEmoji, openPickerEmoji
|
2021-06-21 05:03:09 +02:00
|
|
|
)
|
2021-11-21 07:38:25 +01:00
|
|
|
return
|
2021-06-21 05:03:09 +02:00
|
|
|
}
|
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
if (reCharsNotEmoji.containsMatchIn(part)) {
|
|
|
|
// 範囲内に絵文字に使えない文字がある
|
|
|
|
closeAcctPopup()
|
|
|
|
return
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2022-03-11 00:11:49 +01:00
|
|
|
val codeList = ArrayList<CharSequence>()
|
2021-11-21 07:38:25 +01:00
|
|
|
val limit = 100
|
|
|
|
|
|
|
|
// カスタム絵文字の候補を部分一致検索
|
2022-03-11 00:11:49 +01:00
|
|
|
codeList.addAll(customEmojiCodeList(accessInfo, limit, part))
|
2021-11-21 07:38:25 +01:00
|
|
|
|
|
|
|
// 通常の絵文字を部分一致で検索
|
2022-03-11 00:11:49 +01:00
|
|
|
val remain = limit - codeList.size
|
2021-11-21 07:38:25 +01:00
|
|
|
if (remain > 0) {
|
2022-03-11 00:11:49 +01:00
|
|
|
val s = src.substring(lastColon + 1, end)
|
|
|
|
.lowercase()
|
|
|
|
.replace('-', '_')
|
2021-11-21 07:38:25 +01:00
|
|
|
val matches = EmojiDecoder.searchShortCode(activity, s, remain)
|
|
|
|
log.d("checkEmoji: search for $s, result=${matches.size}")
|
2022-03-11 00:11:49 +01:00
|
|
|
codeList.addAll(matches)
|
2021-11-21 07:38:25 +01:00
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
openPopup()?.setList(
|
|
|
|
et,
|
2022-03-11 00:11:49 +01:00
|
|
|
lastColon,
|
2021-11-21 07:38:25 +01:00
|
|
|
end,
|
2022-03-11 00:11:49 +01:00
|
|
|
codeList,
|
2021-11-21 07:38:25 +01:00
|
|
|
pickerCaptionEmoji,
|
|
|
|
openPickerEmoji
|
|
|
|
)
|
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
// カスタム絵文字の候補を作る
|
|
|
|
private fun customEmojiCodeList(
|
|
|
|
accessInfo: SavedAccount?,
|
|
|
|
@Suppress("SameParameterValue") limit: Int,
|
|
|
|
needle: String,
|
|
|
|
) = buildList<CharSequence> {
|
|
|
|
accessInfo ?: return@buildList
|
|
|
|
|
2022-03-11 00:11:49 +01:00
|
|
|
val customList =
|
2021-11-21 07:38:25 +01:00
|
|
|
App1.custom_emoji_lister.getListWithAliases(accessInfo, onEmojiListLoad)
|
|
|
|
?: return@buildList
|
|
|
|
|
2022-03-11 00:11:49 +01:00
|
|
|
for (item in customList) {
|
2021-11-21 07:38:25 +01:00
|
|
|
if (size >= limit) break
|
|
|
|
if (!item.shortcode.contains(needle)) continue
|
|
|
|
|
|
|
|
val sb = SpannableStringBuilder()
|
|
|
|
sb.append(' ')
|
|
|
|
sb.setSpan(
|
|
|
|
NetworkEmojiSpan(item.url),
|
|
|
|
0,
|
|
|
|
sb.length,
|
|
|
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
|
|
)
|
|
|
|
sb.append(' ')
|
|
|
|
if (item.alias != null) {
|
|
|
|
val start = sb.length
|
|
|
|
sb.append(":")
|
|
|
|
sb.append(item.alias)
|
|
|
|
sb.append(": → ")
|
2021-06-21 05:03:09 +02:00
|
|
|
sb.setSpan(
|
2021-11-21 07:38:25 +01:00
|
|
|
ForegroundColorSpan(activity.attrColor(R.attr.colorTimeSmall)),
|
|
|
|
start,
|
2021-06-21 05:03:09 +02:00
|
|
|
sb.length,
|
|
|
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
|
|
)
|
|
|
|
}
|
2021-11-21 07:38:25 +01:00
|
|
|
|
|
|
|
sb.append(':')
|
|
|
|
sb.append(item.shortcode)
|
|
|
|
sb.append(':')
|
|
|
|
add(sb)
|
2021-06-21 05:03:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun openPopup(): PopupAutoCompleteAcct? {
|
|
|
|
var popup = this@CompletionHelper.popup
|
|
|
|
if (popup?.isShowing == true) return popup
|
|
|
|
val et = this@CompletionHelper.et ?: return null
|
|
|
|
val formRoot = this@CompletionHelper.formRoot ?: return null
|
|
|
|
popup = PopupAutoCompleteAcct(activity, et, formRoot, bMainScreen)
|
|
|
|
this@CompletionHelper.popup = popup
|
|
|
|
return popup
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setInstance(accessInfo: SavedAccount?) {
|
|
|
|
this.accessInfo = accessInfo
|
2021-06-27 12:05:04 +02:00
|
|
|
accessInfo?.let { App1.custom_emoji_lister.getList(it, onEmojiListLoad) }
|
|
|
|
if (popup?.isShowing == true) procTextChanged.run()
|
2021-06-21 05:03:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fun closeAcctPopup() {
|
|
|
|
popup?.dismiss()
|
|
|
|
popup = null
|
|
|
|
}
|
|
|
|
|
|
|
|
fun onScrollChanged() {
|
2021-06-27 12:05:04 +02:00
|
|
|
popup?.takeIf { it.isShowing }?.updatePosition()
|
2021-06-21 05:03:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fun onDestroy() {
|
|
|
|
handler.removeCallbacks(procTextChanged)
|
|
|
|
closeAcctPopup()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun attachEditText(
|
|
|
|
formRoot: View,
|
|
|
|
et: MyEditText,
|
|
|
|
bMainScreen: Boolean,
|
|
|
|
callback2: Callback2,
|
|
|
|
) {
|
|
|
|
this.formRoot = formRoot
|
|
|
|
this.et = et
|
|
|
|
this.callback2 = callback2
|
|
|
|
this.bMainScreen = bMainScreen
|
|
|
|
|
|
|
|
et.addTextChangedListener(object : TextWatcher {
|
|
|
|
override fun beforeTextChanged(
|
|
|
|
s: CharSequence,
|
|
|
|
start: Int,
|
|
|
|
count: Int,
|
|
|
|
after: Int,
|
|
|
|
) {
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
|
|
|
handler.removeCallbacks(procTextChanged)
|
|
|
|
handler.postDelayed(procTextChanged, if (popup?.isShowing == true) 100L else 500L)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun afterTextChanged(s: Editable) {
|
2021-11-07 13:00:06 +01:00
|
|
|
// ペースト時に余計な装飾を取り除く
|
2021-11-15 03:03:03 +01:00
|
|
|
val spans = s.getSpans(0, s.length, Any::class.java)
|
|
|
|
|
|
|
|
val isImeComposing =
|
|
|
|
spans.any { it?.javaClass?.name == "android.view.inputmethod.ComposingText" }
|
|
|
|
|
2021-11-18 18:09:22 +01:00
|
|
|
if (!isImeComposing) {
|
2021-11-15 03:03:03 +01:00
|
|
|
spans?.filter {
|
2021-11-07 13:00:06 +01:00
|
|
|
val name = (it?.javaClass?.name ?: "").replace('$', '.')
|
|
|
|
when {
|
|
|
|
ignoreSpans.contains(name) -> false
|
2021-11-15 03:03:03 +01:00
|
|
|
|
2021-11-07 13:00:06 +01:00
|
|
|
reRemoveSpan.matches(name) -> {
|
|
|
|
log.i("span remove $name")
|
|
|
|
true
|
|
|
|
}
|
2021-11-15 03:03:03 +01:00
|
|
|
|
2021-11-07 13:00:06 +01:00
|
|
|
else -> {
|
|
|
|
log.i("span keep $name")
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-11-15 03:03:03 +01:00
|
|
|
?.map { Triple(it, s.getSpanStart(it), s.getSpanEnd(it)) }
|
|
|
|
?.sortedBy { -it.second }
|
|
|
|
?.forEach {
|
|
|
|
s.removeSpan(it.first)
|
|
|
|
}
|
|
|
|
}
|
2021-11-07 13:00:06 +01:00
|
|
|
|
2021-06-21 05:03:09 +02:00
|
|
|
this@CompletionHelper.callback2?.onTextUpdate()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-10-27 22:58:19 +02:00
|
|
|
// 範囲選択されてるならポップアップは閉じる
|
|
|
|
et.onSelectionChange = { selStart, selEnd ->
|
|
|
|
if (selStart != selEnd) {
|
|
|
|
log.d("onSelectionChange: range selected")
|
|
|
|
closeAcctPopup()
|
2021-06-21 05:03:09 +02:00
|
|
|
}
|
2021-10-27 22:58:19 +02:00
|
|
|
}
|
2021-06-21 05:03:09 +02:00
|
|
|
|
|
|
|
// 全然動いてなさそう…
|
|
|
|
// et.setCustomSelectionActionModeCallback( action_mode_callback );
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun SpannableStringBuilder.appendEmoji(result: EmojiPickerResult) =
|
|
|
|
appendEmoji(result.bInstanceHasCustomEmoji, result.emoji)
|
|
|
|
|
|
|
|
private fun SpannableStringBuilder.appendEmoji(
|
|
|
|
bInstanceHasCustomEmoji: Boolean,
|
|
|
|
emoji: EmojiBase,
|
|
|
|
): SpannableStringBuilder {
|
|
|
|
|
2021-11-21 07:38:25 +01:00
|
|
|
val separator = EmojiDecoder.customEmojiSeparator()
|
2021-06-21 05:03:09 +02:00
|
|
|
when (emoji) {
|
|
|
|
is CustomEmoji -> {
|
|
|
|
// カスタム絵文字は常にshortcode表現
|
|
|
|
if (!EmojiDecoder.canStartShortCode(this, this.length)) append(separator)
|
|
|
|
this.append(SpannableString(":${emoji.shortcode}:"))
|
|
|
|
// セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。
|
|
|
|
if (separator != ' ') append(separator)
|
|
|
|
}
|
|
|
|
is UnicodeEmoji -> {
|
|
|
|
if (!bInstanceHasCustomEmoji) {
|
|
|
|
// 古いタンスだとshortcodeを使う。見た目は絵文字に変える。
|
|
|
|
if (!EmojiDecoder.canStartShortCode(this, this.length)) append(separator)
|
|
|
|
this.append(DecodeOptions(activity).decodeEmoji(":${emoji.unifiedName}:"))
|
|
|
|
// セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。
|
|
|
|
if (separator != ' ') append(separator)
|
|
|
|
} else {
|
|
|
|
// 十分に新しいタンスなら絵文字のunicodeを使う。見た目は絵文字に変える。
|
|
|
|
this.append(DecodeOptions(activity).decodeEmoji(emoji.unifiedCode))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
private val openPickerEmoji: Runnable = Runnable {
|
|
|
|
EmojiPicker(
|
|
|
|
activity, accessInfo,
|
2021-06-22 10:31:51 +02:00
|
|
|
closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected(pref)
|
2021-06-21 05:03:09 +02:00
|
|
|
) { result ->
|
|
|
|
val et = this.et ?: return@EmojiPicker
|
|
|
|
|
|
|
|
val src = et.text ?: ""
|
|
|
|
val srcLength = src.length
|
|
|
|
val end = min(srcLength, et.selectionEnd)
|
|
|
|
val start = src.lastIndexOf(':', end - 1)
|
|
|
|
if (start == -1 || end - start < 1) return@EmojiPicker
|
|
|
|
|
|
|
|
val sb = SpannableStringBuilder()
|
|
|
|
.append(src.subSequence(0, start))
|
|
|
|
.appendEmoji(result)
|
|
|
|
|
|
|
|
val newSelection = sb.length
|
|
|
|
if (end < srcLength) sb.append(src.subSequence(end, srcLength))
|
|
|
|
|
|
|
|
et.text = sb
|
|
|
|
et.setSelection(newSelection)
|
|
|
|
|
|
|
|
procTextChanged.run()
|
|
|
|
|
|
|
|
// キーボードを再度表示する
|
|
|
|
App1.getAppState(
|
|
|
|
activity,
|
|
|
|
"PostHelper/EmojiPicker/cb"
|
|
|
|
).handler.post { et.showKeyboard() }
|
|
|
|
}.show()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun openEmojiPickerFromMore() {
|
|
|
|
EmojiPicker(
|
|
|
|
activity, accessInfo,
|
2021-06-22 10:31:51 +02:00
|
|
|
closeOnSelected = PrefB.bpEmojiPickerCloseOnSelected(pref)
|
2021-06-21 05:03:09 +02:00
|
|
|
) { result ->
|
|
|
|
val et = this.et ?: return@EmojiPicker
|
|
|
|
|
|
|
|
val src = et.text ?: ""
|
|
|
|
val srcLength = src.length
|
|
|
|
val start = min(srcLength, et.selectionStart)
|
|
|
|
val end = min(srcLength, et.selectionEnd)
|
|
|
|
|
|
|
|
val sb = SpannableStringBuilder()
|
|
|
|
.append(src.subSequence(0, start))
|
|
|
|
.appendEmoji(result)
|
|
|
|
|
|
|
|
val newSelection = sb.length
|
|
|
|
if (end < srcLength) sb.append(src.subSequence(end, srcLength))
|
|
|
|
|
|
|
|
et.text = sb
|
|
|
|
et.setSelection(newSelection)
|
|
|
|
|
|
|
|
procTextChanged.run()
|
|
|
|
}.show()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun SpannableStringBuilder.appendHashTag(tagWithoutSharp: String): SpannableStringBuilder {
|
|
|
|
val separator = ' '
|
|
|
|
if (!EmojiDecoder.canStartHashtag(this, this.length)) append(separator)
|
|
|
|
this.append('#').append(tagWithoutSharp)
|
|
|
|
append(separator)
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
fun openFeaturedTagList(list: List<TootTag>?) {
|
|
|
|
val ad = ActionsDialog()
|
|
|
|
list?.forEach { tag ->
|
|
|
|
ad.addAction("#${tag.name}") {
|
|
|
|
val et = this.et ?: return@addAction
|
|
|
|
|
|
|
|
val src = et.text ?: ""
|
|
|
|
val srcLength = src.length
|
|
|
|
val start = min(srcLength, et.selectionStart)
|
|
|
|
val end = min(srcLength, et.selectionEnd)
|
|
|
|
|
|
|
|
val sb = SpannableStringBuilder()
|
|
|
|
.append(src.subSequence(0, start))
|
|
|
|
.appendHashTag(tag.name)
|
|
|
|
val newSelection = sb.length
|
|
|
|
if (end < srcLength) sb.append(src.subSequence(end, srcLength))
|
|
|
|
|
|
|
|
et.text = sb
|
|
|
|
et.setSelection(newSelection)
|
|
|
|
|
|
|
|
procTextChanged.run()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ad.addAction(activity.getString(R.string.input_sharp_itself)) {
|
|
|
|
val et = this.et ?: return@addAction
|
|
|
|
|
|
|
|
val src = et.text ?: ""
|
|
|
|
val srcLength = src.length
|
|
|
|
val start = min(srcLength, et.selectionStart)
|
|
|
|
val end = min(srcLength, et.selectionEnd)
|
|
|
|
|
|
|
|
val sb = SpannableStringBuilder()
|
|
|
|
sb.append(src.subSequence(0, start))
|
|
|
|
if (!EmojiDecoder.canStartHashtag(sb, sb.length)) sb.append(' ')
|
|
|
|
sb.append('#')
|
|
|
|
|
|
|
|
val newSelection = sb.length
|
|
|
|
if (end < srcLength) sb.append(src.subSequence(end, srcLength))
|
|
|
|
et.text = sb
|
|
|
|
et.setSelection(newSelection)
|
|
|
|
|
|
|
|
procTextChanged.run()
|
|
|
|
}
|
|
|
|
ad.show(activity, activity.getString(R.string.featured_hashtags))
|
|
|
|
}
|
|
|
|
|
|
|
|
// final ActionMode.Callback action_mode_callback = new ActionMode.Callback() {
|
|
|
|
// @Override public boolean onCreateActionMode( ActionMode actionMode, Menu menu ){
|
|
|
|
// actionMode.getMenuInflater().inflate(R.menu.toot_long_tap, menu);
|
|
|
|
// return true;
|
|
|
|
// }
|
|
|
|
// @Override public void onDestroyActionMode( ActionMode actionMode ){
|
|
|
|
//
|
|
|
|
// }
|
|
|
|
// @Override public boolean onPrepareActionMode( ActionMode actionMode, Menu menu ){
|
|
|
|
// return false;
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// @Override
|
|
|
|
// public boolean onActionItemClicked( ActionMode actionMode, MenuItem item ){
|
|
|
|
// if (item.getItemId() == R.id.action_pick_emoji) {
|
|
|
|
// actionMode.finish();
|
|
|
|
// EmojiPicker.open( activity, instance, new EmojiPicker.Callback() {
|
|
|
|
// @Override public void onPickedEmoji( String name ){
|
|
|
|
// int end = et.getSelectionEnd();
|
|
|
|
// String src = et.getText().toString();
|
|
|
|
// CharSequence svInsert = ":" + name + ":";
|
|
|
|
// src = src.substring( 0, end ) + svInsert + " " + ( end >= src.length() ? "" : src.substring( end ) );
|
|
|
|
// et.setText( src );
|
|
|
|
// et.setSelection( end + svInsert.length() + 1 );
|
|
|
|
//
|
|
|
|
// proc_text_changed.run();
|
|
|
|
// }
|
|
|
|
// } );
|
|
|
|
// return true;
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// return false;
|
|
|
|
// }
|
|
|
|
// };
|
|
|
|
}
|