238 lines
8.1 KiB
Kotlin
238 lines
8.1 KiB
Kotlin
package jp.juggler.subwaytooter.util
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.app.Activity
|
|
import android.os.Handler
|
|
import android.text.Spannable
|
|
import android.text.SpannableStringBuilder
|
|
import android.view.Gravity
|
|
import android.view.View
|
|
import android.widget.CheckedTextView
|
|
import android.widget.EditText
|
|
import android.widget.LinearLayout
|
|
import android.widget.PopupWindow
|
|
import androidx.core.content.ContextCompat
|
|
import jp.juggler.subwaytooter.App1
|
|
import jp.juggler.subwaytooter.R
|
|
import jp.juggler.subwaytooter.api.entity.Acct
|
|
import jp.juggler.subwaytooter.view.MyEditText
|
|
import jp.juggler.util.*
|
|
import jp.juggler.util.data.*
|
|
import jp.juggler.util.log.*
|
|
import jp.juggler.util.ui.*
|
|
import java.util.*
|
|
|
|
@SuppressLint("InflateParams")
|
|
internal class PopupAutoCompleteAcct(
|
|
val activity: Activity,
|
|
private val etContent: EditText,
|
|
private val formRoot: View,
|
|
private val bMainScreen: Boolean,
|
|
) {
|
|
companion object {
|
|
internal val log = LogCategory("PopupAutoCompleteAcct")
|
|
|
|
// 絵文字ショートコードにマッチするとても雑な正規表現
|
|
private val reLastShortCode = """:([^\s:]+):\z""".asciiPattern()
|
|
}
|
|
|
|
private val acctPopup: PopupWindow
|
|
private val llItems: LinearLayout
|
|
val density: Float
|
|
private val popupWidth: Int
|
|
val handler: Handler
|
|
|
|
private var popupRows: Int = 0
|
|
|
|
val isShowing: Boolean
|
|
get() = acctPopup.isShowing
|
|
|
|
fun dismiss() {
|
|
try {
|
|
acctPopup.dismiss()
|
|
} catch (ex: Throwable) {
|
|
log.e(ex, "acctPopup.dismiss failed.")
|
|
}
|
|
}
|
|
|
|
init {
|
|
this.density = activity.resources.displayMetrics.density
|
|
this.handler = App1.getAppState(activity, "PopupAutoCompleteAcct.ctor").handler
|
|
|
|
popupWidth = (0.5f + 240f * density).toInt()
|
|
|
|
val viewRoot = activity.layoutInflater.inflate(R.layout.acct_complete_popup, null, false)
|
|
llItems = viewRoot.findViewById(R.id.llItems)
|
|
//
|
|
acctPopup = PopupWindow(activity)
|
|
acctPopup.setBackgroundDrawable(
|
|
ContextCompat.getDrawable(
|
|
activity,
|
|
R.drawable.acct_popup_bg
|
|
)
|
|
)
|
|
acctPopup.contentView = viewRoot
|
|
acctPopup.isTouchable = true
|
|
}
|
|
|
|
fun setList(
|
|
et: MyEditText,
|
|
selStart: Int,
|
|
selEnd: Int,
|
|
acctList: List<CharSequence>?,
|
|
pickerCaption: String?,
|
|
pickerCallback: Runnable?,
|
|
) {
|
|
|
|
llItems.removeAllViews()
|
|
|
|
popupRows = 0
|
|
|
|
run {
|
|
val v = activity.layoutInflater
|
|
.inflate(R.layout.lv_spinner_dropdown, llItems, false) as CheckedTextView
|
|
v.setTextColor(activity.attrColor(android.R.attr.textColorPrimary))
|
|
v.setText(R.string.close)
|
|
v.setOnClickListener { acctPopup.dismiss() }
|
|
llItems.addView(v)
|
|
++popupRows
|
|
}
|
|
|
|
if (pickerCaption != null && pickerCallback != null) {
|
|
val v = activity.layoutInflater
|
|
.inflate(R.layout.lv_spinner_dropdown, llItems, false) as CheckedTextView
|
|
v.setTextColor(activity.attrColor(android.R.attr.textColorPrimary))
|
|
v.text = pickerCaption
|
|
v.setOnClickListener {
|
|
acctPopup.dismiss()
|
|
pickerCallback.run()
|
|
}
|
|
llItems.addView(v)
|
|
++popupRows
|
|
}
|
|
|
|
acctList?.forEach { acct ->
|
|
val v = activity.layoutInflater
|
|
.inflate(R.layout.lv_spinner_dropdown, llItems, false) as CheckedTextView
|
|
v.setTextColor(activity.attrColor(android.R.attr.textColorPrimary))
|
|
v.text = acct
|
|
if (acct is Spannable) {
|
|
NetworkEmojiInvalidator(handler, v).text = acct
|
|
}
|
|
v.setOnClickListener { handleItemClick(et, selStart, selEnd, acct) }
|
|
llItems.addView(v)
|
|
++popupRows
|
|
}
|
|
|
|
updatePosition()
|
|
}
|
|
|
|
private fun handleItemClick(et: EditText, selStart: Int, selEnd: Int, acct: CharSequence) {
|
|
val src = et.text ?: ""
|
|
val srcLength = src.length
|
|
val start = selStart.clip(0, srcLength)
|
|
val end = selEnd.clip(0, srcLength)
|
|
|
|
val sb = SpannableStringBuilder()
|
|
sb.append(src.subSequence(0, start))
|
|
val remain = src.subSequence(end, srcLength)
|
|
|
|
when (acct[0]) {
|
|
'#' -> {
|
|
// #hashtag
|
|
// 直後に空白を付与する
|
|
sb.append(acct).append(" ")
|
|
}
|
|
'@' -> if (acct.any { it >= 0x80.toChar() }) {
|
|
// @user@host IDNドメインを含む
|
|
// 直後に空白を付与する
|
|
sb.append("@" + Acct.parse(acct.toString().substring(1)).ascii).append(" ")
|
|
} else {
|
|
// @user@host
|
|
// 直後に空白を付与する
|
|
sb.append(acct).append(" ")
|
|
}
|
|
' ' -> {
|
|
// 絵文字ショートコード(カスタム絵文字 or twemoji)
|
|
val separator = EmojiDecoder.customEmojiSeparator()
|
|
if (!EmojiDecoder.canStartShortCode(sb, start)) sb.append(separator)
|
|
sb.append(findShortCode(acct.toString()))
|
|
// セパレータにZWSPを使う設定なら、補完した次の位置にもZWSPを追加する。連続して入力補完できるようになる。
|
|
if (separator != ' ') sb.append(separator)
|
|
}
|
|
else -> {
|
|
// 絵文字(noto unicode emoji)
|
|
val delm = acct.indexOf(' ')
|
|
if (delm != -1) {
|
|
sb.append(acct.subSequence(0, delm))
|
|
}
|
|
}
|
|
}
|
|
val newSelection = sb.length
|
|
sb.append(remain)
|
|
|
|
et.text = sb
|
|
et.setSelection(newSelection)
|
|
acctPopup.dismiss()
|
|
}
|
|
|
|
private fun findShortCode(acct: String): String {
|
|
val m = reLastShortCode.matcher(acct)
|
|
if (m.find()) return m.groupEx(0)!!
|
|
return acct
|
|
}
|
|
|
|
fun updatePosition() {
|
|
|
|
val location = IntArray(2)
|
|
etContent.getLocationInWindow(location)
|
|
val textTop = location[1]
|
|
|
|
var popupTop: Int
|
|
var popupHeight: Int
|
|
|
|
if (bMainScreen) {
|
|
val popupBottom = textTop + etContent.totalPaddingTop - etContent.scrollY
|
|
val max = popupBottom - (0.5f + 48f * 1f * density).toInt()
|
|
val min = (0.5f + 48f * 2f * density).toInt()
|
|
popupHeight = (0.5f + 48f * popupRows.toFloat() * density).toInt()
|
|
if (popupHeight < min) popupHeight = min
|
|
if (popupHeight > max) popupHeight = max
|
|
popupTop = popupBottom - popupHeight
|
|
} else {
|
|
formRoot.getLocationInWindow(location)
|
|
val formTop = location[1]
|
|
val formBottom = location[1] + formRoot.height
|
|
|
|
val layout = etContent.layout
|
|
|
|
popupTop = try {
|
|
(textTop + etContent.totalPaddingTop + layout.getLineBottom(layout.lineCount - 1)) - etContent.scrollY
|
|
} catch (ignored: Throwable) {
|
|
// java.lang.IllegalStateException
|
|
0
|
|
}
|
|
|
|
if (popupTop < formTop) popupTop = formTop
|
|
|
|
popupHeight = formBottom - popupTop
|
|
|
|
val min = (0.5f + 48f * 2f * density).toInt()
|
|
val max = (0.5f + 48f * popupRows.toFloat() * density).toInt()
|
|
|
|
if (popupHeight < min) popupHeight = min
|
|
if (popupHeight > max) popupHeight = max
|
|
}
|
|
|
|
if (acctPopup.isShowing) {
|
|
acctPopup.update(0, popupTop, popupWidth, popupHeight)
|
|
} else {
|
|
acctPopup.width = popupWidth
|
|
acctPopup.height = popupHeight
|
|
acctPopup.showAtLocation(
|
|
etContent, Gravity.CENTER_HORIZONTAL or Gravity.TOP, 0, popupTop
|
|
)
|
|
}
|
|
}
|
|
}
|