feat: more options for virtual buttons
This commit is contained in:
185
app/src/main/java/com/termux/terminal/KeyHandler.kt
Normal file
185
app/src/main/java/com/termux/terminal/KeyHandler.kt
Normal file
@@ -0,0 +1,185 @@
|
||||
package com.termux.terminal
|
||||
|
||||
import android.view.KeyEvent.*
|
||||
|
||||
object KeyHandler {
|
||||
|
||||
const val KEYMOD_ALT = 0x80000000.toInt()
|
||||
const val KEYMOD_CTRL = 0x40000000
|
||||
const val KEYMOD_SHIFT = 0x20000000
|
||||
const val KEYMOD_NUM_LOCK = 0x10000000
|
||||
|
||||
private val TERMCAP_TO_KEYCODE = HashMap<String, Int>().apply {
|
||||
// terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
|
||||
// termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
|
||||
put("%i", KEYMOD_SHIFT or KEYCODE_DPAD_RIGHT)
|
||||
put("#2", KEYMOD_SHIFT or KEYCODE_MOVE_HOME) // Shifted home
|
||||
put("#4", KEYMOD_SHIFT or KEYCODE_DPAD_LEFT)
|
||||
put("*7", KEYMOD_SHIFT or KEYCODE_MOVE_END) // Shifted end key
|
||||
|
||||
put("k1", KEYCODE_F1)
|
||||
put("k2", KEYCODE_F2)
|
||||
put("k3", KEYCODE_F3)
|
||||
put("k4", KEYCODE_F4)
|
||||
put("k5", KEYCODE_F5)
|
||||
put("k6", KEYCODE_F6)
|
||||
put("k7", KEYCODE_F7)
|
||||
put("k8", KEYCODE_F8)
|
||||
put("k9", KEYCODE_F9)
|
||||
put("k;", KEYCODE_F10)
|
||||
put("F1", KEYCODE_F11)
|
||||
put("F2", KEYCODE_F12)
|
||||
put("F3", KEYMOD_SHIFT or KEYCODE_F1)
|
||||
put("F4", KEYMOD_SHIFT or KEYCODE_F2)
|
||||
put("F5", KEYMOD_SHIFT or KEYCODE_F3)
|
||||
put("F6", KEYMOD_SHIFT or KEYCODE_F4)
|
||||
put("F7", KEYMOD_SHIFT or KEYCODE_F5)
|
||||
put("F8", KEYMOD_SHIFT or KEYCODE_F6)
|
||||
put("F9", KEYMOD_SHIFT or KEYCODE_F7)
|
||||
put("FA", KEYMOD_SHIFT or KEYCODE_F8)
|
||||
put("FB", KEYMOD_SHIFT or KEYCODE_F9)
|
||||
put("FC", KEYMOD_SHIFT or KEYCODE_F10)
|
||||
put("FD", KEYMOD_SHIFT or KEYCODE_F11)
|
||||
put("FE", KEYMOD_SHIFT or KEYCODE_F12)
|
||||
|
||||
put("kb", KEYCODE_DEL) // backspace key
|
||||
|
||||
put("kd", KEYCODE_DPAD_DOWN) // terminfo=kcud1, down-arrow key
|
||||
put("kh", KEYCODE_MOVE_HOME)
|
||||
put("kl", KEYCODE_DPAD_LEFT)
|
||||
put("kr", KEYCODE_DPAD_RIGHT)
|
||||
|
||||
// K1=Upper left of keypad:
|
||||
// t_K1 <kHome> keypad home key
|
||||
// t_K3 <kPageUp> keypad page-up key
|
||||
// t_K4 <kEnd> keypad end key
|
||||
// t_K5 <kPageDown> keypad page-down key
|
||||
put("K1", KEYCODE_MOVE_HOME)
|
||||
put("K3", KEYCODE_PAGE_UP)
|
||||
put("K4", KEYCODE_MOVE_END)
|
||||
put("K5", KEYCODE_PAGE_DOWN)
|
||||
|
||||
put("ku", KEYCODE_DPAD_UP)
|
||||
|
||||
put("kB", KEYMOD_SHIFT or KEYCODE_TAB) // termcap=kB, terminfo=kcbt: Back-tab
|
||||
put("kD", KEYCODE_FORWARD_DEL) // terminfo=kdch1, delete-character key
|
||||
put("kDN", KEYMOD_SHIFT or KEYCODE_DPAD_DOWN) // non-standard shifted arrow down
|
||||
put("kF", KEYMOD_SHIFT or KEYCODE_DPAD_DOWN) // terminfo=kind, scroll-forward key
|
||||
put("kI", KEYCODE_INSERT)
|
||||
put("kN", KEYCODE_PAGE_UP)
|
||||
put("kP", KEYCODE_PAGE_DOWN)
|
||||
put("kR", KEYMOD_SHIFT or KEYCODE_DPAD_UP) // terminfo=kri, scroll-backward key
|
||||
put("kUP", KEYMOD_SHIFT or KEYCODE_DPAD_UP) // non-standard shifted up
|
||||
|
||||
put("@7", KEYCODE_MOVE_END)
|
||||
put("@8", KEYCODE_NUMPAD_ENTER)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getCodeFromTermcap(termcap: String, cursorKeysApplication: Boolean, keypadApplication: Boolean): String? {
|
||||
val keyCodeAndMod = TERMCAP_TO_KEYCODE[termcap] ?: return null
|
||||
var keyCode = keyCodeAndMod
|
||||
var keyMod = 0
|
||||
if ((keyCode and KEYMOD_SHIFT) != 0) {
|
||||
keyMod = keyMod or KEYMOD_SHIFT
|
||||
keyCode = keyCode and KEYMOD_SHIFT.inv()
|
||||
}
|
||||
if ((keyCode and KEYMOD_CTRL) != 0) {
|
||||
keyMod = keyMod or KEYMOD_CTRL
|
||||
keyCode = keyCode and KEYMOD_CTRL.inv()
|
||||
}
|
||||
if ((keyCode and KEYMOD_ALT) != 0) {
|
||||
keyMod = keyMod or KEYMOD_ALT
|
||||
keyCode = keyCode and KEYMOD_ALT.inv()
|
||||
}
|
||||
if ((keyCode and KEYMOD_NUM_LOCK) != 0) {
|
||||
keyMod = keyMod or KEYMOD_NUM_LOCK
|
||||
keyCode = keyCode and KEYMOD_NUM_LOCK.inv()
|
||||
}
|
||||
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getCode(keyCode: Int, keyMode: Int, cursorApp: Boolean, keypadApplication: Boolean): String? {
|
||||
val numLockOn = (keyMode and KEYMOD_NUM_LOCK) != 0
|
||||
val keyMod = keyMode and KEYMOD_NUM_LOCK.inv()
|
||||
return when (keyCode) {
|
||||
KEYCODE_DPAD_CENTER -> "\r"
|
||||
|
||||
KEYCODE_DPAD_UP -> if (keyMod == 0) (if (cursorApp) "\u001bOA" else "\u001b[A") else transformForModifiers("\u001b[1", keyMod, 'A')
|
||||
KEYCODE_DPAD_DOWN -> if (keyMod == 0) (if (cursorApp) "\u001bOB" else "\u001b[B") else transformForModifiers("\u001b[1", keyMod, 'B')
|
||||
KEYCODE_DPAD_RIGHT -> if (keyMod == 0) (if (cursorApp) "\u001bOC" else "\u001b[C") else transformForModifiers("\u001b[1", keyMod, 'C')
|
||||
KEYCODE_DPAD_LEFT -> if (keyMod == 0) (if (cursorApp) "\u001bOD" else "\u001b[D") else transformForModifiers("\u001b[1", keyMod, 'D')
|
||||
|
||||
KEYCODE_MOVE_HOME -> if (keyMod == 0) (if (cursorApp) "\u001bOH" else "\u001b[H") else transformForModifiers("\u001b[1", keyMod, 'H')
|
||||
KEYCODE_MOVE_END -> if (keyMod == 0) (if (cursorApp) "\u001bOF" else "\u001b[F") else transformForModifiers("\u001b[1", keyMod, 'F')
|
||||
|
||||
KEYCODE_F1 -> if (keyMod == 0) "\u001bOP" else transformForModifiers("\u001b[1", keyMod, 'P')
|
||||
KEYCODE_F2 -> if (keyMod == 0) "\u001bOQ" else transformForModifiers("\u001b[1", keyMod, 'Q')
|
||||
KEYCODE_F3 -> if (keyMod == 0) "\u001bOR" else transformForModifiers("\u001b[1", keyMod, 'R')
|
||||
KEYCODE_F4 -> if (keyMod == 0) "\u001bOS" else transformForModifiers("\u001b[1", keyMod, 'S')
|
||||
KEYCODE_F5 -> transformForModifiers("\u001b[15", keyMod, '~')
|
||||
KEYCODE_F6 -> transformForModifiers("\u001b[17", keyMod, '~')
|
||||
KEYCODE_F7 -> transformForModifiers("\u001b[18", keyMod, '~')
|
||||
KEYCODE_F8 -> transformForModifiers("\u001b[19", keyMod, '~')
|
||||
KEYCODE_F9 -> transformForModifiers("\u001b[20", keyMod, '~')
|
||||
KEYCODE_F10 -> transformForModifiers("\u001b[21", keyMod, '~')
|
||||
KEYCODE_F11 -> transformForModifiers("\u001b[23", keyMod, '~')
|
||||
KEYCODE_F12 -> transformForModifiers("\u001b[24", keyMod, '~')
|
||||
|
||||
KEYCODE_SYSRQ -> "\u001b[32~" // Sys Request / Print
|
||||
KEYCODE_BREAK -> "\u001b[34~" // Pause/Break
|
||||
|
||||
KEYCODE_ESCAPE, KEYCODE_BACK -> "\u001b"
|
||||
|
||||
KEYCODE_INSERT -> transformForModifiers("\u001b[2", keyMod, '~')
|
||||
KEYCODE_FORWARD_DEL -> transformForModifiers("\u001b[3", keyMod, '~')
|
||||
|
||||
KEYCODE_PAGE_UP -> transformForModifiers("\u001b[5", keyMod, '~')
|
||||
KEYCODE_PAGE_DOWN -> transformForModifiers("\u001b[6", keyMod, '~')
|
||||
KEYCODE_DEL -> {
|
||||
val prefix = if ((keyMod and KEYMOD_ALT) == 0) "" else "\u001b"
|
||||
prefix + if ((keyMod and KEYMOD_CTRL) == 0) "\u007F" else "\u0008"
|
||||
}
|
||||
KEYCODE_NUM_LOCK -> if (keypadApplication) "\u001bOP" else null
|
||||
KEYCODE_SPACE -> if ((keyMod and KEYMOD_CTRL) == 0) null else "\u0000"
|
||||
KEYCODE_TAB -> if ((keyMod and KEYMOD_SHIFT) == 0) "\t" else "\u001b[Z"
|
||||
KEYCODE_ENTER -> if ((keyMod and KEYMOD_ALT) == 0) "\r" else "\u001b\r"
|
||||
|
||||
KEYCODE_NUMPAD_ENTER -> if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'M') else "\n"
|
||||
KEYCODE_NUMPAD_MULTIPLY -> if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'j') else "*"
|
||||
KEYCODE_NUMPAD_ADD -> if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'k') else "+"
|
||||
KEYCODE_NUMPAD_COMMA -> ","
|
||||
KEYCODE_NUMPAD_DOT -> if (numLockOn) (if (keypadApplication) "\u001bOn" else ".") else transformForModifiers("\u001b[3", keyMod, '~')
|
||||
KEYCODE_NUMPAD_SUBTRACT -> if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'm') else "-"
|
||||
KEYCODE_NUMPAD_DIVIDE -> if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'o') else "/"
|
||||
KEYCODE_NUMPAD_0 -> if (numLockOn) (if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'p') else "0") else transformForModifiers("\u001b[2", keyMod, '~')
|
||||
KEYCODE_NUMPAD_1 -> if (numLockOn) (if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'q') else "1") else (if (keyMod == 0) (if (cursorApp) "\u001bOF" else "\u001b[F") else transformForModifiers("\u001b[1", keyMod, 'F'))
|
||||
KEYCODE_NUMPAD_2 -> if (numLockOn) (if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'r') else "2") else (if (keyMod == 0) (if (cursorApp) "\u001bOB" else "\u001b[B") else transformForModifiers("\u001b[1", keyMod, 'B'))
|
||||
KEYCODE_NUMPAD_3 -> if (numLockOn) (if (keypadApplication) transformForModifiers("\u001bO", keyMod, 's') else "3") else "\u001b[6~"
|
||||
KEYCODE_NUMPAD_4 -> if (numLockOn) (if (keypadApplication) transformForModifiers("\u001bO", keyMod, 't') else "4") else (if (keyMod == 0) (if (cursorApp) "\u001bOD" else "\u001b[D") else transformForModifiers("\u001b[1", keyMod, 'D'))
|
||||
KEYCODE_NUMPAD_5 -> if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'u') else "5"
|
||||
KEYCODE_NUMPAD_6 -> if (numLockOn) (if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'v') else "6") else (if (keyMod == 0) (if (cursorApp) "\u001bOC" else "\u001b[C") else transformForModifiers("\u001b[1", keyMod, 'C'))
|
||||
KEYCODE_NUMPAD_7 -> if (numLockOn) (if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'w') else "7") else (if (keyMod == 0) (if (cursorApp) "\u001bOH" else "\u001b[H") else transformForModifiers("\u001b[1", keyMod, 'H'))
|
||||
KEYCODE_NUMPAD_8 -> if (numLockOn) (if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'x') else "8") else (if (keyMod == 0) (if (cursorApp) "\u001bOA" else "\u001b[A") else transformForModifiers("\u001b[1", keyMod, 'A'))
|
||||
KEYCODE_NUMPAD_9 -> if (numLockOn) (if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'y') else "9") else "\u001b[5~"
|
||||
KEYCODE_NUMPAD_EQUALS -> if (keypadApplication) transformForModifiers("\u001bO", keyMod, 'X') else "="
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun transformForModifiers(start: String, keymod: Int, lastChar: Char): String {
|
||||
val modifier = when (keymod) {
|
||||
KEYMOD_SHIFT -> 2
|
||||
KEYMOD_ALT -> 3
|
||||
KEYMOD_SHIFT or KEYMOD_ALT -> 4
|
||||
KEYMOD_CTRL -> 5
|
||||
KEYMOD_SHIFT or KEYMOD_CTRL -> 6
|
||||
KEYMOD_ALT or KEYMOD_CTRL -> 7
|
||||
KEYMOD_SHIFT or KEYMOD_ALT or KEYMOD_CTRL -> 8
|
||||
else -> return start + lastChar
|
||||
}
|
||||
return "$start;$modifier$lastChar"
|
||||
}
|
||||
}
|
||||
86
app/src/main/java/com/termux/terminal/Logger.kt
Normal file
86
app/src/main/java/com/termux/terminal/Logger.kt
Normal file
@@ -0,0 +1,86 @@
|
||||
package com.termux.terminal
|
||||
|
||||
import android.util.Log
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
object Logger {
|
||||
|
||||
@JvmStatic
|
||||
fun logError(client: TerminalSessionClient?, logTag: String, message: String) {
|
||||
if (client != null) {
|
||||
client.logError(logTag, message)
|
||||
} else {
|
||||
Log.e(logTag, message)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun logWarn(client: TerminalSessionClient?, logTag: String, message: String) {
|
||||
if (client != null) {
|
||||
client.logWarn(logTag, message)
|
||||
} else {
|
||||
Log.w(logTag, message)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun logInfo(client: TerminalSessionClient?, logTag: String, message: String) {
|
||||
if (client != null) {
|
||||
client.logInfo(logTag, message)
|
||||
} else {
|
||||
Log.i(logTag, message)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun logDebug(client: TerminalSessionClient?, logTag: String, message: String) {
|
||||
if (client != null) {
|
||||
client.logDebug(logTag, message)
|
||||
} else {
|
||||
Log.d(logTag, message)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun logVerbose(client: TerminalSessionClient?, logTag: String, message: String) {
|
||||
if (client != null) {
|
||||
client.logVerbose(logTag, message)
|
||||
} else {
|
||||
Log.v(logTag, message)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun logStackTraceWithMessage(client: TerminalSessionClient?, tag: String, message: String?, throwable: Throwable?) {
|
||||
logError(client, tag, getMessageAndStackTraceString(message, throwable) ?: "")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getMessageAndStackTraceString(message: String?, throwable: Throwable?): String? {
|
||||
return when {
|
||||
message == null && throwable == null -> null
|
||||
message != null && throwable != null -> "$message:\n${getStackTraceString(throwable)}"
|
||||
throwable == null -> message
|
||||
else -> getStackTraceString(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getStackTraceString(throwable: Throwable?): String? {
|
||||
if (throwable == null) return null
|
||||
|
||||
return try {
|
||||
val errors = StringWriter()
|
||||
val pw = PrintWriter(errors)
|
||||
throwable.printStackTrace(pw)
|
||||
pw.close()
|
||||
val result = errors.toString()
|
||||
errors.close()
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
534
app/src/main/java/com/termux/terminal/TerminalBuffer.kt
Normal file
534
app/src/main/java/com/termux/terminal/TerminalBuffer.kt
Normal file
@@ -0,0 +1,534 @@
|
||||
package com.termux.terminal
|
||||
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* A circular buffer of [TerminalRow]s which keeps notes about what is visible on a logical screen and the scroll
|
||||
* history.
|
||||
*
|
||||
* See [externalToInternalRow] for how to map from logical screen rows to array indices.
|
||||
*/
|
||||
class TerminalBuffer(
|
||||
columns: Int,
|
||||
totalRows: Int,
|
||||
screenRows: Int
|
||||
) {
|
||||
@JvmField
|
||||
var mLines: Array<TerminalRow?>
|
||||
|
||||
/** The length of [mLines]. */
|
||||
@JvmField
|
||||
var mTotalRows: Int
|
||||
|
||||
/** The number of rows and columns visible on the screen. */
|
||||
@JvmField
|
||||
var mScreenRows: Int
|
||||
|
||||
@JvmField
|
||||
var mColumns: Int
|
||||
|
||||
/** The number of rows kept in history. */
|
||||
private var mActiveTranscriptRows = 0
|
||||
|
||||
/** The index in the circular buffer where the visible screen starts. */
|
||||
private var mScreenFirstRow = 0
|
||||
|
||||
init {
|
||||
mColumns = columns
|
||||
mTotalRows = totalRows
|
||||
mScreenRows = screenRows
|
||||
mLines = arrayOfNulls(totalRows)
|
||||
blockSet(0, 0, columns, screenRows, ' '.code, TextStyle.NORMAL)
|
||||
}
|
||||
|
||||
fun getTranscriptText(): String {
|
||||
return getSelectedText(0, -activeTranscriptRows, mColumns, mScreenRows).trim()
|
||||
}
|
||||
|
||||
fun getTranscriptTextWithoutJoinedLines(): String {
|
||||
return getSelectedText(0, -activeTranscriptRows, mColumns, mScreenRows, false).trim()
|
||||
}
|
||||
|
||||
fun getTranscriptTextWithFullLinesJoined(): String {
|
||||
return getSelectedText(0, -activeTranscriptRows, mColumns, mScreenRows, joinBackLines = true, joinFullLines = true).trim()
|
||||
}
|
||||
|
||||
fun getSelectedText(selX1: Int, selY1: Int, selX2: Int, selY2: Int): String {
|
||||
return getSelectedText(selX1, selY1, selX2, selY2, joinBackLines = true)
|
||||
}
|
||||
|
||||
fun getSelectedText(selX1: Int, selY1: Int, selX2: Int, selY2: Int, joinBackLines: Boolean): String {
|
||||
return getSelectedText(selX1, selY1, selX2, selY2, joinBackLines, joinFullLines = false)
|
||||
}
|
||||
|
||||
fun getSelectedText(
|
||||
selX1: Int,
|
||||
selY1: Int,
|
||||
selX2: Int,
|
||||
selY2: Int,
|
||||
joinBackLines: Boolean,
|
||||
joinFullLines: Boolean
|
||||
): String {
|
||||
val builder = StringBuilder()
|
||||
val columns = mColumns
|
||||
|
||||
var y1 = selY1
|
||||
var y2 = selY2
|
||||
if (y1 < -activeTranscriptRows) y1 = -activeTranscriptRows
|
||||
if (y2 >= mScreenRows) y2 = mScreenRows - 1
|
||||
|
||||
for (row in y1..y2) {
|
||||
val x1 = if (row == y1) selX1 else 0
|
||||
var x2: Int
|
||||
if (row == y2) {
|
||||
x2 = selX2 + 1
|
||||
if (x2 > columns) x2 = columns
|
||||
} else {
|
||||
x2 = columns
|
||||
}
|
||||
val lineObject = mLines[externalToInternalRow(row)]!!
|
||||
val x1Index = lineObject.findStartOfColumn(x1)
|
||||
val x2Index = if (x2 < mColumns) lineObject.findStartOfColumn(x2) else lineObject.spaceUsed
|
||||
val finalX2Index = if (x2Index == x1Index) {
|
||||
// Selected the start of a wide character.
|
||||
lineObject.findStartOfColumn(x2 + 1)
|
||||
} else {
|
||||
x2Index
|
||||
}
|
||||
val line = lineObject.mText
|
||||
var lastPrintingCharIndex = -1
|
||||
val rowLineWrap = getLineWrap(row)
|
||||
if (rowLineWrap && x2 == columns) {
|
||||
// If the line was wrapped, we shouldn't lose trailing space:
|
||||
lastPrintingCharIndex = finalX2Index - 1
|
||||
} else {
|
||||
for (i in x1Index until finalX2Index) {
|
||||
val c = line[i]
|
||||
if (c != ' ') lastPrintingCharIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
val len = lastPrintingCharIndex - x1Index + 1
|
||||
if (lastPrintingCharIndex != -1 && len > 0)
|
||||
builder.append(line, x1Index, len)
|
||||
|
||||
val lineFillsWidth = lastPrintingCharIndex == finalX2Index - 1
|
||||
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
|
||||
&& row < y2 && row < mScreenRows - 1
|
||||
) builder.append('\n')
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun getWordAtLocation(x: Int, y: Int): String {
|
||||
// Set y1 and y2 to the lines where the wrapped line starts and ends.
|
||||
// I.e. if a line that is wrapped to 3 lines starts at line 4, and this
|
||||
// is called with y=5, then y1 would be set to 4 and y2 would be set to 6.
|
||||
var y1 = y
|
||||
var y2 = y
|
||||
while (y1 > 0 && !getSelectedText(0, y1 - 1, mColumns, y, joinBackLines = true, joinFullLines = true).contains("\n")) {
|
||||
y1--
|
||||
}
|
||||
while (y2 < mScreenRows && !getSelectedText(0, y, mColumns, y2 + 1, joinBackLines = true, joinFullLines = true).contains("\n")) {
|
||||
y2++
|
||||
}
|
||||
|
||||
// Get the text for the whole wrapped line
|
||||
val text = getSelectedText(0, y1, mColumns, y2, joinBackLines = true, joinFullLines = true)
|
||||
// The index of x in text
|
||||
val textOffset = (y - y1) * mColumns + x
|
||||
|
||||
if (textOffset >= text.length) {
|
||||
// The click was to the right of the last word on the line, so
|
||||
// there's no word to return
|
||||
return ""
|
||||
}
|
||||
|
||||
// Set x1 and x2 to the indices of the last space before x and the
|
||||
// first space after x in text respectively
|
||||
val x1 = text.lastIndexOf(' ', textOffset)
|
||||
var x2 = text.indexOf(' ', textOffset)
|
||||
if (x2 == -1) {
|
||||
x2 = text.length
|
||||
}
|
||||
|
||||
if (x1 == x2) {
|
||||
// The click was on a space, so there's no word to return
|
||||
return ""
|
||||
}
|
||||
return text.substring(x1 + 1, x2)
|
||||
}
|
||||
|
||||
val activeTranscriptRows: Int
|
||||
get() = mActiveTranscriptRows
|
||||
|
||||
val activeRows: Int
|
||||
get() = mActiveTranscriptRows + mScreenRows
|
||||
|
||||
/**
|
||||
* Convert a row value from the public external coordinate system to our internal private coordinate system.
|
||||
*
|
||||
* ```
|
||||
* - External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
|
||||
* - Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
|
||||
* mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
|
||||
*
|
||||
* External ↔ Internal:
|
||||
*
|
||||
* [ ... ] [ ... ]
|
||||
* [ -mActiveTranscriptRows ] [ mScreenFirstRow - mActiveTranscriptRows ]
|
||||
* [ ... ] [ ... ]
|
||||
* [ 0 (visible screen starts here) ] ↔ [ mScreenFirstRow ]
|
||||
* [ ... ] [ ... ]
|
||||
* [ mScreenRows-1 ] [ mScreenFirstRow + mScreenRows-1 ]
|
||||
* ```
|
||||
*
|
||||
* @param externalRow a row in the external coordinate system.
|
||||
* @return The row corresponding to the input argument in the private coordinate system.
|
||||
*/
|
||||
fun externalToInternalRow(externalRow: Int): Int {
|
||||
if (externalRow < -mActiveTranscriptRows || externalRow > mScreenRows)
|
||||
throw IllegalArgumentException("extRow=$externalRow, mScreenRows=$mScreenRows, mActiveTranscriptRows=$mActiveTranscriptRows")
|
||||
val internalRow = mScreenFirstRow + externalRow
|
||||
return if (internalRow < 0) (mTotalRows + internalRow) else (internalRow % mTotalRows)
|
||||
}
|
||||
|
||||
fun setLineWrap(row: Int) {
|
||||
mLines[externalToInternalRow(row)]!!.mLineWrap = true
|
||||
}
|
||||
|
||||
fun getLineWrap(row: Int): Boolean {
|
||||
return mLines[externalToInternalRow(row)]!!.mLineWrap
|
||||
}
|
||||
|
||||
fun clearLineWrap(row: Int) {
|
||||
mLines[externalToInternalRow(row)]!!.mLineWrap = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
|
||||
* change or the rows expand (that is, it only works when shrinking the number of rows).
|
||||
*
|
||||
* @param newColumns The number of columns the screen should have.
|
||||
* @param newRows The number of rows the screen should have.
|
||||
* @param cursor An int[2] containing the (column, row) cursor location.
|
||||
*/
|
||||
fun resize(
|
||||
newColumns: Int,
|
||||
newRows: Int,
|
||||
newTotalRows: Int,
|
||||
cursor: IntArray,
|
||||
currentStyle: Long,
|
||||
altScreen: Boolean
|
||||
) {
|
||||
// newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000):
|
||||
if (newColumns == mColumns && newRows <= mTotalRows) {
|
||||
// Fast resize where just the rows changed.
|
||||
var shiftDownOfTopRow = mScreenRows - newRows
|
||||
if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) {
|
||||
// Shrinking. Check if we can skip blank rows at bottom below cursor.
|
||||
for (i in mScreenRows - 1 downTo 1) {
|
||||
if (cursor[1] >= i) break
|
||||
val r = externalToInternalRow(i)
|
||||
if (mLines[r] == null || mLines[r]!!.isBlank()) {
|
||||
if (--shiftDownOfTopRow == 0) break
|
||||
}
|
||||
}
|
||||
} else if (shiftDownOfTopRow < 0) {
|
||||
// Negative shift down = expanding. Only move screen up if there is transcript to show:
|
||||
val actualShift = maxOf(shiftDownOfTopRow, -mActiveTranscriptRows)
|
||||
if (shiftDownOfTopRow != actualShift) {
|
||||
// The new lines revealed by the resizing are not all from the transcript. Blank the below ones.
|
||||
for (i in 0 until actualShift - shiftDownOfTopRow)
|
||||
allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle)
|
||||
shiftDownOfTopRow = actualShift
|
||||
}
|
||||
}
|
||||
mScreenFirstRow += shiftDownOfTopRow
|
||||
mScreenFirstRow = if (mScreenFirstRow < 0) (mScreenFirstRow + mTotalRows) else (mScreenFirstRow % mTotalRows)
|
||||
mTotalRows = newTotalRows
|
||||
mActiveTranscriptRows = if (altScreen) 0 else maxOf(0, mActiveTranscriptRows + shiftDownOfTopRow)
|
||||
cursor[1] -= shiftDownOfTopRow
|
||||
mScreenRows = newRows
|
||||
} else {
|
||||
// Copy away old state and update new:
|
||||
val oldLines = mLines
|
||||
mLines = arrayOfNulls(newTotalRows)
|
||||
for (i in 0 until newTotalRows)
|
||||
mLines[i] = TerminalRow(newColumns, currentStyle)
|
||||
|
||||
val oldActiveTranscriptRows = mActiveTranscriptRows
|
||||
val oldScreenFirstRow = mScreenFirstRow
|
||||
val oldScreenRows = mScreenRows
|
||||
val oldTotalRows = mTotalRows
|
||||
mTotalRows = newTotalRows
|
||||
mScreenRows = newRows
|
||||
mActiveTranscriptRows = 0
|
||||
mScreenFirstRow = 0
|
||||
mColumns = newColumns
|
||||
|
||||
var newCursorRow = -1
|
||||
var newCursorColumn = -1
|
||||
val oldCursorRow = cursor[1]
|
||||
val oldCursorColumn = cursor[0]
|
||||
var newCursorPlaced = false
|
||||
|
||||
var currentOutputExternalRow = 0
|
||||
var currentOutputExternalColumn = 0
|
||||
|
||||
// Loop over every character in the initial state.
|
||||
// Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we
|
||||
// keep track how many blank lines we have skipped if we later on find a non-blank line.
|
||||
var skippedBlankLines = 0
|
||||
for (externalOldRow in -oldActiveTranscriptRows until oldScreenRows) {
|
||||
// Do what externalToInternalRow() does but for the old state:
|
||||
var internalOldRow = oldScreenFirstRow + externalOldRow
|
||||
internalOldRow = if (internalOldRow < 0) (oldTotalRows + internalOldRow) else (internalOldRow % oldTotalRows)
|
||||
|
||||
val oldLine = oldLines[internalOldRow]
|
||||
val cursorAtThisRow = externalOldRow == oldCursorRow
|
||||
// The cursor may only be on a non-null line, which we should not skip:
|
||||
if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
|
||||
skippedBlankLines++
|
||||
continue
|
||||
} else if (skippedBlankLines > 0) {
|
||||
// After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines.
|
||||
for (i in 0 until skippedBlankLines) {
|
||||
if (currentOutputExternalRow == mScreenRows - 1) {
|
||||
scrollDownOneLine(0, mScreenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
skippedBlankLines = 0
|
||||
}
|
||||
|
||||
var lastNonSpaceIndex = 0
|
||||
var justToCursor = false
|
||||
if (cursorAtThisRow || oldLine.mLineWrap) {
|
||||
// Take the whole line, either because of cursor on it, or if line wrapping.
|
||||
lastNonSpaceIndex = oldLine.spaceUsed
|
||||
if (cursorAtThisRow) justToCursor = true
|
||||
} else {
|
||||
for (i in 0 until oldLine.spaceUsed) {
|
||||
// NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices
|
||||
if (oldLine.mText[i] != ' ' /* || oldLine.mStyle[i] != currentStyle */)
|
||||
lastNonSpaceIndex = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
var currentOldCol = 0
|
||||
var styleAtCol: Long = 0
|
||||
var i = 0
|
||||
while (i < lastNonSpaceIndex) {
|
||||
// Note that looping over java character, not cells.
|
||||
val c = oldLine.mText[i]
|
||||
val codePoint = if (Character.isHighSurrogate(c)) Character.toCodePoint(c, oldLine.mText[++i]) else c.code
|
||||
val displayWidth = WcWidth.width(codePoint)
|
||||
// Use the last style if this is a zero-width character:
|
||||
if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol)
|
||||
|
||||
// Line wrap as necessary:
|
||||
if (currentOutputExternalColumn + displayWidth > mColumns) {
|
||||
setLineWrap(currentOutputExternalRow)
|
||||
if (currentOutputExternalRow == mScreenRows - 1) {
|
||||
if (newCursorPlaced) newCursorRow--
|
||||
scrollDownOneLine(0, mScreenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
|
||||
val offsetDueToCombiningChar = if (displayWidth <= 0 && currentOutputExternalColumn > 0) 1 else 0
|
||||
val outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar
|
||||
setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol)
|
||||
|
||||
if (displayWidth > 0) {
|
||||
if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
|
||||
newCursorColumn = currentOutputExternalColumn
|
||||
newCursorRow = currentOutputExternalRow
|
||||
newCursorPlaced = true
|
||||
}
|
||||
currentOldCol += displayWidth
|
||||
currentOutputExternalColumn += displayWidth
|
||||
if (justToCursor && newCursorPlaced) break
|
||||
}
|
||||
i++
|
||||
}
|
||||
// Old row has been copied. Check if we need to insert newline if old line was not wrapping:
|
||||
if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) {
|
||||
if (currentOutputExternalRow == mScreenRows - 1) {
|
||||
if (newCursorPlaced) newCursorRow--
|
||||
scrollDownOneLine(0, mScreenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
}
|
||||
|
||||
cursor[0] = newCursorColumn
|
||||
cursor[1] = newCursorRow
|
||||
}
|
||||
|
||||
// Handle cursor scrolling off screen:
|
||||
if (cursor[0] < 0 || cursor[1] < 0) {
|
||||
cursor[0] = 0
|
||||
cursor[1] = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
|
||||
* into account.
|
||||
*
|
||||
* @param srcInternal The first line to be copied.
|
||||
* @param len The number of lines to be copied.
|
||||
*/
|
||||
private fun blockCopyLinesDown(srcInternal: Int, len: Int) {
|
||||
if (len == 0) return
|
||||
val totalRows = mTotalRows
|
||||
|
||||
val start = len - 1
|
||||
// Save away line to be overwritten:
|
||||
val lineToBeOverWritten = mLines[(srcInternal + start + 1) % totalRows]
|
||||
// Do the copy from bottom to top.
|
||||
for (i in start downTo 0)
|
||||
mLines[(srcInternal + i + 1) % totalRows] = mLines[(srcInternal + i) % totalRows]
|
||||
// Put back overwritten line, now above the block:
|
||||
mLines[srcInternal % totalRows] = lineToBeOverWritten
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24).
|
||||
*
|
||||
* @param topMargin First line that is scrolled.
|
||||
* @param bottomMargin One line after the last line that is scrolled.
|
||||
* @param style the style for the newly exposed line.
|
||||
*/
|
||||
fun scrollDownOneLine(topMargin: Int, bottomMargin: Int, style: Long) {
|
||||
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
|
||||
throw IllegalArgumentException("topMargin=$topMargin, bottomMargin=$bottomMargin, mScreenRows=$mScreenRows")
|
||||
|
||||
// Copy the fixed topMargin lines one line down so that they remain on screen in same position:
|
||||
blockCopyLinesDown(mScreenFirstRow, topMargin)
|
||||
// Copy the fixed mScreenRows-bottomMargin lines one line down so that they remain on screen in same
|
||||
// position:
|
||||
blockCopyLinesDown(externalToInternalRow(bottomMargin), mScreenRows - bottomMargin)
|
||||
|
||||
// Update the screen location in the ring buffer:
|
||||
mScreenFirstRow = (mScreenFirstRow + 1) % mTotalRows
|
||||
// Note that the history has grown if not already full:
|
||||
if (mActiveTranscriptRows < mTotalRows - mScreenRows) mActiveTranscriptRows++
|
||||
|
||||
// Blank the newly revealed line above the bottom margin:
|
||||
val blankRow = externalToInternalRow(bottomMargin - 1)
|
||||
if (mLines[blankRow] == null) {
|
||||
mLines[blankRow] = TerminalRow(mColumns, style)
|
||||
} else {
|
||||
mLines[blankRow]!!.clear(style)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block copy characters from one position in the screen to another. The two positions can overlap. All characters
|
||||
* of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will
|
||||
* be thrown.
|
||||
*
|
||||
* @param sx source X coordinate
|
||||
* @param sy source Y coordinate
|
||||
* @param w width
|
||||
* @param h height
|
||||
* @param dx destination X coordinate
|
||||
* @param dy destination Y coordinate
|
||||
*/
|
||||
fun blockCopy(sx: Int, sy: Int, w: Int, h: Int, dx: Int, dy: Int) {
|
||||
if (w == 0) return
|
||||
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows)
|
||||
throw IllegalArgumentException()
|
||||
val copyingUp = sy > dy
|
||||
for (y in 0 until h) {
|
||||
val y2 = if (copyingUp) y else (h - (y + 1))
|
||||
val sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2))
|
||||
allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block set characters. All characters must be within the bounds of the screen, or else and
|
||||
* InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
|
||||
* of characters.
|
||||
*/
|
||||
fun blockSet(sx: Int, sy: Int, w: Int, h: Int, `val`: Int, style: Long) {
|
||||
if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
|
||||
throw IllegalArgumentException(
|
||||
"Illegal arguments! blockSet($sx, $sy, $w, $h, $`val`, $mColumns, $mScreenRows)"
|
||||
)
|
||||
}
|
||||
for (y in 0 until h)
|
||||
for (x in 0 until w)
|
||||
setChar(sx + x, sy + y, `val`, style)
|
||||
}
|
||||
|
||||
fun allocateFullLineIfNecessary(row: Int): TerminalRow {
|
||||
return mLines[row] ?: TerminalRow(mColumns, 0).also { mLines[row] = it }
|
||||
}
|
||||
|
||||
fun setChar(column: Int, row: Int, codePoint: Int, style: Long) {
|
||||
if (row < 0 || row >= mScreenRows || column < 0 || column >= mColumns)
|
||||
throw IllegalArgumentException("TerminalBuffer.setChar(): row=$row, column=$column, mScreenRows=$mScreenRows, mColumns=$mColumns")
|
||||
val internalRow = externalToInternalRow(row)
|
||||
allocateFullLineIfNecessary(internalRow).setChar(column, codePoint, style)
|
||||
}
|
||||
|
||||
fun getStyleAt(externalRow: Int, column: Int): Long {
|
||||
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column)
|
||||
}
|
||||
|
||||
/** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
|
||||
fun setOrClearEffect(
|
||||
bits: Int,
|
||||
setOrClear: Boolean,
|
||||
reverse: Boolean,
|
||||
rectangular: Boolean,
|
||||
leftMargin: Int,
|
||||
rightMargin: Int,
|
||||
top: Int,
|
||||
left: Int,
|
||||
bottom: Int,
|
||||
right: Int
|
||||
) {
|
||||
for (y in top until bottom) {
|
||||
val line = mLines[externalToInternalRow(y)]!!
|
||||
val startOfLine = if (rectangular || y == top) left else leftMargin
|
||||
val endOfLine = if (rectangular || y + 1 == bottom) right else rightMargin
|
||||
for (x in startOfLine until endOfLine) {
|
||||
val currentStyle = line.getStyle(x)
|
||||
val foreColor = TextStyle.decodeForeColor(currentStyle)
|
||||
val backColor = TextStyle.decodeBackColor(currentStyle)
|
||||
var effect = TextStyle.decodeEffect(currentStyle)
|
||||
effect = if (reverse) {
|
||||
// Clear out the bits to reverse and add them back in reversed:
|
||||
(effect and bits.inv()) or (bits and effect.inv())
|
||||
} else if (setOrClear) {
|
||||
effect or bits
|
||||
} else {
|
||||
effect and bits.inv()
|
||||
}
|
||||
line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearTranscript() {
|
||||
if (mScreenFirstRow < mActiveTranscriptRows) {
|
||||
Arrays.fill(mLines, mTotalRows + mScreenFirstRow - mActiveTranscriptRows, mTotalRows, null)
|
||||
Arrays.fill(mLines, 0, mScreenFirstRow, null)
|
||||
} else {
|
||||
Arrays.fill(mLines, mScreenFirstRow - mActiveTranscriptRows, mScreenFirstRow, null)
|
||||
}
|
||||
mActiveTranscriptRows = 0
|
||||
}
|
||||
}
|
||||
151
app/src/main/java/com/termux/terminal/TerminalColorScheme.kt
Normal file
151
app/src/main/java/com/termux/terminal/TerminalColorScheme.kt
Normal file
@@ -0,0 +1,151 @@
|
||||
package com.termux.terminal
|
||||
|
||||
import java.util.Properties
|
||||
|
||||
/**
|
||||
* Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
|
||||
* Operating System Control (OSC) sequences.
|
||||
*
|
||||
* @see TerminalColors
|
||||
*/
|
||||
class TerminalColorScheme {
|
||||
|
||||
@JvmField
|
||||
val mDefaultColors = IntArray(TextStyle.NUM_INDEXED_COLORS)
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
System.arraycopy(DEFAULT_COLORSCHEME, 0, mDefaultColors, 0, TextStyle.NUM_INDEXED_COLORS)
|
||||
}
|
||||
|
||||
fun updateWith(props: Properties) {
|
||||
reset()
|
||||
var cursorPropExists = false
|
||||
for ((key, value) in props.entries) {
|
||||
val keyStr = key as String
|
||||
val valueStr = value as String
|
||||
val colorIndex: Int
|
||||
|
||||
when {
|
||||
keyStr == "foreground" -> colorIndex = TextStyle.COLOR_INDEX_FOREGROUND
|
||||
keyStr == "background" -> colorIndex = TextStyle.COLOR_INDEX_BACKGROUND
|
||||
keyStr == "cursor" -> {
|
||||
colorIndex = TextStyle.COLOR_INDEX_CURSOR
|
||||
cursorPropExists = true
|
||||
}
|
||||
keyStr.startsWith("color") -> {
|
||||
colorIndex = try {
|
||||
keyStr.substring(5).toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
throw IllegalArgumentException("Invalid property: '$keyStr'")
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Invalid property: '$keyStr'")
|
||||
}
|
||||
|
||||
val colorValue = TerminalColors.parse(valueStr)
|
||||
if (colorValue == 0) {
|
||||
throw IllegalArgumentException("Property '$keyStr' has invalid color: '$valueStr'")
|
||||
}
|
||||
|
||||
mDefaultColors[colorIndex] = colorValue
|
||||
}
|
||||
|
||||
if (!cursorPropExists) {
|
||||
setCursorColorForBackground()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the "cursor" color is not set by user, we need to decide on the appropriate color that will
|
||||
* be visible on the current terminal background. White will not be visible on light backgrounds
|
||||
* and black won't be visible on dark backgrounds. So we find the perceived brightness of the
|
||||
* background color and if its below the threshold (too dark), we use white cursor and if its
|
||||
* above (too bright), we use black cursor.
|
||||
*/
|
||||
fun setCursorColorForBackground() {
|
||||
val backgroundColor = mDefaultColors[TextStyle.COLOR_INDEX_BACKGROUND]
|
||||
val brightness = TerminalColors.getPerceivedBrightnessOfColor(backgroundColor)
|
||||
if (brightness > 0) {
|
||||
mDefaultColors[TextStyle.COLOR_INDEX_CURSOR] = if (brightness < 130) {
|
||||
0xffffffff.toInt()
|
||||
} else {
|
||||
0xff000000.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg, but with blue color brighter. */
|
||||
private val DEFAULT_COLORSCHEME = intArrayOf(
|
||||
// 16 original colors. First 8 are dim.
|
||||
0xff000000.toInt(), // black
|
||||
0xffcd0000.toInt(), // dim red
|
||||
0xff00cd00.toInt(), // dim green
|
||||
0xffcdcd00.toInt(), // dim yellow
|
||||
0xff6495ed.toInt(), // dim blue
|
||||
0xffcd00cd.toInt(), // dim magenta
|
||||
0xff00cdcd.toInt(), // dim cyan
|
||||
0xffe5e5e5.toInt(), // dim white
|
||||
// Second 8 are bright:
|
||||
0xff7f7f7f.toInt(), // medium grey
|
||||
0xffff0000.toInt(), // bright red
|
||||
0xff00ff00.toInt(), // bright green
|
||||
0xffffff00.toInt(), // bright yellow
|
||||
0xff5c5cff.toInt(), // light blue
|
||||
0xffff00ff.toInt(), // bright magenta
|
||||
0xff00ffff.toInt(), // bright cyan
|
||||
0xffffffff.toInt(), // bright white
|
||||
|
||||
// 216 color cube, six shades of each color:
|
||||
0xff000000.toInt(), 0xff00005f.toInt(), 0xff000087.toInt(), 0xff0000af.toInt(), 0xff0000d7.toInt(), 0xff0000ff.toInt(),
|
||||
0xff005f00.toInt(), 0xff005f5f.toInt(), 0xff005f87.toInt(), 0xff005faf.toInt(), 0xff005fd7.toInt(), 0xff005fff.toInt(),
|
||||
0xff008700.toInt(), 0xff00875f.toInt(), 0xff008787.toInt(), 0xff0087af.toInt(), 0xff0087d7.toInt(), 0xff0087ff.toInt(),
|
||||
0xff00af00.toInt(), 0xff00af5f.toInt(), 0xff00af87.toInt(), 0xff00afaf.toInt(), 0xff00afd7.toInt(), 0xff00afff.toInt(),
|
||||
0xff00d700.toInt(), 0xff00d75f.toInt(), 0xff00d787.toInt(), 0xff00d7af.toInt(), 0xff00d7d7.toInt(), 0xff00d7ff.toInt(),
|
||||
0xff00ff00.toInt(), 0xff00ff5f.toInt(), 0xff00ff87.toInt(), 0xff00ffaf.toInt(), 0xff00ffd7.toInt(), 0xff00ffff.toInt(),
|
||||
0xff5f0000.toInt(), 0xff5f005f.toInt(), 0xff5f0087.toInt(), 0xff5f00af.toInt(), 0xff5f00d7.toInt(), 0xff5f00ff.toInt(),
|
||||
0xff5f5f00.toInt(), 0xff5f5f5f.toInt(), 0xff5f5f87.toInt(), 0xff5f5faf.toInt(), 0xff5f5fd7.toInt(), 0xff5f5fff.toInt(),
|
||||
0xff5f8700.toInt(), 0xff5f875f.toInt(), 0xff5f8787.toInt(), 0xff5f87af.toInt(), 0xff5f87d7.toInt(), 0xff5f87ff.toInt(),
|
||||
0xff5faf00.toInt(), 0xff5faf5f.toInt(), 0xff5faf87.toInt(), 0xff5fafaf.toInt(), 0xff5fafd7.toInt(), 0xff5fafff.toInt(),
|
||||
0xff5fd700.toInt(), 0xff5fd75f.toInt(), 0xff5fd787.toInt(), 0xff5fd7af.toInt(), 0xff5fd7d7.toInt(), 0xff5fd7ff.toInt(),
|
||||
0xff5fff00.toInt(), 0xff5fff5f.toInt(), 0xff5fff87.toInt(), 0xff5fffaf.toInt(), 0xff5fffd7.toInt(), 0xff5fffff.toInt(),
|
||||
0xff870000.toInt(), 0xff87005f.toInt(), 0xff870087.toInt(), 0xff8700af.toInt(), 0xff8700d7.toInt(), 0xff8700ff.toInt(),
|
||||
0xff875f00.toInt(), 0xff875f5f.toInt(), 0xff875f87.toInt(), 0xff875faf.toInt(), 0xff875fd7.toInt(), 0xff875fff.toInt(),
|
||||
0xff878700.toInt(), 0xff87875f.toInt(), 0xff878787.toInt(), 0xff8787af.toInt(), 0xff8787d7.toInt(), 0xff8787ff.toInt(),
|
||||
0xff87af00.toInt(), 0xff87af5f.toInt(), 0xff87af87.toInt(), 0xff87afaf.toInt(), 0xff87afd7.toInt(), 0xff87afff.toInt(),
|
||||
0xff87d700.toInt(), 0xff87d75f.toInt(), 0xff87d787.toInt(), 0xff87d7af.toInt(), 0xff87d7d7.toInt(), 0xff87d7ff.toInt(),
|
||||
0xff87ff00.toInt(), 0xff87ff5f.toInt(), 0xff87ff87.toInt(), 0xff87ffaf.toInt(), 0xff87ffd7.toInt(), 0xff87ffff.toInt(),
|
||||
0xffaf0000.toInt(), 0xffaf005f.toInt(), 0xffaf0087.toInt(), 0xffaf00af.toInt(), 0xffaf00d7.toInt(), 0xffaf00ff.toInt(),
|
||||
0xffaf5f00.toInt(), 0xffaf5f5f.toInt(), 0xffaf5f87.toInt(), 0xffaf5faf.toInt(), 0xffaf5fd7.toInt(), 0xffaf5fff.toInt(),
|
||||
0xffaf8700.toInt(), 0xffaf875f.toInt(), 0xffaf8787.toInt(), 0xffaf87af.toInt(), 0xffaf87d7.toInt(), 0xffaf87ff.toInt(),
|
||||
0xffafaf00.toInt(), 0xffafaf5f.toInt(), 0xffafaf87.toInt(), 0xffafafaf.toInt(), 0xffafafd7.toInt(), 0xffafafff.toInt(),
|
||||
0xffafd700.toInt(), 0xffafd75f.toInt(), 0xffafd787.toInt(), 0xffafd7af.toInt(), 0xffafd7d7.toInt(), 0xffafd7ff.toInt(),
|
||||
0xffafff00.toInt(), 0xffafff5f.toInt(), 0xffafff87.toInt(), 0xffafffaf.toInt(), 0xffafffd7.toInt(), 0xffafffff.toInt(),
|
||||
0xffd70000.toInt(), 0xffd7005f.toInt(), 0xffd70087.toInt(), 0xffd700af.toInt(), 0xffd700d7.toInt(), 0xffd700ff.toInt(),
|
||||
0xffd75f00.toInt(), 0xffd75f5f.toInt(), 0xffd75f87.toInt(), 0xffd75faf.toInt(), 0xffd75fd7.toInt(), 0xffd75fff.toInt(),
|
||||
0xffd78700.toInt(), 0xffd7875f.toInt(), 0xffd78787.toInt(), 0xffd787af.toInt(), 0xffd787d7.toInt(), 0xffd787ff.toInt(),
|
||||
0xffd7af00.toInt(), 0xffd7af5f.toInt(), 0xffd7af87.toInt(), 0xffd7afaf.toInt(), 0xffd7afd7.toInt(), 0xffd7afff.toInt(),
|
||||
0xffd7d700.toInt(), 0xffd7d75f.toInt(), 0xffd7d787.toInt(), 0xffd7d7af.toInt(), 0xffd7d7d7.toInt(), 0xffd7d7ff.toInt(),
|
||||
0xffd7ff00.toInt(), 0xffd7ff5f.toInt(), 0xffd7ff87.toInt(), 0xffd7ffaf.toInt(), 0xffd7ffd7.toInt(), 0xffd7ffff.toInt(),
|
||||
0xffff0000.toInt(), 0xffff005f.toInt(), 0xffff0087.toInt(), 0xffff00af.toInt(), 0xffff00d7.toInt(), 0xffff00ff.toInt(),
|
||||
0xffff5f00.toInt(), 0xffff5f5f.toInt(), 0xffff5f87.toInt(), 0xffff5faf.toInt(), 0xffff5fd7.toInt(), 0xffff5fff.toInt(),
|
||||
0xffff8700.toInt(), 0xffff875f.toInt(), 0xffff8787.toInt(), 0xffff87af.toInt(), 0xffff87d7.toInt(), 0xffff87ff.toInt(),
|
||||
0xffffaf00.toInt(), 0xffffaf5f.toInt(), 0xffffaf87.toInt(), 0xffffafaf.toInt(), 0xffffafd7.toInt(), 0xffffafff.toInt(),
|
||||
0xffffd700.toInt(), 0xffffd75f.toInt(), 0xffffd787.toInt(), 0xffffd7af.toInt(), 0xffffd7d7.toInt(), 0xffffd7ff.toInt(),
|
||||
0xffffff00.toInt(), 0xffffff5f.toInt(), 0xffffff87.toInt(), 0xffffffaf.toInt(), 0xffffffd7.toInt(), 0xffffffff.toInt(),
|
||||
|
||||
// 24 grey scale ramp:
|
||||
0xff080808.toInt(), 0xff121212.toInt(), 0xff1c1c1c.toInt(), 0xff262626.toInt(), 0xff303030.toInt(), 0xff3a3a3a.toInt(),
|
||||
0xff444444.toInt(), 0xff4e4e4e.toInt(), 0xff585858.toInt(), 0xff626262.toInt(), 0xff6c6c6c.toInt(), 0xff767676.toInt(),
|
||||
0xff808080.toInt(), 0xff8a8a8a.toInt(), 0xff949494.toInt(), 0xff9e9e9e.toInt(), 0xffa8a8a8.toInt(), 0xffb2b2b2.toInt(),
|
||||
0xffbcbcbc.toInt(), 0xffc6c6c6.toInt(), 0xffd0d0d0.toInt(), 0xffdadada.toInt(), 0xffe4e4e4.toInt(), 0xffeeeeee.toInt(),
|
||||
|
||||
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
|
||||
0xffffffff.toInt(), 0xff000000.toInt(), 0xffffffff.toInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
108
app/src/main/java/com/termux/terminal/TerminalColors.kt
Normal file
108
app/src/main/java/com/termux/terminal/TerminalColors.kt
Normal file
@@ -0,0 +1,108 @@
|
||||
package com.termux.terminal
|
||||
|
||||
import android.graphics.Color
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/** Current terminal colors (if different from default). */
|
||||
class TerminalColors {
|
||||
|
||||
/**
|
||||
* The current terminal colors, which are normally set from the color theme, but may be set dynamically with the OSC
|
||||
* 4 control sequence.
|
||||
*/
|
||||
@JvmField
|
||||
val mCurrentColors = IntArray(TextStyle.NUM_INDEXED_COLORS)
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
/** Reset a particular indexed color with the default color from the color theme. */
|
||||
fun reset(index: Int) {
|
||||
mCurrentColors[index] = COLOR_SCHEME.mDefaultColors[index]
|
||||
}
|
||||
|
||||
/** Reset all indexed colors with the default color from the color theme. */
|
||||
fun reset() {
|
||||
System.arraycopy(COLOR_SCHEME.mDefaultColors, 0, mCurrentColors, 0, TextStyle.NUM_INDEXED_COLORS)
|
||||
}
|
||||
|
||||
/** Try parse a color from a text parameter and into a specified index. */
|
||||
fun tryParseColor(intoIndex: Int, textParameter: String?) {
|
||||
val c = parse(textParameter)
|
||||
if (c != 0) mCurrentColors[intoIndex] = c
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Static data - a bit ugly but ok for now. */
|
||||
@JvmField
|
||||
val COLOR_SCHEME = TerminalColorScheme()
|
||||
|
||||
/**
|
||||
* Parse color according to http://manpages.ubuntu.com/manpages/intrepid/man3/XQueryColor.3.html
|
||||
*
|
||||
* Highest bit is set if successful, so return value is 0xFF${R}${G}${B}. Return 0 if failed.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun parse(c: String?): Int {
|
||||
if (c == null) return 0
|
||||
try {
|
||||
val skipInitial: Int
|
||||
val skipBetween: Int
|
||||
when {
|
||||
c[0] == '#' -> {
|
||||
// #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB. Most significant bits.
|
||||
skipInitial = 1
|
||||
skipBetween = 0
|
||||
}
|
||||
c.startsWith("rgb:") -> {
|
||||
// rgb:<red>/<green>/<blue> where <red>, <green>, <blue> := h | hh | hhh | hhhh. Scaled.
|
||||
skipInitial = 4
|
||||
skipBetween = 1
|
||||
}
|
||||
else -> return 0
|
||||
}
|
||||
val charsForColors = c.length - skipInitial - 2 * skipBetween
|
||||
if (charsForColors % 3 != 0) return 0 // Unequal lengths.
|
||||
val componentLength = charsForColors / 3
|
||||
val mult = 255.0 / (2.0.pow(componentLength * 4.0) - 1)
|
||||
|
||||
var currentPosition = skipInitial
|
||||
val rString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
currentPosition += componentLength + skipBetween
|
||||
val gString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
currentPosition += componentLength + skipBetween
|
||||
val bString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
|
||||
val r = (Integer.parseInt(rString, 16) * mult).toInt()
|
||||
val g = (Integer.parseInt(gString, 16) * mult).toInt()
|
||||
val b = (Integer.parseInt(bString, 16) * mult).toInt()
|
||||
return (0xFF shl 24) or (r shl 16) or (g shl 8) or b
|
||||
} catch (e: Exception) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the perceived brightness of the color based on its RGB components.
|
||||
*
|
||||
* https://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx
|
||||
* http://alienryderflex.com/hsp.html
|
||||
*
|
||||
* @param color The color code int.
|
||||
* @return Returns value between 0-255.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getPerceivedBrightnessOfColor(color: Int): Int {
|
||||
return floor(
|
||||
sqrt(
|
||||
Color.red(color).toDouble().pow(2.0) * 0.241 +
|
||||
Color.green(color).toDouble().pow(2.0) * 0.691 +
|
||||
Color.blue(color).toDouble().pow(2.0) * 0.068
|
||||
)
|
||||
).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
1838
app/src/main/java/com/termux/terminal/TerminalEmulator.kt
Normal file
1838
app/src/main/java/com/termux/terminal/TerminalEmulator.kt
Normal file
File diff suppressed because it is too large
Load Diff
31
app/src/main/java/com/termux/terminal/TerminalOutput.kt
Normal file
31
app/src/main/java/com/termux/terminal/TerminalOutput.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.termux.terminal
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/** A client which receives callbacks from events triggered by feeding input to a [TerminalEmulator]. */
|
||||
abstract class TerminalOutput {
|
||||
|
||||
/** Write a string using the UTF-8 encoding to the terminal client. */
|
||||
fun write(data: String?) {
|
||||
if (data == null) return
|
||||
val bytes = data.toByteArray(StandardCharsets.UTF_8)
|
||||
write(bytes, 0, bytes.size)
|
||||
}
|
||||
|
||||
/** Write bytes to the terminal client. */
|
||||
abstract fun write(data: ByteArray, offset: Int, count: Int)
|
||||
|
||||
/** Notify the terminal client that the terminal title has changed. */
|
||||
abstract fun titleChanged(oldTitle: String?, newTitle: String?)
|
||||
|
||||
/** Notify the terminal client that text should be copied to clipboard. */
|
||||
abstract fun onCopyTextToClipboard(text: String?)
|
||||
|
||||
/** Notify the terminal client that text should be pasted from clipboard. */
|
||||
abstract fun onPasteTextFromClipboard()
|
||||
|
||||
/** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */
|
||||
abstract fun onBell()
|
||||
|
||||
abstract fun onColorsChanged()
|
||||
}
|
||||
279
app/src/main/java/com/termux/terminal/TerminalRow.kt
Normal file
279
app/src/main/java/com/termux/terminal/TerminalRow.kt
Normal file
@@ -0,0 +1,279 @@
|
||||
package com.termux.terminal
|
||||
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* A row in a terminal, composed of a fixed number of cells.
|
||||
*
|
||||
* The text in the row is stored in a char[] array, [mText], for quick access during rendering.
|
||||
*/
|
||||
class TerminalRow(
|
||||
/** The number of columns in this terminal row. */
|
||||
private val mColumns: Int,
|
||||
style: Long
|
||||
) {
|
||||
/** The text filling this terminal row. */
|
||||
@JvmField
|
||||
var mText: CharArray
|
||||
|
||||
/** The number of java chars used in [mText]. */
|
||||
private var mSpaceUsed: Short = 0
|
||||
|
||||
/** If this row has been line wrapped due to text output at the end of line. */
|
||||
@JvmField
|
||||
var mLineWrap: Boolean = false
|
||||
|
||||
/** The style bits of each cell in the row. See [TextStyle]. */
|
||||
@JvmField
|
||||
val mStyle: LongArray
|
||||
|
||||
/** If this row might contain chars with width != 1, used for deactivating fast path */
|
||||
@JvmField
|
||||
var mHasNonOneWidthOrSurrogateChars: Boolean = false
|
||||
|
||||
init {
|
||||
mText = CharArray((SPARE_CAPACITY_FACTOR * mColumns).toInt())
|
||||
mStyle = LongArray(mColumns)
|
||||
clear(style)
|
||||
}
|
||||
|
||||
/** NOTE: The sourceX2 is exclusive. */
|
||||
fun copyInterval(line: TerminalRow, sourceX1: Int, sourceX2: Int, destinationX: Int) {
|
||||
var srcX1 = sourceX1
|
||||
var destX = destinationX
|
||||
mHasNonOneWidthOrSurrogateChars = mHasNonOneWidthOrSurrogateChars or line.mHasNonOneWidthOrSurrogateChars
|
||||
val x1 = line.findStartOfColumn(srcX1)
|
||||
val x2 = line.findStartOfColumn(sourceX2)
|
||||
var startingFromSecondHalfOfWideChar = srcX1 > 0 && line.wideDisplayCharacterStartingAt(srcX1 - 1)
|
||||
val sourceChars = if (this === line) Arrays.copyOf(line.mText, line.mText.size) else line.mText
|
||||
var latestNonCombiningWidth = 0
|
||||
var i = x1
|
||||
while (i < x2) {
|
||||
var sourceChar = sourceChars[i]
|
||||
var codePoint = if (Character.isHighSurrogate(sourceChar)) Character.toCodePoint(sourceChar, sourceChars[++i]) else sourceChar.code
|
||||
if (startingFromSecondHalfOfWideChar) {
|
||||
// Just treat copying second half of wide char as copying whitespace.
|
||||
codePoint = ' '.code
|
||||
startingFromSecondHalfOfWideChar = false
|
||||
}
|
||||
val w = WcWidth.width(codePoint)
|
||||
if (w > 0) {
|
||||
destX += latestNonCombiningWidth
|
||||
srcX1 += latestNonCombiningWidth
|
||||
latestNonCombiningWidth = w
|
||||
}
|
||||
setChar(destX, codePoint, line.getStyle(srcX1))
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
val spaceUsed: Int
|
||||
get() = mSpaceUsed.toInt()
|
||||
|
||||
/** Note that the column may end of second half of wide character. */
|
||||
fun findStartOfColumn(column: Int): Int {
|
||||
if (column == mColumns) return spaceUsed
|
||||
|
||||
var currentColumn = 0
|
||||
var currentCharIndex = 0
|
||||
while (true) {
|
||||
var newCharIndex = currentCharIndex
|
||||
val c = mText[newCharIndex++]
|
||||
val isHigh = Character.isHighSurrogate(c)
|
||||
val codePoint = if (isHigh) Character.toCodePoint(c, mText[newCharIndex++]) else c.code
|
||||
val wcwidth = WcWidth.width(codePoint)
|
||||
if (wcwidth > 0) {
|
||||
currentColumn += wcwidth
|
||||
if (currentColumn == column) {
|
||||
while (newCharIndex < mSpaceUsed) {
|
||||
// Skip combining chars.
|
||||
if (Character.isHighSurrogate(mText[newCharIndex])) {
|
||||
if (WcWidth.width(Character.toCodePoint(mText[newCharIndex], mText[newCharIndex + 1])) <= 0) {
|
||||
newCharIndex += 2
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if (WcWidth.width(mText[newCharIndex].code) <= 0) {
|
||||
newCharIndex++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return newCharIndex
|
||||
} else if (currentColumn > column) {
|
||||
// Wide column going past end.
|
||||
return currentCharIndex
|
||||
}
|
||||
}
|
||||
currentCharIndex = newCharIndex
|
||||
}
|
||||
}
|
||||
|
||||
private fun wideDisplayCharacterStartingAt(column: Int): Boolean {
|
||||
var currentCharIndex = 0
|
||||
var currentColumn = 0
|
||||
while (currentCharIndex < mSpaceUsed) {
|
||||
val c = mText[currentCharIndex++]
|
||||
val codePoint = if (Character.isHighSurrogate(c)) Character.toCodePoint(c, mText[currentCharIndex++]) else c.code
|
||||
val wcwidth = WcWidth.width(codePoint)
|
||||
if (wcwidth > 0) {
|
||||
if (currentColumn == column && wcwidth == 2) return true
|
||||
currentColumn += wcwidth
|
||||
if (currentColumn > column) return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun clear(style: Long) {
|
||||
Arrays.fill(mText, ' ')
|
||||
Arrays.fill(mStyle, style)
|
||||
mSpaceUsed = mColumns.toShort()
|
||||
mHasNonOneWidthOrSurrogateChars = false
|
||||
}
|
||||
|
||||
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||
fun setChar(columnToSet: Int, codePoint: Int, style: Long) {
|
||||
if (columnToSet < 0 || columnToSet >= mStyle.size)
|
||||
throw IllegalArgumentException("TerminalRow.setChar(): columnToSet=$columnToSet, codePoint=$codePoint, style=$style")
|
||||
|
||||
mStyle[columnToSet] = style
|
||||
|
||||
val newCodePointDisplayWidth = WcWidth.width(codePoint)
|
||||
|
||||
// Fast path when we don't have any chars with width != 1
|
||||
if (!mHasNonOneWidthOrSurrogateChars) {
|
||||
if (codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT || newCodePointDisplayWidth != 1) {
|
||||
mHasNonOneWidthOrSurrogateChars = true
|
||||
} else {
|
||||
mText[columnToSet] = codePoint.toChar()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val newIsCombining = newCodePointDisplayWidth <= 0
|
||||
var actualColumnToSet = columnToSet
|
||||
|
||||
val wasExtraColForWideChar = columnToSet > 0 && wideDisplayCharacterStartingAt(columnToSet - 1)
|
||||
|
||||
if (newIsCombining) {
|
||||
// When standing at second half of wide character and inserting combining:
|
||||
if (wasExtraColForWideChar) actualColumnToSet--
|
||||
} else {
|
||||
// Check if we are overwriting the second half of a wide character starting at the previous column:
|
||||
if (wasExtraColForWideChar) setChar(columnToSet - 1, ' '.code, style)
|
||||
// Check if we are overwriting the first half of a wide character starting at the next column:
|
||||
val overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(columnToSet + 1)
|
||||
if (overwritingWideCharInNextColumn) setChar(columnToSet + 1, ' '.code, style)
|
||||
}
|
||||
|
||||
var text = mText
|
||||
val oldStartOfColumnIndex = findStartOfColumn(actualColumnToSet)
|
||||
val oldCodePointDisplayWidth = WcWidth.width(text, oldStartOfColumnIndex)
|
||||
|
||||
// Get the number of elements in the mText array this column uses now
|
||||
val oldCharactersUsedForColumn: Int
|
||||
if (actualColumnToSet + oldCodePointDisplayWidth < mColumns) {
|
||||
val oldEndOfColumnIndex = findStartOfColumn(actualColumnToSet + oldCodePointDisplayWidth)
|
||||
oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex
|
||||
} else {
|
||||
// Last character.
|
||||
oldCharactersUsedForColumn = mSpaceUsed - oldStartOfColumnIndex
|
||||
}
|
||||
|
||||
// If MAX_COMBINING_CHARACTERS_PER_COLUMN already exist in column, then ignore adding additional combining characters.
|
||||
if (newIsCombining) {
|
||||
val combiningCharsCount = WcWidth.zeroWidthCharsCount(mText, oldStartOfColumnIndex, oldStartOfColumnIndex + oldCharactersUsedForColumn)
|
||||
if (combiningCharsCount >= MAX_COMBINING_CHARACTERS_PER_COLUMN)
|
||||
return
|
||||
}
|
||||
|
||||
// Find how many chars this column will need
|
||||
var newCharactersUsedForColumn = Character.charCount(codePoint)
|
||||
if (newIsCombining) {
|
||||
// Combining characters are added to the contents of the column instead of overwriting them, so that they
|
||||
// modify the existing contents.
|
||||
// FIXME: Unassigned characters also get width=0.
|
||||
newCharactersUsedForColumn += oldCharactersUsedForColumn
|
||||
}
|
||||
|
||||
val oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn
|
||||
val newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn
|
||||
|
||||
val javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn
|
||||
if (javaCharDifference > 0) {
|
||||
// Shift the rest of the line right.
|
||||
val oldCharactersAfterColumn = mSpaceUsed - oldNextColumnIndex
|
||||
if (mSpaceUsed + javaCharDifference > text.size) {
|
||||
// We need to grow the array
|
||||
val newText = CharArray(text.size + mColumns)
|
||||
System.arraycopy(text, 0, newText, 0, oldNextColumnIndex)
|
||||
System.arraycopy(text, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn)
|
||||
mText = newText
|
||||
text = newText
|
||||
} else {
|
||||
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, oldCharactersAfterColumn)
|
||||
}
|
||||
} else if (javaCharDifference < 0) {
|
||||
// Shift the rest of the line left.
|
||||
System.arraycopy(text, oldNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - oldNextColumnIndex)
|
||||
}
|
||||
mSpaceUsed = (mSpaceUsed + javaCharDifference).toShort()
|
||||
|
||||
// Store char. A combining character is stored at the end of the existing contents so that it modifies them:
|
||||
Character.toChars(codePoint, text, oldStartOfColumnIndex + (if (newIsCombining) oldCharactersUsedForColumn else 0))
|
||||
|
||||
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
|
||||
// Replace second half of wide char with a space. Which mean that we actually add a ' ' java character.
|
||||
if (mSpaceUsed + 1 > text.size) {
|
||||
val newText = CharArray(text.size + mColumns)
|
||||
System.arraycopy(text, 0, newText, 0, newNextColumnIndex)
|
||||
System.arraycopy(text, newNextColumnIndex, newText, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex)
|
||||
mText = newText
|
||||
text = newText
|
||||
} else {
|
||||
System.arraycopy(text, newNextColumnIndex, text, newNextColumnIndex + 1, mSpaceUsed - newNextColumnIndex)
|
||||
}
|
||||
text[newNextColumnIndex] = ' '
|
||||
|
||||
mSpaceUsed++
|
||||
} else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) {
|
||||
if (actualColumnToSet == mColumns - 1) {
|
||||
throw IllegalArgumentException("Cannot put wide character in last column")
|
||||
} else if (actualColumnToSet == mColumns - 2) {
|
||||
// Truncate the line to the second part of this wide char:
|
||||
mSpaceUsed = newNextColumnIndex.toShort()
|
||||
} else {
|
||||
// Overwrite the contents of the next column, which mean we actually remove java characters. Due to the
|
||||
// check at the beginning of this method we know that we are not overwriting a wide char.
|
||||
val newNextNextColumnIndex = newNextColumnIndex + (if (Character.isHighSurrogate(mText[newNextColumnIndex])) 2 else 1)
|
||||
val nextLen = newNextNextColumnIndex - newNextColumnIndex
|
||||
|
||||
// Shift the array leftwards.
|
||||
System.arraycopy(text, newNextNextColumnIndex, text, newNextColumnIndex, mSpaceUsed - newNextNextColumnIndex)
|
||||
mSpaceUsed = (mSpaceUsed - nextLen).toShort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isBlank(): Boolean {
|
||||
for (charIndex in 0 until spaceUsed) {
|
||||
if (mText[charIndex] != ' ') return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun getStyle(column: Int): Long {
|
||||
return mStyle[column]
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SPARE_CAPACITY_FACTOR = 1.5f
|
||||
|
||||
/**
|
||||
* Max combining characters that can exist in a column, that are separate from the base character
|
||||
* itself. Any additional combining characters will be ignored and not added to the column.
|
||||
*/
|
||||
private const val MAX_COMBINING_CHARACTERS_PER_COLUMN = 15
|
||||
}
|
||||
}
|
||||
82
app/src/main/java/com/termux/terminal/TerminalSession.kt
Normal file
82
app/src/main/java/com/termux/terminal/TerminalSession.kt
Normal file
@@ -0,0 +1,82 @@
|
||||
package com.termux.terminal
|
||||
|
||||
class TerminalSession(
|
||||
private val shellWriter: (ByteArray, Int, Int) -> Unit,
|
||||
private val onScreenUpdated: () -> Unit,
|
||||
private val onCopyTextToClipboardRequested: (String) -> Unit,
|
||||
private val onPasteTextFromClipboardRequested: () -> Unit,
|
||||
private val onBellRequested: () -> Unit,
|
||||
) : TerminalOutput() {
|
||||
|
||||
var emulator: TerminalEmulator = createEmulator()
|
||||
private set
|
||||
|
||||
fun append(data: ByteArray, count: Int) {
|
||||
if (count <= 0) return
|
||||
emulator.append(data, count)
|
||||
onScreenUpdated()
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
emulator = createEmulator()
|
||||
onScreenUpdated()
|
||||
}
|
||||
|
||||
fun updateSize(
|
||||
columns: Int,
|
||||
rows: Int,
|
||||
cellWidthPixels: Int,
|
||||
cellHeightPixels: Int,
|
||||
) {
|
||||
emulator.resize(columns, rows, cellWidthPixels, cellHeightPixels)
|
||||
onScreenUpdated()
|
||||
}
|
||||
|
||||
override fun write(data: ByteArray, offset: Int, count: Int) {
|
||||
shellWriter(data, offset, count)
|
||||
}
|
||||
|
||||
fun writeCodePoint(prependEscape: Boolean, codePoint: Int) {
|
||||
if (codePoint > 0x10FFFF || codePoint in 0xD800..0xDFFF) {
|
||||
throw IllegalArgumentException("Invalid code point: $codePoint")
|
||||
}
|
||||
val utf8 = StringBuilder()
|
||||
if (prependEscape) {
|
||||
utf8.append('\u001B')
|
||||
}
|
||||
utf8.appendCodePoint(codePoint)
|
||||
write(utf8.toString())
|
||||
}
|
||||
|
||||
override fun titleChanged(oldTitle: String?, newTitle: String?) = Unit
|
||||
|
||||
override fun onCopyTextToClipboard(text: String?) {
|
||||
if (!text.isNullOrEmpty()) {
|
||||
onCopyTextToClipboardRequested(text)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPasteTextFromClipboard() {
|
||||
onPasteTextFromClipboardRequested()
|
||||
}
|
||||
|
||||
override fun onBell() {
|
||||
onBellRequested()
|
||||
}
|
||||
|
||||
override fun onColorsChanged() {
|
||||
onScreenUpdated()
|
||||
}
|
||||
|
||||
private fun createEmulator(): TerminalEmulator {
|
||||
return TerminalEmulator(
|
||||
mSession = this,
|
||||
columns = 80,
|
||||
rows = 24,
|
||||
cellWidthPixels = 1,
|
||||
cellHeightPixels = 1,
|
||||
transcriptRows = null,
|
||||
client = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.termux.terminal
|
||||
|
||||
/**
|
||||
* The interface for communication between [TerminalSession] and its client. It is used to
|
||||
* send callbacks to the client when [TerminalSession] changes or for sending other
|
||||
* back data to the client like logs.
|
||||
*/
|
||||
interface TerminalSessionClient {
|
||||
|
||||
fun onTextChanged(changedSession: TerminalSession)
|
||||
|
||||
fun onTitleChanged(changedSession: TerminalSession)
|
||||
|
||||
fun onSessionFinished(finishedSession: TerminalSession)
|
||||
|
||||
fun onCopyTextToClipboard(session: TerminalSession, text: String?)
|
||||
|
||||
fun onPasteTextFromClipboard(session: TerminalSession?)
|
||||
|
||||
fun onBell(session: TerminalSession)
|
||||
|
||||
fun onColorsChanged(session: TerminalSession)
|
||||
|
||||
fun onTerminalCursorStateChange(state: Boolean)
|
||||
|
||||
fun setTerminalShellPid(session: TerminalSession, pid: Int)
|
||||
|
||||
fun getTerminalCursorStyle(): Int?
|
||||
|
||||
fun logError(tag: String?, message: String?)
|
||||
|
||||
fun logWarn(tag: String?, message: String?)
|
||||
|
||||
fun logInfo(tag: String?, message: String?)
|
||||
|
||||
fun logDebug(tag: String?, message: String?)
|
||||
|
||||
fun logVerbose(tag: String?, message: String?)
|
||||
|
||||
fun logStackTraceWithMessage(tag: String?, message: String?, e: Exception?)
|
||||
|
||||
fun logStackTrace(tag: String?, e: Exception?)
|
||||
}
|
||||
88
app/src/main/java/com/termux/terminal/TextStyle.kt
Normal file
88
app/src/main/java/com/termux/terminal/TextStyle.kt
Normal file
@@ -0,0 +1,88 @@
|
||||
package com.termux.terminal
|
||||
|
||||
/**
|
||||
* Encodes effects, foreground and background colors into a 64 bit long, which are stored for each cell in a terminal
|
||||
* row in [TerminalRow.mStyle].
|
||||
*
|
||||
* The bit layout is:
|
||||
* - 16 flags (11 currently used).
|
||||
* - 24 for foreground color (only 9 first bits if a color index).
|
||||
* - 24 for background color (only 9 first bits if a color index).
|
||||
*/
|
||||
object TextStyle {
|
||||
|
||||
const val CHARACTER_ATTRIBUTE_BOLD = 1
|
||||
const val CHARACTER_ATTRIBUTE_ITALIC = 1 shl 1
|
||||
const val CHARACTER_ATTRIBUTE_UNDERLINE = 1 shl 2
|
||||
const val CHARACTER_ATTRIBUTE_BLINK = 1 shl 3
|
||||
const val CHARACTER_ATTRIBUTE_INVERSE = 1 shl 4
|
||||
const val CHARACTER_ATTRIBUTE_INVISIBLE = 1 shl 5
|
||||
const val CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 shl 6
|
||||
/**
|
||||
* The selective erase control functions (DECSED and DECSEL) can only erase characters defined as erasable.
|
||||
*
|
||||
* This bit is set if DECSCA (Select Character Protection Attribute) has been used to define the characters that
|
||||
* come after it as erasable from the screen.
|
||||
*/
|
||||
const val CHARACTER_ATTRIBUTE_PROTECTED = 1 shl 7
|
||||
/** Dim colors. Also known as faint or half intensity. */
|
||||
const val CHARACTER_ATTRIBUTE_DIM = 1 shl 8
|
||||
/** If true (24-bit) color is used for the cell for foreground. */
|
||||
private const val CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 shl 9
|
||||
/** If true (24-bit) color is used for the cell for foreground. */
|
||||
private const val CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND = 1 shl 10
|
||||
|
||||
const val COLOR_INDEX_FOREGROUND = 256
|
||||
const val COLOR_INDEX_BACKGROUND = 257
|
||||
const val COLOR_INDEX_CURSOR = 258
|
||||
|
||||
/** The 256 standard color entries and the three special (foreground, background and cursor) ones. */
|
||||
const val NUM_INDEXED_COLORS = 259
|
||||
|
||||
/** Normal foreground and background colors and no effects. */
|
||||
@JvmField
|
||||
val NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0)
|
||||
|
||||
@JvmStatic
|
||||
fun encode(foreColor: Int, backColor: Int, effect: Int): Long {
|
||||
var result = (effect and 0b111111111).toLong()
|
||||
if ((0xff000000.toInt() and foreColor) == 0xff000000.toInt()) {
|
||||
// 24-bit color.
|
||||
result = result or CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND.toLong() or ((foreColor.toLong() and 0x00ffffffL) shl 40)
|
||||
} else {
|
||||
// Indexed color.
|
||||
result = result or ((foreColor.toLong() and 0b111111111L) shl 40)
|
||||
}
|
||||
if ((0xff000000.toInt() and backColor) == 0xff000000.toInt()) {
|
||||
// 24-bit color.
|
||||
result = result or CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND.toLong() or ((backColor.toLong() and 0x00ffffffL) shl 16)
|
||||
} else {
|
||||
// Indexed color.
|
||||
result = result or ((backColor.toLong() and 0b111111111L) shl 16)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun decodeForeColor(style: Long): Int {
|
||||
return if ((style and CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND.toLong()) == 0L) {
|
||||
((style ushr 40) and 0b111111111L).toInt()
|
||||
} else {
|
||||
0xff000000.toInt() or ((style ushr 40) and 0x00ffffffL).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun decodeBackColor(style: Long): Int {
|
||||
return if ((style and CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND.toLong()) == 0L) {
|
||||
((style ushr 16) and 0b111111111L).toInt()
|
||||
} else {
|
||||
0xff000000.toInt() or ((style ushr 16) and 0x00ffffffL).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun decodeEffect(style: Long): Int {
|
||||
return (style and 0b11111111111L).toInt()
|
||||
}
|
||||
}
|
||||
571
app/src/main/java/com/termux/terminal/WcWidth.kt
Normal file
571
app/src/main/java/com/termux/terminal/WcWidth.kt
Normal file
@@ -0,0 +1,571 @@
|
||||
package com.termux.terminal
|
||||
|
||||
/**
|
||||
* Implementation of wcwidth(3) for Unicode 15.
|
||||
*
|
||||
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
|
||||
*
|
||||
* IMPORTANT:
|
||||
* Must be kept in sync with the following:
|
||||
* https://github.com/termux/wcwidth
|
||||
* https://github.com/termux/libandroid-support
|
||||
* https://github.com/termux/termux-packages/tree/master/packages/libandroid-support
|
||||
*/
|
||||
object WcWidth {
|
||||
|
||||
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private val ZERO_WIDTH = arrayOf(
|
||||
|
||||
intArrayOf(0x00300, 0x0036f), // Combining Grave Accent ..Combining Latin Small Le
|
||||
intArrayOf(0x00483, 0x00489), // Combining Cyrillic Titlo..Combining Cyrillic Milli
|
||||
intArrayOf(0x00591, 0x005bd), // Hebrew Accent Etnahta ..Hebrew Point Meteg
|
||||
intArrayOf(0x005bf, 0x005bf), // Hebrew Point Rafe ..Hebrew Point Rafe
|
||||
intArrayOf(0x005c1, 0x005c2), // Hebrew Point Shin Dot ..Hebrew Point Sin Dot
|
||||
intArrayOf(0x005c4, 0x005c5), // Hebrew Mark Upper Dot ..Hebrew Mark Lower Dot
|
||||
intArrayOf(0x005c7, 0x005c7), // Hebrew Point Qamats Qata..Hebrew Point Qamats Qata
|
||||
intArrayOf(0x00610, 0x0061a), // Arabic Sign Sallallahou ..Arabic Small Kasra
|
||||
intArrayOf(0x0064b, 0x0065f), // Arabic Fathatan ..Arabic Wavy Hamza Below
|
||||
intArrayOf(0x00670, 0x00670), // Arabic Letter Superscrip..Arabic Letter Superscrip
|
||||
intArrayOf(0x006d6, 0x006dc), // Arabic Small High Ligatu..Arabic Small High Seen
|
||||
intArrayOf(0x006df, 0x006e4), // Arabic Small High Rounde..Arabic Small High Madda
|
||||
intArrayOf(0x006e7, 0x006e8), // Arabic Small High Yeh ..Arabic Small High Noon
|
||||
intArrayOf(0x006ea, 0x006ed), // Arabic Empty Centre Low ..Arabic Small Low Meem
|
||||
intArrayOf(0x00711, 0x00711), // Syriac Letter Superscrip..Syriac Letter Superscrip
|
||||
intArrayOf(0x00730, 0x0074a), // Syriac Pthaha Above ..Syriac Barrekh
|
||||
intArrayOf(0x007a6, 0x007b0), // Thaana Abafili ..Thaana Sukun
|
||||
intArrayOf(0x007eb, 0x007f3), // Nko Combining Short High..Nko Combining Double Dot
|
||||
intArrayOf(0x007fd, 0x007fd), // Nko Dantayalan ..Nko Dantayalan
|
||||
intArrayOf(0x00816, 0x00819), // Samaritan Mark In ..Samaritan Mark Dagesh
|
||||
intArrayOf(0x0081b, 0x00823), // Samaritan Mark Epentheti..Samaritan Vowel Sign A
|
||||
intArrayOf(0x00825, 0x00827), // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
|
||||
intArrayOf(0x00829, 0x0082d), // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
|
||||
intArrayOf(0x00859, 0x0085b), // Mandaic Affrication Mark..Mandaic Gemination Mark
|
||||
intArrayOf(0x00898, 0x0089f), // Arabic Small High Word A..Arabic Half Madda Over M
|
||||
intArrayOf(0x008ca, 0x008e1), // Arabic Small High Farsi ..Arabic Small High Sign S
|
||||
intArrayOf(0x008e3, 0x00902), // Arabic Turned Damma Belo..Devanagari Sign Anusvara
|
||||
intArrayOf(0x0093a, 0x0093a), // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
|
||||
intArrayOf(0x0093c, 0x0093c), // Devanagari Sign Nukta ..Devanagari Sign Nukta
|
||||
intArrayOf(0x00941, 0x00948), // Devanagari Vowel Sign U ..Devanagari Vowel Sign Ai
|
||||
intArrayOf(0x0094d, 0x0094d), // Devanagari Sign Virama ..Devanagari Sign Virama
|
||||
intArrayOf(0x00951, 0x00957), // Devanagari Stress Sign U..Devanagari Vowel Sign Uu
|
||||
intArrayOf(0x00962, 0x00963), // Devanagari Vowel Sign Vo..Devanagari Vowel Sign Vo
|
||||
intArrayOf(0x00981, 0x00981), // Bengali Sign Candrabindu..Bengali Sign Candrabindu
|
||||
intArrayOf(0x009bc, 0x009bc), // Bengali Sign Nukta ..Bengali Sign Nukta
|
||||
intArrayOf(0x009c1, 0x009c4), // Bengali Vowel Sign U ..Bengali Vowel Sign Vocal
|
||||
intArrayOf(0x009cd, 0x009cd), // Bengali Sign Virama ..Bengali Sign Virama
|
||||
intArrayOf(0x009e2, 0x009e3), // Bengali Vowel Sign Vocal..Bengali Vowel Sign Vocal
|
||||
intArrayOf(0x009fe, 0x009fe), // Bengali Sandhi Mark ..Bengali Sandhi Mark
|
||||
intArrayOf(0x00a01, 0x00a02), // Gurmukhi Sign Adak Bindi..Gurmukhi Sign Bindi
|
||||
intArrayOf(0x00a3c, 0x00a3c), // Gurmukhi Sign Nukta ..Gurmukhi Sign Nukta
|
||||
intArrayOf(0x00a41, 0x00a42), // Gurmukhi Vowel Sign U ..Gurmukhi Vowel Sign Uu
|
||||
intArrayOf(0x00a47, 0x00a48), // Gurmukhi Vowel Sign Ee ..Gurmukhi Vowel Sign Ai
|
||||
intArrayOf(0x00a4b, 0x00a4d), // Gurmukhi Vowel Sign Oo ..Gurmukhi Sign Virama
|
||||
intArrayOf(0x00a51, 0x00a51), // Gurmukhi Sign Udaat ..Gurmukhi Sign Udaat
|
||||
intArrayOf(0x00a70, 0x00a71), // Gurmukhi Tippi ..Gurmukhi Addak
|
||||
intArrayOf(0x00a75, 0x00a75), // Gurmukhi Sign Yakash ..Gurmukhi Sign Yakash
|
||||
intArrayOf(0x00a81, 0x00a82), // Gujarati Sign Candrabind..Gujarati Sign Anusvara
|
||||
intArrayOf(0x00abc, 0x00abc), // Gujarati Sign Nukta ..Gujarati Sign Nukta
|
||||
intArrayOf(0x00ac1, 0x00ac5), // Gujarati Vowel Sign U ..Gujarati Vowel Sign Cand
|
||||
intArrayOf(0x00ac7, 0x00ac8), // Gujarati Vowel Sign E ..Gujarati Vowel Sign Ai
|
||||
intArrayOf(0x00acd, 0x00acd), // Gujarati Sign Virama ..Gujarati Sign Virama
|
||||
intArrayOf(0x00ae2, 0x00ae3), // Gujarati Vowel Sign Voca..Gujarati Vowel Sign Voca
|
||||
intArrayOf(0x00afa, 0x00aff), // Gujarati Sign Sukun ..Gujarati Sign Two-circle
|
||||
intArrayOf(0x00b01, 0x00b01), // Oriya Sign Candrabindu ..Oriya Sign Candrabindu
|
||||
intArrayOf(0x00b3c, 0x00b3c), // Oriya Sign Nukta ..Oriya Sign Nukta
|
||||
intArrayOf(0x00b3f, 0x00b3f), // Oriya Vowel Sign I ..Oriya Vowel Sign I
|
||||
intArrayOf(0x00b41, 0x00b44), // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
|
||||
intArrayOf(0x00b4d, 0x00b4d), // Oriya Sign Virama ..Oriya Sign Virama
|
||||
intArrayOf(0x00b55, 0x00b56), // Oriya Sign Overline ..Oriya Ai Length Mark
|
||||
intArrayOf(0x00b62, 0x00b63), // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
|
||||
intArrayOf(0x00b82, 0x00b82), // Tamil Sign Anusvara ..Tamil Sign Anusvara
|
||||
intArrayOf(0x00bc0, 0x00bc0), // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
|
||||
intArrayOf(0x00bcd, 0x00bcd), // Tamil Sign Virama ..Tamil Sign Virama
|
||||
intArrayOf(0x00c00, 0x00c00), // Telugu Sign Combining Ca..Telugu Sign Combining Ca
|
||||
intArrayOf(0x00c04, 0x00c04), // Telugu Sign Combining An..Telugu Sign Combining An
|
||||
intArrayOf(0x00c3c, 0x00c3c), // Telugu Sign Nukta ..Telugu Sign Nukta
|
||||
intArrayOf(0x00c3e, 0x00c40), // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
|
||||
intArrayOf(0x00c46, 0x00c48), // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
|
||||
intArrayOf(0x00c4a, 0x00c4d), // Telugu Vowel Sign O ..Telugu Sign Virama
|
||||
intArrayOf(0x00c55, 0x00c56), // Telugu Length Mark ..Telugu Ai Length Mark
|
||||
intArrayOf(0x00c62, 0x00c63), // Telugu Vowel Sign Vocali..Telugu Vowel Sign Vocali
|
||||
intArrayOf(0x00c81, 0x00c81), // Kannada Sign Candrabindu..Kannada Sign Candrabindu
|
||||
intArrayOf(0x00cbc, 0x00cbc), // Kannada Sign Nukta ..Kannada Sign Nukta
|
||||
intArrayOf(0x00cbf, 0x00cbf), // Kannada Vowel Sign I ..Kannada Vowel Sign I
|
||||
intArrayOf(0x00cc6, 0x00cc6), // Kannada Vowel Sign E ..Kannada Vowel Sign E
|
||||
intArrayOf(0x00ccc, 0x00ccd), // Kannada Vowel Sign Au ..Kannada Sign Virama
|
||||
intArrayOf(0x00ce2, 0x00ce3), // Kannada Vowel Sign Vocal..Kannada Vowel Sign Vocal
|
||||
intArrayOf(0x00d00, 0x00d01), // Malayalam Sign Combining..Malayalam Sign Candrabin
|
||||
intArrayOf(0x00d3b, 0x00d3c), // Malayalam Sign Vertical ..Malayalam Sign Circular
|
||||
intArrayOf(0x00d41, 0x00d44), // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
|
||||
intArrayOf(0x00d4d, 0x00d4d), // Malayalam Sign Virama ..Malayalam Sign Virama
|
||||
intArrayOf(0x00d62, 0x00d63), // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
|
||||
intArrayOf(0x00d81, 0x00d81), // Sinhala Sign Candrabindu..Sinhala Sign Candrabindu
|
||||
intArrayOf(0x00dca, 0x00dca), // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
|
||||
intArrayOf(0x00dd2, 0x00dd4), // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
|
||||
intArrayOf(0x00dd6, 0x00dd6), // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
|
||||
intArrayOf(0x00e31, 0x00e31), // Thai Character Mai Han-a..Thai Character Mai Han-a
|
||||
intArrayOf(0x00e34, 0x00e3a), // Thai Character Sara I ..Thai Character Phinthu
|
||||
intArrayOf(0x00e47, 0x00e4e), // Thai Character Maitaikhu..Thai Character Yamakkan
|
||||
intArrayOf(0x00eb1, 0x00eb1), // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
|
||||
intArrayOf(0x00eb4, 0x00ebc), // Lao Vowel Sign I ..Lao Semivowel Sign Lo
|
||||
intArrayOf(0x00ec8, 0x00ece), // Lao Tone Mai Ek ..(nil)
|
||||
intArrayOf(0x00f18, 0x00f19), // Tibetan Astrological Sig..Tibetan Astrological Sig
|
||||
intArrayOf(0x00f35, 0x00f35), // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
intArrayOf(0x00f37, 0x00f37), // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
intArrayOf(0x00f39, 0x00f39), // Tibetan Mark Tsa -phru ..Tibetan Mark Tsa -phru
|
||||
intArrayOf(0x00f71, 0x00f7e), // Tibetan Vowel Sign Aa ..Tibetan Sign Rjes Su Nga
|
||||
intArrayOf(0x00f80, 0x00f84), // Tibetan Vowel Sign Rever..Tibetan Mark Halanta
|
||||
intArrayOf(0x00f86, 0x00f87), // Tibetan Sign Lci Rtags ..Tibetan Sign Yang Rtags
|
||||
intArrayOf(0x00f8d, 0x00f97), // Tibetan Subjoined Sign L..Tibetan Subjoined Letter
|
||||
intArrayOf(0x00f99, 0x00fbc), // Tibetan Subjoined Letter..Tibetan Subjoined Letter
|
||||
intArrayOf(0x00fc6, 0x00fc6), // Tibetan Symbol Padma Gda..Tibetan Symbol Padma Gda
|
||||
intArrayOf(0x0102d, 0x01030), // Myanmar Vowel Sign I ..Myanmar Vowel Sign Uu
|
||||
intArrayOf(0x01032, 0x01037), // Myanmar Vowel Sign Ai ..Myanmar Sign Dot Below
|
||||
intArrayOf(0x01039, 0x0103a), // Myanmar Sign Virama ..Myanmar Sign Asat
|
||||
intArrayOf(0x0103d, 0x0103e), // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||
intArrayOf(0x01058, 0x01059), // Myanmar Vowel Sign Vocal..Myanmar Vowel Sign Vocal
|
||||
intArrayOf(0x0105e, 0x01060), // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||
intArrayOf(0x01071, 0x01074), // Myanmar Vowel Sign Geba ..Myanmar Vowel Sign Kayah
|
||||
intArrayOf(0x01082, 0x01082), // Myanmar Consonant Sign S..Myanmar Consonant Sign S
|
||||
intArrayOf(0x01085, 0x01086), // Myanmar Vowel Sign Shan ..Myanmar Vowel Sign Shan
|
||||
intArrayOf(0x0108d, 0x0108d), // Myanmar Sign Shan Counci..Myanmar Sign Shan Counci
|
||||
intArrayOf(0x0109d, 0x0109d), // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
|
||||
intArrayOf(0x0135d, 0x0135f), // Ethiopic Combining Gemin..Ethiopic Combining Gemin
|
||||
intArrayOf(0x01712, 0x01714), // Tagalog Vowel Sign I ..Tagalog Sign Virama
|
||||
intArrayOf(0x01732, 0x01733), // Hanunoo Vowel Sign I ..Hanunoo Vowel Sign U
|
||||
intArrayOf(0x01752, 0x01753), // Buhid Vowel Sign I ..Buhid Vowel Sign U
|
||||
intArrayOf(0x01772, 0x01773), // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
|
||||
intArrayOf(0x017b4, 0x017b5), // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
|
||||
intArrayOf(0x017b7, 0x017bd), // Khmer Vowel Sign I ..Khmer Vowel Sign Ua
|
||||
intArrayOf(0x017c6, 0x017c6), // Khmer Sign Nikahit ..Khmer Sign Nikahit
|
||||
intArrayOf(0x017c9, 0x017d3), // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
|
||||
intArrayOf(0x017dd, 0x017dd), // Khmer Sign Atthacan ..Khmer Sign Atthacan
|
||||
intArrayOf(0x0180b, 0x0180d), // Mongolian Free Variation..Mongolian Free Variation
|
||||
intArrayOf(0x0180f, 0x0180f), // Mongolian Free Variation..Mongolian Free Variation
|
||||
intArrayOf(0x01885, 0x01886), // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
intArrayOf(0x018a9, 0x018a9), // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
intArrayOf(0x01920, 0x01922), // Limbu Vowel Sign A ..Limbu Vowel Sign U
|
||||
intArrayOf(0x01927, 0x01928), // Limbu Vowel Sign E ..Limbu Vowel Sign O
|
||||
intArrayOf(0x01932, 0x01932), // Limbu Small Letter Anusv..Limbu Small Letter Anusv
|
||||
intArrayOf(0x01939, 0x0193b), // Limbu Sign Mukphreng ..Limbu Sign Sa-i
|
||||
intArrayOf(0x01a17, 0x01a18), // Buginese Vowel Sign I ..Buginese Vowel Sign U
|
||||
intArrayOf(0x01a1b, 0x01a1b), // Buginese Vowel Sign Ae ..Buginese Vowel Sign Ae
|
||||
intArrayOf(0x01a56, 0x01a56), // Tai Tham Consonant Sign ..Tai Tham Consonant Sign
|
||||
intArrayOf(0x01a58, 0x01a5e), // Tai Tham Sign Mai Kang L..Tai Tham Consonant Sign
|
||||
intArrayOf(0x01a60, 0x01a60), // Tai Tham Sign Sakot ..Tai Tham Sign Sakot
|
||||
intArrayOf(0x01a62, 0x01a62), // Tai Tham Vowel Sign Mai ..Tai Tham Vowel Sign Mai
|
||||
intArrayOf(0x01a65, 0x01a6c), // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
|
||||
intArrayOf(0x01a73, 0x01a7c), // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
|
||||
intArrayOf(0x01a7f, 0x01a7f), // Tai Tham Combining Crypt..Tai Tham Combining Crypt
|
||||
intArrayOf(0x01ab0, 0x01ace), // Combining Doubled Circum..Combining Latin Small Le
|
||||
intArrayOf(0x01b00, 0x01b03), // Balinese Sign Ulu Ricem ..Balinese Sign Surang
|
||||
intArrayOf(0x01b34, 0x01b34), // Balinese Sign Rerekan ..Balinese Sign Rerekan
|
||||
intArrayOf(0x01b36, 0x01b3a), // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
|
||||
intArrayOf(0x01b3c, 0x01b3c), // Balinese Vowel Sign La L..Balinese Vowel Sign La L
|
||||
intArrayOf(0x01b42, 0x01b42), // Balinese Vowel Sign Pepe..Balinese Vowel Sign Pepe
|
||||
intArrayOf(0x01b6b, 0x01b73), // Balinese Musical Symbol ..Balinese Musical Symbol
|
||||
intArrayOf(0x01b80, 0x01b81), // Sundanese Sign Panyecek ..Sundanese Sign Panglayar
|
||||
intArrayOf(0x01ba2, 0x01ba5), // Sundanese Consonant Sign..Sundanese Vowel Sign Pan
|
||||
intArrayOf(0x01ba8, 0x01ba9), // Sundanese Vowel Sign Pam..Sundanese Vowel Sign Pan
|
||||
intArrayOf(0x01bab, 0x01bad), // Sundanese Sign Virama ..Sundanese Consonant Sign
|
||||
intArrayOf(0x01be6, 0x01be6), // Batak Sign Tompi ..Batak Sign Tompi
|
||||
intArrayOf(0x01be8, 0x01be9), // Batak Vowel Sign Pakpak ..Batak Vowel Sign Ee
|
||||
intArrayOf(0x01bed, 0x01bed), // Batak Vowel Sign Karo O ..Batak Vowel Sign Karo O
|
||||
intArrayOf(0x01bef, 0x01bf1), // Batak Vowel Sign U For S..Batak Consonant Sign H
|
||||
intArrayOf(0x01c2c, 0x01c33), // Lepcha Vowel Sign E ..Lepcha Consonant Sign T
|
||||
intArrayOf(0x01c36, 0x01c37), // Lepcha Sign Ran ..Lepcha Sign Nukta
|
||||
intArrayOf(0x01cd0, 0x01cd2), // Vedic Tone Karshana ..Vedic Tone Prenkha
|
||||
intArrayOf(0x01cd4, 0x01ce0), // Vedic Sign Yajurvedic Mi..Vedic Tone Rigvedic Kash
|
||||
intArrayOf(0x01ce2, 0x01ce8), // Vedic Sign Visarga Svari..Vedic Sign Visarga Anuda
|
||||
intArrayOf(0x01ced, 0x01ced), // Vedic Sign Tiryak ..Vedic Sign Tiryak
|
||||
intArrayOf(0x01cf4, 0x01cf4), // Vedic Tone Candra Above ..Vedic Tone Candra Above
|
||||
intArrayOf(0x01cf8, 0x01cf9), // Vedic Tone Ring Above ..Vedic Tone Double Ring A
|
||||
intArrayOf(0x01dc0, 0x01dff), // Combining Dotted Grave A..Combining Right Arrowhea
|
||||
intArrayOf(0x020d0, 0x020f0), // Combining Left Harpoon A..Combining Asterisk Above
|
||||
intArrayOf(0x02cef, 0x02cf1), // Coptic Combining Ni Abov..Coptic Combining Spiritu
|
||||
intArrayOf(0x02d7f, 0x02d7f), // Tifinagh Consonant Joine..Tifinagh Consonant Joine
|
||||
intArrayOf(0x02de0, 0x02dff), // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||
intArrayOf(0x0302a, 0x0302d), // Ideographic Level Tone M..Ideographic Entering Ton
|
||||
intArrayOf(0x03099, 0x0309a), // Combining Katakana-hirag..Combining Katakana-hirag
|
||||
intArrayOf(0x0a66f, 0x0a672), // Combining Cyrillic Vzmet..Combining Cyrillic Thous
|
||||
intArrayOf(0x0a674, 0x0a67d), // Combining Cyrillic Lette..Combining Cyrillic Payer
|
||||
intArrayOf(0x0a69e, 0x0a69f), // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||
intArrayOf(0x0a6f0, 0x0a6f1), // Bamum Combining Mark Koq..Bamum Combining Mark Tuk
|
||||
intArrayOf(0x0a802, 0x0a802), // Syloti Nagri Sign Dvisva..Syloti Nagri Sign Dvisva
|
||||
intArrayOf(0x0a806, 0x0a806), // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
|
||||
intArrayOf(0x0a80b, 0x0a80b), // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
|
||||
intArrayOf(0x0a825, 0x0a826), // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
|
||||
intArrayOf(0x0a82c, 0x0a82c), // Syloti Nagri Sign Altern..Syloti Nagri Sign Altern
|
||||
intArrayOf(0x0a8c4, 0x0a8c5), // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
|
||||
intArrayOf(0x0a8e0, 0x0a8f1), // Combining Devanagari Dig..Combining Devanagari Sig
|
||||
intArrayOf(0x0a8ff, 0x0a8ff), // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
|
||||
intArrayOf(0x0a926, 0x0a92d), // Kayah Li Vowel Ue ..Kayah Li Tone Calya Plop
|
||||
intArrayOf(0x0a947, 0x0a951), // Rejang Vowel Sign I ..Rejang Consonant Sign R
|
||||
intArrayOf(0x0a980, 0x0a982), // Javanese Sign Panyangga ..Javanese Sign Layar
|
||||
intArrayOf(0x0a9b3, 0x0a9b3), // Javanese Sign Cecak Telu..Javanese Sign Cecak Telu
|
||||
intArrayOf(0x0a9b6, 0x0a9b9), // Javanese Vowel Sign Wulu..Javanese Vowel Sign Suku
|
||||
intArrayOf(0x0a9bc, 0x0a9bd), // Javanese Vowel Sign Pepe..Javanese Consonant Sign
|
||||
intArrayOf(0x0a9e5, 0x0a9e5), // Myanmar Sign Shan Saw ..Myanmar Sign Shan Saw
|
||||
intArrayOf(0x0aa29, 0x0aa2e), // Cham Vowel Sign Aa ..Cham Vowel Sign Oe
|
||||
intArrayOf(0x0aa31, 0x0aa32), // Cham Vowel Sign Au ..Cham Vowel Sign Ue
|
||||
intArrayOf(0x0aa35, 0x0aa36), // Cham Consonant Sign La ..Cham Consonant Sign Wa
|
||||
intArrayOf(0x0aa43, 0x0aa43), // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||
intArrayOf(0x0aa4c, 0x0aa4c), // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||
intArrayOf(0x0aa7c, 0x0aa7c), // Myanmar Sign Tai Laing T..Myanmar Sign Tai Laing T
|
||||
intArrayOf(0x0aab0, 0x0aab0), // Tai Viet Mai Kang ..Tai Viet Mai Kang
|
||||
intArrayOf(0x0aab2, 0x0aab4), // Tai Viet Vowel I ..Tai Viet Vowel U
|
||||
intArrayOf(0x0aab7, 0x0aab8), // Tai Viet Mai Khit ..Tai Viet Vowel Ia
|
||||
intArrayOf(0x0aabe, 0x0aabf), // Tai Viet Vowel Am ..Tai Viet Tone Mai Ek
|
||||
intArrayOf(0x0aac1, 0x0aac1), // Tai Viet Tone Mai Tho ..Tai Viet Tone Mai Tho
|
||||
intArrayOf(0x0aaec, 0x0aaed), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0aaf6, 0x0aaf6), // Meetei Mayek Virama ..Meetei Mayek Virama
|
||||
intArrayOf(0x0abe5, 0x0abe5), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0abe8, 0x0abe8), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0abed, 0x0abed), // Meetei Mayek Apun Iyek ..Meetei Mayek Apun Iyek
|
||||
intArrayOf(0x0fb1e, 0x0fb1e), // Hebrew Point Judeo-spani..Hebrew Point Judeo-spani
|
||||
intArrayOf(0x0fe00, 0x0fe0f), // Variation Selector-1 ..Variation Selector-16
|
||||
intArrayOf(0x0fe20, 0x0fe2f), // Combining Ligature Left ..Combining Cyrillic Titlo
|
||||
intArrayOf(0x101fd, 0x101fd), // Phaistos Disc Sign Combi..Phaistos Disc Sign Combi
|
||||
intArrayOf(0x102e0, 0x102e0), // Coptic Epact Thousands M..Coptic Epact Thousands M
|
||||
intArrayOf(0x10376, 0x1037a), // Combining Old Permic Let..Combining Old Permic Let
|
||||
intArrayOf(0x10a01, 0x10a03), // Kharoshthi Vowel Sign I ..Kharoshthi Vowel Sign Vo
|
||||
intArrayOf(0x10a05, 0x10a06), // Kharoshthi Vowel Sign E ..Kharoshthi Vowel Sign O
|
||||
intArrayOf(0x10a0c, 0x10a0f), // Kharoshthi Vowel Length ..Kharoshthi Sign Visarga
|
||||
intArrayOf(0x10a38, 0x10a3a), // Kharoshthi Sign Bar Abov..Kharoshthi Sign Dot Belo
|
||||
intArrayOf(0x10a3f, 0x10a3f), // Kharoshthi Virama ..Kharoshthi Virama
|
||||
intArrayOf(0x10ae5, 0x10ae6), // Manichaean Abbreviation ..Manichaean Abbreviation
|
||||
intArrayOf(0x10d24, 0x10d27), // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
|
||||
intArrayOf(0x10eab, 0x10eac), // Yezidi Combining Hamza M..Yezidi Combining Madda M
|
||||
intArrayOf(0x10efd, 0x10eff), // (nil) ..(nil)
|
||||
intArrayOf(0x10f46, 0x10f50), // Sogdian Combining Dot Be..Sogdian Combining Stroke
|
||||
intArrayOf(0x10f82, 0x10f85), // Old Uyghur Combining Dot..Old Uyghur Combining Two
|
||||
intArrayOf(0x11001, 0x11001), // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
|
||||
intArrayOf(0x11038, 0x11046), // Brahmi Vowel Sign Aa ..Brahmi Virama
|
||||
intArrayOf(0x11070, 0x11070), // Brahmi Sign Old Tamil Vi..Brahmi Sign Old Tamil Vi
|
||||
intArrayOf(0x11073, 0x11074), // Brahmi Vowel Sign Old Ta..Brahmi Vowel Sign Old Ta
|
||||
intArrayOf(0x1107f, 0x11081), // Brahmi Number Joiner ..Kaithi Sign Anusvara
|
||||
intArrayOf(0x110b3, 0x110b6), // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
|
||||
intArrayOf(0x110b9, 0x110ba), // Kaithi Sign Virama ..Kaithi Sign Nukta
|
||||
intArrayOf(0x110c2, 0x110c2), // Kaithi Vowel Sign Vocali..Kaithi Vowel Sign Vocali
|
||||
intArrayOf(0x11100, 0x11102), // Chakma Sign Candrabindu ..Chakma Sign Visarga
|
||||
intArrayOf(0x11127, 0x1112b), // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
|
||||
intArrayOf(0x1112d, 0x11134), // Chakma Vowel Sign Ai ..Chakma Maayyaa
|
||||
intArrayOf(0x11173, 0x11173), // Mahajani Sign Nukta ..Mahajani Sign Nukta
|
||||
intArrayOf(0x11180, 0x11181), // Sharada Sign Candrabindu..Sharada Sign Anusvara
|
||||
intArrayOf(0x111b6, 0x111be), // Sharada Vowel Sign U ..Sharada Vowel Sign O
|
||||
intArrayOf(0x111c9, 0x111cc), // Sharada Sandhi Mark ..Sharada Extra Short Vowe
|
||||
intArrayOf(0x111cf, 0x111cf), // Sharada Sign Inverted Ca..Sharada Sign Inverted Ca
|
||||
intArrayOf(0x1122f, 0x11231), // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
|
||||
intArrayOf(0x11234, 0x11234), // Khojki Sign Anusvara ..Khojki Sign Anusvara
|
||||
intArrayOf(0x11236, 0x11237), // Khojki Sign Nukta ..Khojki Sign Shadda
|
||||
intArrayOf(0x1123e, 0x1123e), // Khojki Sign Sukun ..Khojki Sign Sukun
|
||||
intArrayOf(0x11241, 0x11241), // (nil) ..(nil)
|
||||
intArrayOf(0x112df, 0x112df), // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
|
||||
intArrayOf(0x112e3, 0x112ea), // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
|
||||
intArrayOf(0x11300, 0x11301), // Grantha Sign Combining A..Grantha Sign Candrabindu
|
||||
intArrayOf(0x1133b, 0x1133c), // Combining Bindu Below ..Grantha Sign Nukta
|
||||
intArrayOf(0x11340, 0x11340), // Grantha Vowel Sign Ii ..Grantha Vowel Sign Ii
|
||||
intArrayOf(0x11366, 0x1136c), // Combining Grantha Digit ..Combining Grantha Digit
|
||||
intArrayOf(0x11370, 0x11374), // Combining Grantha Letter..Combining Grantha Letter
|
||||
intArrayOf(0x11438, 0x1143f), // Newa Vowel Sign U ..Newa Vowel Sign Ai
|
||||
intArrayOf(0x11442, 0x11444), // Newa Sign Virama ..Newa Sign Anusvara
|
||||
intArrayOf(0x11446, 0x11446), // Newa Sign Nukta ..Newa Sign Nukta
|
||||
intArrayOf(0x1145e, 0x1145e), // Newa Sandhi Mark ..Newa Sandhi Mark
|
||||
intArrayOf(0x114b3, 0x114b8), // Tirhuta Vowel Sign U ..Tirhuta Vowel Sign Vocal
|
||||
intArrayOf(0x114ba, 0x114ba), // Tirhuta Vowel Sign Short..Tirhuta Vowel Sign Short
|
||||
intArrayOf(0x114bf, 0x114c0), // Tirhuta Sign Candrabindu..Tirhuta Sign Anusvara
|
||||
intArrayOf(0x114c2, 0x114c3), // Tirhuta Sign Virama ..Tirhuta Sign Nukta
|
||||
intArrayOf(0x115b2, 0x115b5), // Siddham Vowel Sign U ..Siddham Vowel Sign Vocal
|
||||
intArrayOf(0x115bc, 0x115bd), // Siddham Sign Candrabindu..Siddham Sign Anusvara
|
||||
intArrayOf(0x115bf, 0x115c0), // Siddham Sign Virama ..Siddham Sign Nukta
|
||||
intArrayOf(0x115dc, 0x115dd), // Siddham Vowel Sign Alter..Siddham Vowel Sign Alter
|
||||
intArrayOf(0x11633, 0x1163a), // Modi Vowel Sign U ..Modi Vowel Sign Ai
|
||||
intArrayOf(0x1163d, 0x1163d), // Modi Sign Anusvara ..Modi Sign Anusvara
|
||||
intArrayOf(0x1163f, 0x11640), // Modi Sign Virama ..Modi Sign Ardhacandra
|
||||
intArrayOf(0x116ab, 0x116ab), // Takri Sign Anusvara ..Takri Sign Anusvara
|
||||
intArrayOf(0x116ad, 0x116ad), // Takri Vowel Sign Aa ..Takri Vowel Sign Aa
|
||||
intArrayOf(0x116b0, 0x116b5), // Takri Vowel Sign U ..Takri Vowel Sign Au
|
||||
intArrayOf(0x116b7, 0x116b7), // Takri Sign Nukta ..Takri Sign Nukta
|
||||
intArrayOf(0x1171d, 0x1171f), // Ahom Consonant Sign Medi..Ahom Consonant Sign Medi
|
||||
intArrayOf(0x11722, 0x11725), // Ahom Vowel Sign I ..Ahom Vowel Sign Uu
|
||||
intArrayOf(0x11727, 0x1172b), // Ahom Vowel Sign Aw ..Ahom Sign Killer
|
||||
intArrayOf(0x1182f, 0x11837), // Dogra Vowel Sign U ..Dogra Sign Anusvara
|
||||
intArrayOf(0x11839, 0x1183a), // Dogra Sign Virama ..Dogra Sign Nukta
|
||||
intArrayOf(0x1193b, 0x1193c), // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
|
||||
intArrayOf(0x1193e, 0x1193e), // Dives Akuru Virama ..Dives Akuru Virama
|
||||
intArrayOf(0x11943, 0x11943), // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
|
||||
intArrayOf(0x119d4, 0x119d7), // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
|
||||
intArrayOf(0x119da, 0x119db), // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
|
||||
intArrayOf(0x119e0, 0x119e0), // Nandinagari Sign Virama ..Nandinagari Sign Virama
|
||||
intArrayOf(0x11a01, 0x11a0a), // Zanabazar Square Vowel S..Zanabazar Square Vowel L
|
||||
intArrayOf(0x11a33, 0x11a38), // Zanabazar Square Final C..Zanabazar Square Sign An
|
||||
intArrayOf(0x11a3b, 0x11a3e), // Zanabazar Square Cluster..Zanabazar Square Cluster
|
||||
intArrayOf(0x11a47, 0x11a47), // Zanabazar Square Subjoin..Zanabazar Square Subjoin
|
||||
intArrayOf(0x11a51, 0x11a56), // Soyombo Vowel Sign I ..Soyombo Vowel Sign Oe
|
||||
intArrayOf(0x11a59, 0x11a5b), // Soyombo Vowel Sign Vocal..Soyombo Vowel Length Mar
|
||||
intArrayOf(0x11a8a, 0x11a96), // Soyombo Final Consonant ..Soyombo Sign Anusvara
|
||||
intArrayOf(0x11a98, 0x11a99), // Soyombo Gemination Mark ..Soyombo Subjoiner
|
||||
intArrayOf(0x11c30, 0x11c36), // Bhaiksuki Vowel Sign I ..Bhaiksuki Vowel Sign Voc
|
||||
intArrayOf(0x11c38, 0x11c3d), // Bhaiksuki Vowel Sign E ..Bhaiksuki Sign Anusvara
|
||||
intArrayOf(0x11c3f, 0x11c3f), // Bhaiksuki Sign Virama ..Bhaiksuki Sign Virama
|
||||
intArrayOf(0x11c92, 0x11ca7), // Marchen Subjoined Letter..Marchen Subjoined Letter
|
||||
intArrayOf(0x11caa, 0x11cb0), // Marchen Subjoined Letter..Marchen Vowel Sign Aa
|
||||
intArrayOf(0x11cb2, 0x11cb3), // Marchen Vowel Sign U ..Marchen Vowel Sign E
|
||||
intArrayOf(0x11cb5, 0x11cb6), // Marchen Sign Anusvara ..Marchen Sign Candrabindu
|
||||
intArrayOf(0x11d31, 0x11d36), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3a, 0x11d3a), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3c, 0x11d3d), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3f, 0x11d45), // Masaram Gondi Vowel Sign..Masaram Gondi Virama
|
||||
intArrayOf(0x11d47, 0x11d47), // Masaram Gondi Ra-kara ..Masaram Gondi Ra-kara
|
||||
intArrayOf(0x11d90, 0x11d91), // Gunjala Gondi Vowel Sign..Gunjala Gondi Vowel Sign
|
||||
intArrayOf(0x11d95, 0x11d95), // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
|
||||
intArrayOf(0x11d97, 0x11d97), // Gunjala Gondi Virama ..Gunjala Gondi Virama
|
||||
intArrayOf(0x11ef3, 0x11ef4), // Makasar Vowel Sign I ..Makasar Vowel Sign U
|
||||
intArrayOf(0x11f00, 0x11f01), // (nil) ..(nil)
|
||||
intArrayOf(0x11f36, 0x11f3a), // (nil) ..(nil)
|
||||
intArrayOf(0x11f40, 0x11f40), // (nil) ..(nil)
|
||||
intArrayOf(0x11f42, 0x11f42), // (nil) ..(nil)
|
||||
intArrayOf(0x13440, 0x13440), // (nil) ..(nil)
|
||||
intArrayOf(0x13447, 0x13455), // (nil) ..(nil)
|
||||
intArrayOf(0x16af0, 0x16af4), // Bassa Vah Combining High..Bassa Vah Combining High
|
||||
intArrayOf(0x16b30, 0x16b36), // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
|
||||
intArrayOf(0x16f4f, 0x16f4f), // Miao Sign Consonant Modi..Miao Sign Consonant Modi
|
||||
intArrayOf(0x16f8f, 0x16f92), // Miao Tone Right ..Miao Tone Below
|
||||
intArrayOf(0x16fe4, 0x16fe4), // Khitan Small Script Fill..Khitan Small Script Fill
|
||||
intArrayOf(0x1bc9d, 0x1bc9e), // Duployan Thick Letter Se..Duployan Double Mark
|
||||
intArrayOf(0x1cf00, 0x1cf2d), // Znamenny Combining Mark ..Znamenny Combining Mark
|
||||
intArrayOf(0x1cf30, 0x1cf46), // Znamenny Combining Tonal..Znamenny Priznak Modifie
|
||||
intArrayOf(0x1d167, 0x1d169), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d17b, 0x1d182), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d185, 0x1d18b), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d1aa, 0x1d1ad), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d242, 0x1d244), // Combining Greek Musical ..Combining Greek Musical
|
||||
intArrayOf(0x1da00, 0x1da36), // Signwriting Head Rim ..Signwriting Air Sucking
|
||||
intArrayOf(0x1da3b, 0x1da6c), // Signwriting Mouth Closed..Signwriting Excitement
|
||||
intArrayOf(0x1da75, 0x1da75), // Signwriting Upper Body T..Signwriting Upper Body T
|
||||
intArrayOf(0x1da84, 0x1da84), // Signwriting Location Hea..Signwriting Location Hea
|
||||
intArrayOf(0x1da9b, 0x1da9f), // Signwriting Fill Modifie..Signwriting Fill Modifie
|
||||
intArrayOf(0x1daa1, 0x1daaf), // Signwriting Rotation Mod..Signwriting Rotation Mod
|
||||
intArrayOf(0x1e000, 0x1e006), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e008, 0x1e018), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e01b, 0x1e021), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e023, 0x1e024), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e026, 0x1e02a), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e08f, 0x1e08f), // (nil) ..(nil)
|
||||
intArrayOf(0x1e130, 0x1e136), // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T
|
||||
intArrayOf(0x1e2ae, 0x1e2ae), // Toto Sign Rising Tone ..Toto Sign Rising Tone
|
||||
intArrayOf(0x1e2ec, 0x1e2ef), // Wancho Tone Tup ..Wancho Tone Koini
|
||||
intArrayOf(0x1e4ec, 0x1e4ef), // (nil) ..(nil)
|
||||
intArrayOf(0x1e8d0, 0x1e8d6), // Mende Kikakui Combining ..Mende Kikakui Combining
|
||||
intArrayOf(0x1e944, 0x1e94a), // Adlam Alif Lengthener ..Adlam Nukta
|
||||
intArrayOf(0xe0100, 0xe01ef), // Variation Selector-17 ..Variation Selector-256
|
||||
|
||||
)
|
||||
|
||||
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private val WIDE_EASTASIAN = arrayOf(
|
||||
|
||||
intArrayOf(0x01100, 0x0115f), // Hangul Choseong Kiyeok ..Hangul Choseong Filler
|
||||
intArrayOf(0x0231a, 0x0231b), // Watch ..Hourglass
|
||||
intArrayOf(0x02329, 0x0232a), // Left-pointing Angle Brac..Right-pointing Angle Bra
|
||||
intArrayOf(0x023e9, 0x023ec), // Black Right-pointing Dou..Black Down-pointing Doub
|
||||
intArrayOf(0x023f0, 0x023f0), // Alarm Clock ..Alarm Clock
|
||||
intArrayOf(0x023f3, 0x023f3), // Hourglass With Flowing S..Hourglass With Flowing S
|
||||
intArrayOf(0x025fd, 0x025fe), // White Medium Small Squar..Black Medium Small Squar
|
||||
intArrayOf(0x02614, 0x02615), // Umbrella With Rain Drops..Hot Beverage
|
||||
intArrayOf(0x02648, 0x02653), // Aries ..Pisces
|
||||
intArrayOf(0x0267f, 0x0267f), // Wheelchair Symbol ..Wheelchair Symbol
|
||||
intArrayOf(0x02693, 0x02693), // Anchor ..Anchor
|
||||
intArrayOf(0x026a1, 0x026a1), // High Voltage Sign ..High Voltage Sign
|
||||
intArrayOf(0x026aa, 0x026ab), // Medium White Circle ..Medium Black Circle
|
||||
intArrayOf(0x026bd, 0x026be), // Soccer Ball ..Baseball
|
||||
intArrayOf(0x026c4, 0x026c5), // Snowman Without Snow ..Sun Behind Cloud
|
||||
intArrayOf(0x026ce, 0x026ce), // Ophiuchus ..Ophiuchus
|
||||
intArrayOf(0x026d4, 0x026d4), // No Entry ..No Entry
|
||||
intArrayOf(0x026ea, 0x026ea), // Church ..Church
|
||||
intArrayOf(0x026f2, 0x026f3), // Fountain ..Flag In Hole
|
||||
intArrayOf(0x026f5, 0x026f5), // Sailboat ..Sailboat
|
||||
intArrayOf(0x026fa, 0x026fa), // Tent ..Tent
|
||||
intArrayOf(0x026fd, 0x026fd), // Fuel Pump ..Fuel Pump
|
||||
intArrayOf(0x02705, 0x02705), // White Heavy Check Mark ..White Heavy Check Mark
|
||||
intArrayOf(0x0270a, 0x0270b), // Raised Fist ..Raised Hand
|
||||
intArrayOf(0x02728, 0x02728), // Sparkles ..Sparkles
|
||||
intArrayOf(0x0274c, 0x0274c), // Cross Mark ..Cross Mark
|
||||
intArrayOf(0x0274e, 0x0274e), // Negative Squared Cross M..Negative Squared Cross M
|
||||
intArrayOf(0x02753, 0x02755), // Black Question Mark Orna..White Exclamation Mark O
|
||||
intArrayOf(0x02757, 0x02757), // Heavy Exclamation Mark S..Heavy Exclamation Mark S
|
||||
intArrayOf(0x02795, 0x02797), // Heavy Plus Sign ..Heavy Division Sign
|
||||
intArrayOf(0x027b0, 0x027b0), // Curly Loop ..Curly Loop
|
||||
intArrayOf(0x027bf, 0x027bf), // Double Curly Loop ..Double Curly Loop
|
||||
intArrayOf(0x02b1b, 0x02b1c), // Black Large Square ..White Large Square
|
||||
intArrayOf(0x02b50, 0x02b50), // White Medium Star ..White Medium Star
|
||||
intArrayOf(0x02b55, 0x02b55), // Heavy Large Circle ..Heavy Large Circle
|
||||
intArrayOf(0x02e80, 0x02e99), // Cjk Radical Repeat ..Cjk Radical Rap
|
||||
intArrayOf(0x02e9b, 0x02ef3), // Cjk Radical Choke ..Cjk Radical C-simplified
|
||||
intArrayOf(0x02f00, 0x02fd5), // Kangxi Radical One ..Kangxi Radical Flute
|
||||
intArrayOf(0x02ff0, 0x02ffb), // Ideographic Description ..Ideographic Description
|
||||
intArrayOf(0x03000, 0x0303e), // Ideographic Space ..Ideographic Variation In
|
||||
intArrayOf(0x03041, 0x03096), // Hiragana Letter Small A ..Hiragana Letter Small Ke
|
||||
intArrayOf(0x03099, 0x030ff), // Combining Katakana-hirag..Katakana Digraph Koto
|
||||
intArrayOf(0x03105, 0x0312f), // Bopomofo Letter B ..Bopomofo Letter Nn
|
||||
intArrayOf(0x03131, 0x0318e), // Hangul Letter Kiyeok ..Hangul Letter Araeae
|
||||
intArrayOf(0x03190, 0x031e3), // Ideographic Annotation L..Cjk Stroke Q
|
||||
intArrayOf(0x031f0, 0x0321e), // Katakana Letter Small Ku..Parenthesized Korean Cha
|
||||
intArrayOf(0x03220, 0x03247), // Parenthesized Ideograph ..Circled Ideograph Koto
|
||||
intArrayOf(0x03250, 0x04dbf), // Partnership Sign ..Cjk Unified Ideograph-4d
|
||||
intArrayOf(0x04e00, 0x0a48c), // Cjk Unified Ideograph-4e..Yi Syllable Yyr
|
||||
intArrayOf(0x0a490, 0x0a4c6), // Yi Radical Qot ..Yi Radical Ke
|
||||
intArrayOf(0x0a960, 0x0a97c), // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
|
||||
intArrayOf(0x0ac00, 0x0d7a3), // Hangul Syllable Ga ..Hangul Syllable Hih
|
||||
intArrayOf(0x0f900, 0x0faff), // Cjk Compatibility Ideogr..(nil)
|
||||
intArrayOf(0x0fe10, 0x0fe19), // Presentation Form For Ve..Presentation Form For Ve
|
||||
intArrayOf(0x0fe30, 0x0fe52), // Presentation Form For Ve..Small Full Stop
|
||||
intArrayOf(0x0fe54, 0x0fe66), // Small Semicolon ..Small Equals Sign
|
||||
intArrayOf(0x0fe68, 0x0fe6b), // Small Reverse Solidus ..Small Commercial At
|
||||
intArrayOf(0x0ff01, 0x0ff60), // Fullwidth Exclamation Ma..Fullwidth Right White Pa
|
||||
intArrayOf(0x0ffe0, 0x0ffe6), // Fullwidth Cent Sign ..Fullwidth Won Sign
|
||||
intArrayOf(0x16fe0, 0x16fe4), // Tangut Iteration Mark ..Khitan Small Script Fill
|
||||
intArrayOf(0x16ff0, 0x16ff1), // Vietnamese Alternate Rea..Vietnamese Alternate Rea
|
||||
intArrayOf(0x17000, 0x187f7), // (nil) ..(nil)
|
||||
intArrayOf(0x18800, 0x18cd5), // Tangut Component-001 ..Khitan Small Script Char
|
||||
intArrayOf(0x18d00, 0x18d08), // (nil) ..(nil)
|
||||
intArrayOf(0x1aff0, 0x1aff3), // Katakana Letter Minnan T..Katakana Letter Minnan T
|
||||
intArrayOf(0x1aff5, 0x1affb), // Katakana Letter Minnan T..Katakana Letter Minnan N
|
||||
intArrayOf(0x1affd, 0x1affe), // Katakana Letter Minnan N..Katakana Letter Minnan N
|
||||
intArrayOf(0x1b000, 0x1b122), // Katakana Letter Archaic ..Katakana Letter Archaic
|
||||
intArrayOf(0x1b132, 0x1b132), // (nil) ..(nil)
|
||||
intArrayOf(0x1b150, 0x1b152), // Hiragana Letter Small Wi..Hiragana Letter Small Wo
|
||||
intArrayOf(0x1b155, 0x1b155), // (nil) ..(nil)
|
||||
intArrayOf(0x1b164, 0x1b167), // Katakana Letter Small Wi..Katakana Letter Small N
|
||||
intArrayOf(0x1b170, 0x1b2fb), // Nushu Character-1b170 ..Nushu Character-1b2fb
|
||||
intArrayOf(0x1f004, 0x1f004), // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
|
||||
intArrayOf(0x1f0cf, 0x1f0cf), // Playing Card Black Joker..Playing Card Black Joker
|
||||
intArrayOf(0x1f18e, 0x1f18e), // Negative Squared Ab ..Negative Squared Ab
|
||||
intArrayOf(0x1f191, 0x1f19a), // Squared Cl ..Squared Vs
|
||||
intArrayOf(0x1f200, 0x1f202), // Square Hiragana Hoka ..Squared Katakana Sa
|
||||
intArrayOf(0x1f210, 0x1f23b), // Squared Cjk Unified Ideo..Squared Cjk Unified Ideo
|
||||
intArrayOf(0x1f240, 0x1f248), // Tortoise Shell Bracketed..Tortoise Shell Bracketed
|
||||
intArrayOf(0x1f250, 0x1f251), // Circled Ideograph Advant..Circled Ideograph Accept
|
||||
intArrayOf(0x1f260, 0x1f265), // Rounded Symbol For Fu ..Rounded Symbol For Cai
|
||||
intArrayOf(0x1f300, 0x1f320), // Cyclone ..Shooting Star
|
||||
intArrayOf(0x1f32d, 0x1f335), // Hot Dog ..Cactus
|
||||
intArrayOf(0x1f337, 0x1f37c), // Tulip ..Baby Bottle
|
||||
intArrayOf(0x1f37e, 0x1f393), // Bottle With Popping Cork..Graduation Cap
|
||||
intArrayOf(0x1f3a0, 0x1f3ca), // Carousel Horse ..Swimmer
|
||||
intArrayOf(0x1f3cf, 0x1f3d3), // Cricket Bat And Ball ..Table Tennis Paddle And
|
||||
intArrayOf(0x1f3e0, 0x1f3f0), // House Building ..European Castle
|
||||
intArrayOf(0x1f3f4, 0x1f3f4), // Waving Black Flag ..Waving Black Flag
|
||||
intArrayOf(0x1f3f8, 0x1f43e), // Badminton Racquet And Sh..Paw Prints
|
||||
intArrayOf(0x1f440, 0x1f440), // Eyes ..Eyes
|
||||
intArrayOf(0x1f442, 0x1f4fc), // Ear ..Videocassette
|
||||
intArrayOf(0x1f4ff, 0x1f53d), // Prayer Beads ..Down-pointing Small Red
|
||||
intArrayOf(0x1f54b, 0x1f54e), // Kaaba ..Menorah With Nine Branch
|
||||
intArrayOf(0x1f550, 0x1f567), // Clock Face One Oclock ..Clock Face Twelve-thirty
|
||||
intArrayOf(0x1f57a, 0x1f57a), // Man Dancing ..Man Dancing
|
||||
intArrayOf(0x1f595, 0x1f596), // Reversed Hand With Middl..Raised Hand With Part Be
|
||||
intArrayOf(0x1f5a4, 0x1f5a4), // Black Heart ..Black Heart
|
||||
intArrayOf(0x1f5fb, 0x1f64f), // Mount Fuji ..Person With Folded Hands
|
||||
intArrayOf(0x1f680, 0x1f6c5), // Rocket ..Left Luggage
|
||||
intArrayOf(0x1f6cc, 0x1f6cc), // Sleeping Accommodation ..Sleeping Accommodation
|
||||
intArrayOf(0x1f6d0, 0x1f6d2), // Place Of Worship ..Shopping Trolley
|
||||
intArrayOf(0x1f6d5, 0x1f6d7), // Hindu Temple ..Elevator
|
||||
intArrayOf(0x1f6dc, 0x1f6df), // (nil) ..Ring Buoy
|
||||
intArrayOf(0x1f6eb, 0x1f6ec), // Airplane Departure ..Airplane Arriving
|
||||
intArrayOf(0x1f6f4, 0x1f6fc), // Scooter ..Roller Skate
|
||||
intArrayOf(0x1f7e0, 0x1f7eb), // Large Orange Circle ..Large Brown Square
|
||||
intArrayOf(0x1f7f0, 0x1f7f0), // Heavy Equals Sign ..Heavy Equals Sign
|
||||
intArrayOf(0x1f90c, 0x1f93a), // Pinched Fingers ..Fencer
|
||||
intArrayOf(0x1f93c, 0x1f945), // Wrestlers ..Goal Net
|
||||
intArrayOf(0x1f947, 0x1f9ff), // First Place Medal ..Nazar Amulet
|
||||
intArrayOf(0x1fa70, 0x1fa7c), // Ballet Shoes ..Crutch
|
||||
intArrayOf(0x1fa80, 0x1fa88), // Yo-yo ..(nil)
|
||||
intArrayOf(0x1fa90, 0x1fabd), // Ringed Planet ..(nil)
|
||||
intArrayOf(0x1fabf, 0x1fac5), // (nil) ..Person With Crown
|
||||
intArrayOf(0x1face, 0x1fadb), // (nil) ..(nil)
|
||||
intArrayOf(0x1fae0, 0x1fae8), // Melting Face ..(nil)
|
||||
intArrayOf(0x1faf0, 0x1faf8), // Hand With Index Finger A..(nil)
|
||||
intArrayOf(0x20000, 0x2fffd), // Cjk Unified Ideograph-20..(nil)
|
||||
intArrayOf(0x30000, 0x3fffd), // Cjk Unified Ideograph-30..(nil)
|
||||
|
||||
)
|
||||
|
||||
private fun intable(table: Array<IntArray>, c: Int): Boolean {
|
||||
// First quick check for Latin1 etc. characters.
|
||||
if (c < table[0][0]) return false
|
||||
|
||||
// Binary search in table.
|
||||
var bot = 0
|
||||
var top = table.size - 1
|
||||
while (top >= bot) {
|
||||
val mid = (bot + top) / 2
|
||||
if (table[mid][1] < c) {
|
||||
bot = mid + 1
|
||||
} else if (table[mid][0] > c) {
|
||||
top = mid - 1
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Return the terminal display width of a code point: 0, 1 or 2. */
|
||||
@JvmStatic
|
||||
fun width(ucs: Int): Int {
|
||||
if (ucs == 0 ||
|
||||
ucs == 0x034F ||
|
||||
(ucs in 0x200B..0x200F) ||
|
||||
ucs == 0x2028 ||
|
||||
ucs == 0x2029 ||
|
||||
(ucs in 0x202A..0x202E) ||
|
||||
(ucs in 0x2060..0x2063)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// C0/C1 control characters
|
||||
// Termux change: Return 0 instead of -1.
|
||||
if (ucs < 32 || (ucs in 0x07F until 0x0A0)) return 0
|
||||
|
||||
// combining characters with zero width
|
||||
if (intable(ZERO_WIDTH, ucs)) return 0
|
||||
|
||||
return if (intable(WIDE_EASTASIAN, ucs)) 2 else 1
|
||||
}
|
||||
|
||||
/** The width at an index position in a java char array. */
|
||||
@JvmStatic
|
||||
fun width(chars: CharArray, index: Int): Int {
|
||||
val c = chars[index]
|
||||
return if (Character.isHighSurrogate(c)) width(Character.toCodePoint(c, chars[index + 1])) else width(c.code)
|
||||
}
|
||||
|
||||
/**
|
||||
* The zero width characters count like combining characters in the `chars` array from start
|
||||
* index to end index (exclusive).
|
||||
*/
|
||||
@JvmStatic
|
||||
fun zeroWidthCharsCount(chars: CharArray, start: Int, end: Int): Int {
|
||||
if (start < 0 || start >= chars.size) return 0
|
||||
|
||||
var count = 0
|
||||
var i = start
|
||||
while (i < end && i < chars.size) {
|
||||
if (Character.isHighSurrogate(chars[i])) {
|
||||
if (width(Character.toCodePoint(chars[i], chars[i + 1])) <= 0) {
|
||||
count++
|
||||
}
|
||||
i += 2
|
||||
} else {
|
||||
if (width(chars[i].code) <= 0) {
|
||||
count++
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.termux.view
|
||||
|
||||
import android.content.Context
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
|
||||
/** A combination of [GestureDetector] and [ScaleGestureDetector]. */
|
||||
internal class GestureAndScaleRecognizer(context: Context, val mListener: Listener) {
|
||||
|
||||
interface Listener {
|
||||
fun onSingleTapUp(e: MotionEvent): Boolean
|
||||
fun onDoubleTap(e: MotionEvent): Boolean
|
||||
fun onScroll(e2: MotionEvent, dx: Float, dy: Float): Boolean
|
||||
fun onFling(e: MotionEvent, velocityX: Float, velocityY: Float): Boolean
|
||||
fun onScale(focusX: Float, focusY: Float, scale: Float): Boolean
|
||||
fun onDown(x: Float, y: Float): Boolean
|
||||
fun onUp(e: MotionEvent): Boolean
|
||||
fun onLongPress(e: MotionEvent)
|
||||
}
|
||||
|
||||
private val mGestureDetector: GestureDetector
|
||||
private val mScaleDetector: ScaleGestureDetector
|
||||
var isAfterLongPress = false
|
||||
|
||||
init {
|
||||
mGestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dx: Float, dy: Float): Boolean {
|
||||
return mListener.onScroll(e2, dx, dy)
|
||||
}
|
||||
|
||||
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
|
||||
return mListener.onFling(e2, velocityX, velocityY)
|
||||
}
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
return mListener.onDown(e.x, e.y)
|
||||
}
|
||||
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
mListener.onLongPress(e)
|
||||
isAfterLongPress = true
|
||||
}
|
||||
}, null, true /* ignoreMultitouch */)
|
||||
|
||||
mGestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
return mListener.onSingleTapUp(e)
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
return mListener.onDoubleTap(e)
|
||||
}
|
||||
|
||||
override fun onDoubleTapEvent(e: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
mScaleDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
return mListener.onScale(detector.focusX, detector.focusY, detector.scaleFactor)
|
||||
}
|
||||
})
|
||||
mScaleDetector.isQuickScaleEnabled = false
|
||||
}
|
||||
|
||||
fun onTouchEvent(event: MotionEvent) {
|
||||
mGestureDetector.onTouchEvent(event)
|
||||
mScaleDetector.onTouchEvent(event)
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> isAfterLongPress = false
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (!isAfterLongPress) {
|
||||
// This behaviour is desired when in e.g. vim with mouse events, where we do not
|
||||
// want to move the cursor when lifting finger after a long press.
|
||||
mListener.onUp(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isInProgress(): Boolean {
|
||||
return mScaleDetector.isInProgress
|
||||
}
|
||||
}
|
||||
265
app/src/main/java/com/termux/view/TerminalRenderer.kt
Normal file
265
app/src/main/java/com/termux/view/TerminalRenderer.kt
Normal file
@@ -0,0 +1,265 @@
|
||||
package com.termux.view
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.Typeface
|
||||
import com.termux.terminal.TerminalBuffer
|
||||
import com.termux.terminal.TerminalEmulator
|
||||
import com.termux.terminal.TerminalRow
|
||||
import com.termux.terminal.TextStyle
|
||||
import com.termux.terminal.WcWidth
|
||||
|
||||
/**
|
||||
* Renderer of a [TerminalEmulator] into a [Canvas].
|
||||
*
|
||||
* Saves font metrics, so needs to be recreated each time the typeface or font size changes.
|
||||
*/
|
||||
class TerminalRenderer(
|
||||
@JvmField val mTextSize: Int,
|
||||
@JvmField val mTypeface: Typeface
|
||||
) {
|
||||
private val mTextPaint = Paint()
|
||||
|
||||
/** The width of a single mono spaced character obtained by [Paint.measureText] on a single 'X'. */
|
||||
@JvmField
|
||||
val mFontWidth: Float
|
||||
|
||||
/** The [Paint.getFontSpacing]. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||
@JvmField
|
||||
val mFontLineSpacing: Int
|
||||
|
||||
/** The [Paint.ascent]. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||
private val mFontAscent: Int
|
||||
|
||||
/** The [mFontLineSpacing] + [mFontAscent]. */
|
||||
@JvmField
|
||||
val mFontLineSpacingAndAscent: Int
|
||||
|
||||
private val asciiMeasures = FloatArray(127)
|
||||
|
||||
init {
|
||||
mTextPaint.typeface = mTypeface
|
||||
mTextPaint.isAntiAlias = true
|
||||
mTextPaint.textSize = mTextSize.toFloat()
|
||||
|
||||
mFontLineSpacing = Math.ceil(mTextPaint.fontSpacing.toDouble()).toInt()
|
||||
mFontAscent = Math.ceil(mTextPaint.ascent().toDouble()).toInt()
|
||||
mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent
|
||||
mFontWidth = mTextPaint.measureText("X")
|
||||
|
||||
val sb = StringBuilder(" ")
|
||||
for (i in asciiMeasures.indices) {
|
||||
sb.setCharAt(0, i.toChar())
|
||||
asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */
|
||||
fun render(
|
||||
mEmulator: TerminalEmulator, canvas: Canvas, topRow: Int,
|
||||
selectionY1: Int, selectionY2: Int, selectionX1: Int, selectionX2: Int
|
||||
) {
|
||||
val reverseVideo = mEmulator.isReverseVideo()
|
||||
val endRow = topRow + mEmulator.mRows
|
||||
val columns = mEmulator.mColumns
|
||||
val cursorCol = mEmulator.getCursorCol()
|
||||
val cursorRow = mEmulator.getCursorRow()
|
||||
val cursorVisible = mEmulator.shouldCursorBeVisible()
|
||||
val screen = mEmulator.getScreen()
|
||||
val palette = mEmulator.mColors.mCurrentColors
|
||||
val cursorShape = mEmulator.getCursorStyle()
|
||||
|
||||
if (reverseVideo)
|
||||
canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC)
|
||||
|
||||
var heightOffset = mFontLineSpacingAndAscent.toFloat()
|
||||
for (row in topRow until endRow) {
|
||||
heightOffset += mFontLineSpacing
|
||||
|
||||
val cursorX = if (row == cursorRow && cursorVisible) cursorCol else -1
|
||||
var selx1 = -1
|
||||
var selx2 = -1
|
||||
if (row >= selectionY1 && row <= selectionY2) {
|
||||
if (row == selectionY1) selx1 = selectionX1
|
||||
selx2 = if (row == selectionY2) selectionX2 else mEmulator.mColumns
|
||||
}
|
||||
|
||||
val lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row))
|
||||
val line = lineObject.mText
|
||||
val charsUsedInLine = lineObject.spaceUsed
|
||||
|
||||
var lastRunStyle: Long = 0
|
||||
var lastRunInsideCursor = false
|
||||
var lastRunInsideSelection = false
|
||||
var lastRunStartColumn = -1
|
||||
var lastRunStartIndex = 0
|
||||
var lastRunFontWidthMismatch = false
|
||||
var currentCharIndex = 0
|
||||
var measuredWidthForRun = 0f
|
||||
|
||||
var column = 0
|
||||
while (column < columns) {
|
||||
val charAtIndex = line[currentCharIndex]
|
||||
val charIsHighsurrogate = Character.isHighSurrogate(charAtIndex)
|
||||
val charsForCodePoint = if (charIsHighsurrogate) 2 else 1
|
||||
val codePoint = if (charIsHighsurrogate) Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) else charAtIndex.code
|
||||
val codePointWcWidth = WcWidth.width(codePoint)
|
||||
val insideCursor = cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1)
|
||||
val insideSelection = column >= selx1 && column <= selx2
|
||||
val style = lineObject.getStyle(column)
|
||||
|
||||
// Check if the measured text width for this code point is not the same as that expected by wcwidth().
|
||||
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as
|
||||
// smileys which android font renders as wide.
|
||||
// If this is detected, we draw this code point scaled to match what wcwidth() expects.
|
||||
val measuredCodePointWidth = if (codePoint < asciiMeasures.size) asciiMeasures[codePoint] else mTextPaint.measureText(
|
||||
line as CharArray,
|
||||
currentCharIndex, charsForCodePoint
|
||||
)
|
||||
val fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01
|
||||
|
||||
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || insideSelection != lastRunInsideSelection || fontWidthMismatch || lastRunFontWidthMismatch) {
|
||||
if (column == 0) {
|
||||
// Skip first column as there is nothing to draw, just record the current style.
|
||||
} else {
|
||||
val columnWidthSinceLastRun = column - lastRunStartColumn
|
||||
val charsSinceLastRun = currentCharIndex - lastRunStartIndex
|
||||
val cursorColor = if (lastRunInsideCursor) mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] else 0
|
||||
var invertCursorTextColor = false
|
||||
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||
invertCursorTextColor = true
|
||||
}
|
||||
drawTextRun(
|
||||
canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||
cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection
|
||||
)
|
||||
}
|
||||
measuredWidthForRun = 0f
|
||||
lastRunStyle = style
|
||||
lastRunInsideCursor = insideCursor
|
||||
lastRunInsideSelection = insideSelection
|
||||
lastRunStartColumn = column
|
||||
lastRunStartIndex = currentCharIndex
|
||||
lastRunFontWidthMismatch = fontWidthMismatch
|
||||
}
|
||||
measuredWidthForRun += measuredCodePointWidth
|
||||
column += codePointWcWidth
|
||||
currentCharIndex += charsForCodePoint
|
||||
while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex).compareTo(0) <= 0) {
|
||||
// Eat combining chars so that they are treated as part of the last non-combining code point,
|
||||
// instead of e.g. being considered inside the cursor in the next run.
|
||||
currentCharIndex += if (Character.isHighSurrogate(line[currentCharIndex])) 2 else 1
|
||||
}
|
||||
}
|
||||
|
||||
val columnWidthSinceLastRun = columns - lastRunStartColumn
|
||||
val charsSinceLastRun = currentCharIndex - lastRunStartIndex
|
||||
val cursorColor = if (lastRunInsideCursor) mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] else 0
|
||||
var invertCursorTextColor = false
|
||||
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||
invertCursorTextColor = true
|
||||
}
|
||||
drawTextRun(
|
||||
canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
|
||||
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NewApi")
|
||||
private fun drawTextRun(
|
||||
canvas: Canvas, text: CharArray, palette: IntArray, y: Float, startColumn: Int, runWidthColumns: Int,
|
||||
startCharIndex: Int, runWidthChars: Int, mes: Float, cursor: Int, cursorStyle: Int,
|
||||
textStyle: Long, reverseVideo: Boolean
|
||||
) {
|
||||
var foreColor = TextStyle.decodeForeColor(textStyle)
|
||||
val effect = TextStyle.decodeEffect(textStyle)
|
||||
var backColor = TextStyle.decodeBackColor(textStyle)
|
||||
val bold = (effect and (TextStyle.CHARACTER_ATTRIBUTE_BOLD or TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0
|
||||
val underline = (effect and TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0
|
||||
val italic = (effect and TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0
|
||||
val strikeThrough = (effect and TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0
|
||||
val dim = (effect and TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0
|
||||
|
||||
if ((foreColor and 0xff000000.toInt()) != 0xff000000.toInt()) {
|
||||
// Let bold have bright colors if applicable (one of the first 8):
|
||||
if (bold && foreColor >= 0 && foreColor < 8) foreColor += 8
|
||||
foreColor = palette[foreColor]
|
||||
}
|
||||
|
||||
if ((backColor and 0xff000000.toInt()) != 0xff000000.toInt()) {
|
||||
backColor = palette[backColor]
|
||||
}
|
||||
|
||||
// Reverse video here if _one and only one_ of the reverse flags are set:
|
||||
val reverseVideoHere = reverseVideo xor ((effect and TextStyle.CHARACTER_ATTRIBUTE_INVERSE) != 0)
|
||||
if (reverseVideoHere) {
|
||||
val tmp = foreColor
|
||||
foreColor = backColor
|
||||
backColor = tmp
|
||||
}
|
||||
|
||||
var left = startColumn * mFontWidth
|
||||
var right = left + runWidthColumns * mFontWidth
|
||||
|
||||
var mesScaled = mes / mFontWidth
|
||||
var savedMatrix = false
|
||||
if (Math.abs(mesScaled - runWidthColumns) > 0.01) {
|
||||
canvas.save()
|
||||
canvas.scale(runWidthColumns / mesScaled, 1f)
|
||||
left *= mesScaled / runWidthColumns
|
||||
right *= mesScaled / runWidthColumns
|
||||
savedMatrix = true
|
||||
}
|
||||
|
||||
if (backColor != palette[TextStyle.COLOR_INDEX_BACKGROUND]) {
|
||||
// Only draw non-default background.
|
||||
mTextPaint.color = backColor
|
||||
canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint)
|
||||
}
|
||||
|
||||
if (cursor != 0) {
|
||||
mTextPaint.color = cursor
|
||||
var cursorHeight = (mFontLineSpacingAndAscent - mFontAscent).toFloat()
|
||||
if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE) cursorHeight /= 4f
|
||||
else if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4f
|
||||
canvas.drawRect(left, y - cursorHeight, right, y, mTextPaint)
|
||||
}
|
||||
|
||||
if ((effect and TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
|
||||
var actualForeColor = foreColor
|
||||
if (dim) {
|
||||
var red = 0xFF and (foreColor shr 16)
|
||||
var green = 0xFF and (foreColor shr 8)
|
||||
var blue = 0xFF and foreColor
|
||||
// Dim color handling used by libvte which in turn took it from xterm
|
||||
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
|
||||
red = red * 2 / 3
|
||||
green = green * 2 / 3
|
||||
blue = blue * 2 / 3
|
||||
actualForeColor = 0xFF000000.toInt() + (red shl 16) + (green shl 8) + blue
|
||||
}
|
||||
|
||||
mTextPaint.isFakeBoldText = bold
|
||||
mTextPaint.isUnderlineText = underline
|
||||
mTextPaint.textSkewX = if (italic) -0.35f else 0f
|
||||
mTextPaint.isStrikeThruText = strikeThrough
|
||||
mTextPaint.color = actualForeColor
|
||||
|
||||
// The text alignment is the default Paint.Align.LEFT.
|
||||
canvas.drawTextRun(text, startCharIndex, runWidthChars, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, false, mTextPaint)
|
||||
}
|
||||
|
||||
if (savedMatrix) canvas.restore()
|
||||
}
|
||||
|
||||
fun getFontWidth(): Float {
|
||||
return mFontWidth
|
||||
}
|
||||
|
||||
fun getFontLineSpacing(): Int {
|
||||
return mFontLineSpacing
|
||||
}
|
||||
}
|
||||
1222
app/src/main/java/com/termux/view/TerminalView.kt
Normal file
1222
app/src/main/java/com/termux/view/TerminalView.kt
Normal file
File diff suppressed because it is too large
Load Diff
66
app/src/main/java/com/termux/view/TerminalViewClient.kt
Normal file
66
app/src/main/java/com/termux/view/TerminalViewClient.kt
Normal file
@@ -0,0 +1,66 @@
|
||||
package com.termux.view
|
||||
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import com.termux.terminal.TerminalSession
|
||||
|
||||
/**
|
||||
* The interface for communication between [TerminalView] and its client.
|
||||
* It allows for getting various configuration options from the client and for
|
||||
* sending back data to the client like logs, key events, both hardware and IME.
|
||||
* It must be set for the [TerminalView] through [TerminalView.setTerminalViewClient].
|
||||
*/
|
||||
interface TerminalViewClient {
|
||||
|
||||
/**
|
||||
* Callback function on scale events according to [android.view.ScaleGestureDetector.getScaleFactor].
|
||||
*/
|
||||
fun onScale(scale: Float): Float
|
||||
|
||||
/**
|
||||
* On a single tap on the terminal if terminal mouse reporting not enabled.
|
||||
*/
|
||||
fun onSingleTapUp(e: MotionEvent)
|
||||
|
||||
fun shouldBackButtonBeMappedToEscape(): Boolean
|
||||
|
||||
fun shouldEnforceCharBasedInput(): Boolean
|
||||
|
||||
fun shouldUseCtrlSpaceWorkaround(): Boolean
|
||||
|
||||
fun isTerminalViewSelected(): Boolean
|
||||
|
||||
fun copyModeChanged(copyMode: Boolean)
|
||||
|
||||
fun onKeyDown(keyCode: Int, e: KeyEvent, session: TerminalSession): Boolean
|
||||
|
||||
fun onKeyUp(keyCode: Int, e: KeyEvent): Boolean
|
||||
|
||||
fun onLongPress(event: MotionEvent): Boolean
|
||||
|
||||
fun readControlKey(): Boolean
|
||||
|
||||
fun readAltKey(): Boolean
|
||||
|
||||
fun readShiftKey(): Boolean
|
||||
|
||||
fun readFnKey(): Boolean
|
||||
|
||||
fun onCodePoint(codePoint: Int, ctrlDown: Boolean, session: TerminalSession): Boolean
|
||||
|
||||
fun onEmulatorSet()
|
||||
|
||||
fun logError(tag: String?, message: String?)
|
||||
|
||||
fun logWarn(tag: String?, message: String?)
|
||||
|
||||
fun logInfo(tag: String?, message: String?)
|
||||
|
||||
fun logDebug(tag: String?, message: String?)
|
||||
|
||||
fun logVerbose(tag: String?, message: String?)
|
||||
|
||||
fun logStackTraceWithMessage(tag: String?, message: String?, e: Exception?)
|
||||
|
||||
fun logStackTrace(tag: String?, e: Exception?)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
package com.termux.view.support
|
||||
|
||||
import android.widget.PopupWindow
|
||||
|
||||
/**
|
||||
* Implementation of PopupWindow compatibility that can call Gingerbread APIs.
|
||||
* https://chromium.googlesource.com/android_tools/+/HEAD/sdk/extras/android/support/v4/src/gingerbread/android/support/v4/widget/PopupWindowCompatGingerbread.java
|
||||
*/
|
||||
object PopupWindowCompatGingerbread {
|
||||
|
||||
private var setWindowLayoutTypeMethod: java.lang.reflect.Method? = null
|
||||
private var setWindowLayoutTypeMethodAttempted = false
|
||||
private var getWindowLayoutTypeMethod: java.lang.reflect.Method? = null
|
||||
private var getWindowLayoutTypeMethodAttempted = false
|
||||
|
||||
@JvmStatic
|
||||
fun setWindowLayoutType(popupWindow: PopupWindow, layoutType: Int) {
|
||||
if (!setWindowLayoutTypeMethodAttempted) {
|
||||
try {
|
||||
setWindowLayoutTypeMethod = PopupWindow::class.java.getDeclaredMethod(
|
||||
"setWindowLayoutType", Int::class.javaPrimitiveType
|
||||
)
|
||||
setWindowLayoutTypeMethod?.isAccessible = true
|
||||
} catch (e: Exception) {
|
||||
// Reflection method fetch failed. Oh well.
|
||||
}
|
||||
setWindowLayoutTypeMethodAttempted = true
|
||||
}
|
||||
setWindowLayoutTypeMethod?.let {
|
||||
try {
|
||||
it.invoke(popupWindow, layoutType)
|
||||
} catch (e: Exception) {
|
||||
// Reflection call failed. Oh well.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getWindowLayoutType(popupWindow: PopupWindow): Int {
|
||||
if (!getWindowLayoutTypeMethodAttempted) {
|
||||
try {
|
||||
getWindowLayoutTypeMethod = PopupWindow::class.java.getDeclaredMethod(
|
||||
"getWindowLayoutType"
|
||||
)
|
||||
getWindowLayoutTypeMethod?.isAccessible = true
|
||||
} catch (e: Exception) {
|
||||
// Reflection method fetch failed. Oh well.
|
||||
}
|
||||
getWindowLayoutTypeMethodAttempted = true
|
||||
}
|
||||
getWindowLayoutTypeMethod?.let {
|
||||
try {
|
||||
return it.invoke(popupWindow) as Int
|
||||
} catch (e: Exception) {
|
||||
// Reflection call failed. Oh well.
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.termux.view.textselection
|
||||
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewTreeObserver
|
||||
|
||||
/**
|
||||
* A CursorController instance can be used to control cursors in the text.
|
||||
* It is not used outside of [com.termux.view.TerminalView].
|
||||
*/
|
||||
interface CursorController : ViewTreeObserver.OnTouchModeChangeListener {
|
||||
|
||||
/**
|
||||
* Show the cursors on screen. Will be drawn by [render] by a call during onDraw.
|
||||
* See also [hide].
|
||||
*/
|
||||
fun show(event: MotionEvent)
|
||||
|
||||
/**
|
||||
* Hide the cursors from screen.
|
||||
* See also [show].
|
||||
*/
|
||||
fun hide(): Boolean
|
||||
|
||||
/**
|
||||
* Render the cursors.
|
||||
*/
|
||||
fun render()
|
||||
|
||||
/**
|
||||
* Update the cursor positions.
|
||||
*/
|
||||
fun updatePosition(handle: TextSelectionHandleView, x: Int, y: Int)
|
||||
|
||||
/**
|
||||
* This method is called by [onTouchEvent] and gives the cursors
|
||||
* a chance to become active and/or visible.
|
||||
*
|
||||
* @param event The touch event
|
||||
*/
|
||||
fun onTouchEvent(event: MotionEvent): Boolean
|
||||
|
||||
/**
|
||||
* Called when the view is detached from window. Perform house keeping task, such as
|
||||
* stopping Runnable thread that would otherwise keep a reference on the context, thus
|
||||
* preventing the activity to be recycled.
|
||||
*/
|
||||
fun onDetached()
|
||||
|
||||
/**
|
||||
* @return true if the cursors are currently active.
|
||||
*/
|
||||
fun isActive(): Boolean
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
package com.termux.view.textselection
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
|
||||
import com.termux.terminal.TerminalBuffer
|
||||
import com.termux.terminal.WcWidth
|
||||
import com.termux.view.TerminalView
|
||||
|
||||
class TextSelectionCursorController(private val terminalView: TerminalView) : CursorController {
|
||||
|
||||
private val mStartHandle: TextSelectionHandleView = TextSelectionHandleView(terminalView, this, TextSelectionHandleView.LEFT)
|
||||
private val mEndHandle: TextSelectionHandleView = TextSelectionHandleView(terminalView, this, TextSelectionHandleView.RIGHT)
|
||||
private var mStoredSelectedText: String? = null
|
||||
private var mIsSelectingText = false
|
||||
private var mShowStartTime = System.currentTimeMillis()
|
||||
|
||||
private val mHandleHeight: Int = maxOf(mStartHandle.handleHeight, mEndHandle.handleHeight)
|
||||
private var mSelX1 = -1
|
||||
private var mSelX2 = -1
|
||||
private var mSelY1 = -1
|
||||
private var mSelY2 = -1
|
||||
|
||||
private var mActionMode: ActionMode? = null
|
||||
val ACTION_COPY = 1
|
||||
val ACTION_PASTE = 2
|
||||
val ACTION_MORE = 3
|
||||
|
||||
override fun show(event: MotionEvent) {
|
||||
setInitialTextSelectionPosition(event)
|
||||
mStartHandle.positionAtCursor(mSelX1, mSelY1, true)
|
||||
mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, true)
|
||||
|
||||
setActionModeCallBacks()
|
||||
mShowStartTime = System.currentTimeMillis()
|
||||
mIsSelectingText = true
|
||||
}
|
||||
|
||||
override fun hide(): Boolean {
|
||||
if (!isActive()) return false
|
||||
|
||||
// prevent hide calls right after a show call, like long pressing the down key
|
||||
// 300ms seems long enough that it wouldn't cause hide problems if action button
|
||||
// is quickly clicked after the show, otherwise decrease it
|
||||
if (System.currentTimeMillis() - mShowStartTime < 300) {
|
||||
return false
|
||||
}
|
||||
|
||||
mStartHandle.hide()
|
||||
mEndHandle.hide()
|
||||
|
||||
mActionMode?.finish()
|
||||
|
||||
mSelY2 = -1
|
||||
mSelX2 = mSelY2
|
||||
mSelY1 = mSelX2
|
||||
mSelX1 = mSelY1
|
||||
mIsSelectingText = false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun render() {
|
||||
if (!isActive()) return
|
||||
|
||||
mStartHandle.positionAtCursor(mSelX1, mSelY1, false)
|
||||
mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, false)
|
||||
|
||||
mActionMode?.invalidate()
|
||||
}
|
||||
|
||||
fun setInitialTextSelectionPosition(event: MotionEvent) {
|
||||
val columnAndRow = terminalView.getColumnAndRow(event, true)
|
||||
mSelX2 = columnAndRow[0]
|
||||
mSelX1 = mSelX2
|
||||
mSelY2 = columnAndRow[1]
|
||||
mSelY1 = mSelY2
|
||||
|
||||
val screen = terminalView.mEmulator!!.getScreen()
|
||||
if (" " != screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1)) {
|
||||
// Selecting something other than whitespace. Expand to word.
|
||||
while (mSelX1 > 0 && "" != screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1)) {
|
||||
mSelX1--
|
||||
}
|
||||
while (mSelX2 < terminalView.mEmulator!!.mColumns - 1 && "" != screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1)) {
|
||||
mSelX2++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setActionModeCallBacks() {
|
||||
val callback = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val show = MenuItem.SHOW_AS_ACTION_IF_ROOM or MenuItem.SHOW_AS_ACTION_WITH_TEXT
|
||||
|
||||
val clipboard = terminalView.context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
val copyItem = menu.add(Menu.NONE, ACTION_COPY, Menu.NONE, "Copy")
|
||||
copyItem.setShowAsAction(show)
|
||||
val pasteItem = menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, "Paste")
|
||||
pasteItem.isEnabled = clipboard?.hasPrimaryClip() == true
|
||||
pasteItem.setShowAsAction(show)
|
||||
// val moreItem = menu.add(Menu.NONE, ACTION_MORE, Menu.NONE, "More…")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = false
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
if (!isActive()) {
|
||||
// Fix issue where the dialog is pressed while being dismissed.
|
||||
return true
|
||||
}
|
||||
|
||||
when (item.itemId) {
|
||||
ACTION_COPY -> {
|
||||
val selectedText = getSelectedText()
|
||||
terminalView.mTermSession!!.onCopyTextToClipboard(selectedText)
|
||||
terminalView.stopTextSelectionMode()
|
||||
}
|
||||
ACTION_PASTE -> {
|
||||
terminalView.stopTextSelectionMode()
|
||||
terminalView.mTermSession!!.onPasteTextFromClipboard()
|
||||
}
|
||||
ACTION_MORE -> {
|
||||
// We first store the selected text in case TerminalViewClient needs the
|
||||
// selected text before MORE button was pressed since we are going to
|
||||
// stop selection mode
|
||||
mStoredSelectedText = getSelectedText()
|
||||
// The text selection needs to be stopped before showing context menu,
|
||||
// otherwise handles will show above popup
|
||||
terminalView.stopTextSelectionMode()
|
||||
terminalView.showContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
mActionMode = terminalView.startActionMode(callback)
|
||||
return
|
||||
}
|
||||
|
||||
mActionMode = terminalView.startActionMode(object : ActionMode.Callback2() {
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
return callback.onCreateActionMode(mode, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = false
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
return callback.onActionItemClicked(mode, item)
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {}
|
||||
|
||||
override fun onGetContentRect(mode: ActionMode, view: View, outRect: Rect) {
|
||||
var x1 = (mSelX1 * terminalView.mRenderer!!.mFontWidth).toInt()
|
||||
var x2 = (mSelX2 * terminalView.mRenderer!!.mFontWidth).toInt()
|
||||
val y1 = (mSelY1 - 1 - terminalView.getTopRow()) * terminalView.mRenderer!!.mFontLineSpacing
|
||||
val y2 = (mSelY2 + 1 - terminalView.getTopRow()) * terminalView.mRenderer!!.mFontLineSpacing
|
||||
|
||||
if (x1 > x2) {
|
||||
val tmp = x1
|
||||
x1 = x2
|
||||
x2 = tmp
|
||||
}
|
||||
|
||||
val terminalBottom = terminalView.bottom
|
||||
var top = y1 + mHandleHeight
|
||||
var bottom = y2 + mHandleHeight
|
||||
if (top > terminalBottom) top = terminalBottom
|
||||
if (bottom > terminalBottom) bottom = terminalBottom
|
||||
|
||||
outRect.set(x1, top, x2, bottom)
|
||||
}
|
||||
}, ActionMode.TYPE_FLOATING)
|
||||
}
|
||||
|
||||
override fun updatePosition(handle: TextSelectionHandleView, x: Int, y: Int) {
|
||||
val screen = terminalView.mEmulator!!.getScreen()
|
||||
val scrollRows = screen.activeRows - terminalView.mEmulator!!.mRows
|
||||
if (handle === mStartHandle) {
|
||||
mSelX1 = terminalView.getCursorX(x.toFloat())
|
||||
mSelY1 = terminalView.getCursorY(y.toFloat())
|
||||
if (mSelX1 < 0) {
|
||||
mSelX1 = 0
|
||||
}
|
||||
|
||||
if (mSelY1 < -scrollRows) {
|
||||
mSelY1 = -scrollRows
|
||||
} else if (mSelY1 > terminalView.mEmulator!!.mRows - 1) {
|
||||
mSelY1 = terminalView.mEmulator!!.mRows - 1
|
||||
}
|
||||
|
||||
if (mSelY1 > mSelY2) {
|
||||
mSelY1 = mSelY2
|
||||
}
|
||||
if (mSelY1 == mSelY2 && mSelX1 > mSelX2) {
|
||||
mSelX1 = mSelX2
|
||||
}
|
||||
|
||||
if (!terminalView.mEmulator!!.isAlternateBufferActive()) {
|
||||
var topRow = terminalView.getTopRow()
|
||||
|
||||
if (mSelY1 <= topRow) {
|
||||
topRow--
|
||||
if (topRow < -scrollRows) {
|
||||
topRow = -scrollRows
|
||||
}
|
||||
} else if (mSelY1 >= topRow + terminalView.mEmulator!!.mRows) {
|
||||
topRow++
|
||||
if (topRow > 0) {
|
||||
topRow = 0
|
||||
}
|
||||
}
|
||||
|
||||
terminalView.setTopRow(topRow)
|
||||
}
|
||||
|
||||
mSelX1 = getValidCurX(screen, mSelY1, mSelX1)
|
||||
} else {
|
||||
mSelX2 = terminalView.getCursorX(x.toFloat())
|
||||
mSelY2 = terminalView.getCursorY(y.toFloat())
|
||||
if (mSelX2 < 0) {
|
||||
mSelX2 = 0
|
||||
}
|
||||
|
||||
if (mSelY2 < -scrollRows) {
|
||||
mSelY2 = -scrollRows
|
||||
} else if (mSelY2 > terminalView.mEmulator!!.mRows - 1) {
|
||||
mSelY2 = terminalView.mEmulator!!.mRows - 1
|
||||
}
|
||||
|
||||
if (mSelY1 > mSelY2) {
|
||||
mSelY2 = mSelY1
|
||||
}
|
||||
if (mSelY1 == mSelY2 && mSelX1 > mSelX2) {
|
||||
mSelX2 = mSelX1
|
||||
}
|
||||
|
||||
if (!terminalView.mEmulator!!.isAlternateBufferActive()) {
|
||||
var topRow = terminalView.getTopRow()
|
||||
|
||||
if (mSelY2 <= topRow) {
|
||||
topRow--
|
||||
if (topRow < -scrollRows) {
|
||||
topRow = -scrollRows
|
||||
}
|
||||
} else if (mSelY2 >= topRow + terminalView.mEmulator!!.mRows) {
|
||||
topRow++
|
||||
if (topRow > 0) {
|
||||
topRow = 0
|
||||
}
|
||||
}
|
||||
|
||||
terminalView.setTopRow(topRow)
|
||||
}
|
||||
|
||||
mSelX2 = getValidCurX(screen, mSelY2, mSelX2)
|
||||
}
|
||||
|
||||
terminalView.invalidate()
|
||||
}
|
||||
|
||||
private fun getValidCurX(screen: TerminalBuffer, cy: Int, cx: Int): Int {
|
||||
val line = screen.getSelectedText(0, cy, cx, cy)
|
||||
if (!TextUtils.isEmpty(line)) {
|
||||
var col = 0
|
||||
var i = 0
|
||||
val len = line.length
|
||||
while (i < len) {
|
||||
val ch1 = line[i]
|
||||
if (ch1.code == 0) {
|
||||
break
|
||||
}
|
||||
|
||||
val wc: Int
|
||||
if (Character.isHighSurrogate(ch1) && i + 1 < len) {
|
||||
val ch2 = line[++i]
|
||||
wc = WcWidth.width(Character.toCodePoint(ch1, ch2))
|
||||
} else {
|
||||
wc = WcWidth.width(ch1.code)
|
||||
}
|
||||
|
||||
val cend = col + wc
|
||||
if (cx > col && cx < cend) {
|
||||
return cend
|
||||
}
|
||||
if (cend == col) {
|
||||
return col
|
||||
}
|
||||
col = cend
|
||||
i++
|
||||
}
|
||||
}
|
||||
return cx
|
||||
}
|
||||
|
||||
fun decrementYTextSelectionCursors(decrement: Int) {
|
||||
mSelY1 -= decrement
|
||||
mSelY2 -= decrement
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean = false
|
||||
|
||||
override fun onTouchModeChanged(isInTouchMode: Boolean) {
|
||||
if (!isInTouchMode) {
|
||||
terminalView.stopTextSelectionMode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetached() {}
|
||||
|
||||
override fun isActive(): Boolean = mIsSelectingText
|
||||
|
||||
fun getSelectors(sel: IntArray?) {
|
||||
if (sel == null || sel.size != 4) {
|
||||
return
|
||||
}
|
||||
|
||||
sel[0] = mSelY1
|
||||
sel[1] = mSelY2
|
||||
sel[2] = mSelX1
|
||||
sel[3] = mSelX2
|
||||
}
|
||||
|
||||
/** Get the currently selected text. */
|
||||
fun getSelectedText(): String {
|
||||
return terminalView.mEmulator!!.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2)
|
||||
}
|
||||
|
||||
/** Get the selected text stored before "MORE" button was pressed on the context menu. */
|
||||
val storedSelectedText: String?
|
||||
get() = mStoredSelectedText
|
||||
|
||||
/** Unset the selected text stored before "MORE" button was pressed on the context menu. */
|
||||
fun unsetStoredSelectedText() {
|
||||
mStoredSelectedText = null
|
||||
}
|
||||
|
||||
fun getActionMode(): ActionMode? = mActionMode
|
||||
|
||||
/** @return true if this controller is currently used to move the start selection. */
|
||||
val isSelectionStartDragged: Boolean
|
||||
get() = mStartHandle.isDragging
|
||||
|
||||
/** @return true if this controller is currently used to move the end selection. */
|
||||
val isSelectionEndDragged: Boolean
|
||||
get() = mEndHandle.isDragging
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package com.termux.view.textselection
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.PopupWindow
|
||||
|
||||
import io.github.miuzarte.scrcpyforandroid.R
|
||||
import com.termux.view.TerminalView
|
||||
import com.termux.view.support.PopupWindowCompatGingerbread
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
class TextSelectionHandleView(
|
||||
private val terminalView: TerminalView,
|
||||
private val mCursorController: CursorController,
|
||||
private val mInitialOrientation: Int
|
||||
) : View(terminalView.context) {
|
||||
|
||||
private var mHandle: PopupWindow? = null
|
||||
|
||||
private val mHandleLeftDrawable: Drawable?
|
||||
private val mHandleRightDrawable: Drawable?
|
||||
private var mHandleDrawable: Drawable? = null
|
||||
|
||||
private var mIsDragging = false
|
||||
|
||||
val mTempCoords = IntArray(2)
|
||||
var mTempRect: Rect? = null
|
||||
|
||||
private var mPointX = 0
|
||||
private var mPointY = 0
|
||||
private var mTouchToWindowOffsetX = 0f
|
||||
private var mTouchToWindowOffsetY = 0f
|
||||
private var mHotspotX = 0f
|
||||
private var mHotspotY = 0f
|
||||
private var mTouchOffsetY = 0f
|
||||
private var mLastParentX = 0
|
||||
private var mLastParentY = 0
|
||||
|
||||
private var mHandleHeight = 0
|
||||
private var mHandleWidth = 0
|
||||
|
||||
private var mOrientation = 0
|
||||
|
||||
private var mLastTime: Long = 0
|
||||
|
||||
init {
|
||||
mHandleLeftDrawable = context.getDrawable(R.drawable.text_select_handle_left_material)
|
||||
mHandleRightDrawable = context.getDrawable(R.drawable.text_select_handle_right_material)
|
||||
|
||||
setOrientation(mInitialOrientation)
|
||||
}
|
||||
|
||||
private fun initHandle() {
|
||||
mHandle = PopupWindow(terminalView.context, null,
|
||||
android.R.attr.textSelectHandleWindowStyle)
|
||||
mHandle?.setSplitTouchEnabled(true)
|
||||
mHandle?.isClippingEnabled = false
|
||||
mHandle?.width = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
mHandle?.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
mHandle?.setBackgroundDrawable(null)
|
||||
mHandle?.animationStyle = 0
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mHandle?.windowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL
|
||||
mHandle?.enterTransition = null
|
||||
mHandle?.exitTransition = null
|
||||
} else {
|
||||
mHandle?.let { PopupWindowCompatGingerbread.setWindowLayoutType(it, WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL) }
|
||||
}
|
||||
mHandle?.contentView = this
|
||||
}
|
||||
|
||||
fun setOrientation(orientation: Int) {
|
||||
mOrientation = orientation
|
||||
var handleWidth = 0
|
||||
when (orientation) {
|
||||
LEFT -> {
|
||||
mHandleDrawable = mHandleLeftDrawable
|
||||
handleWidth = mHandleDrawable?.intrinsicWidth ?: 0
|
||||
mHotspotX = (handleWidth * 3) / 4f
|
||||
}
|
||||
RIGHT -> {
|
||||
mHandleDrawable = mHandleRightDrawable
|
||||
handleWidth = mHandleDrawable?.intrinsicWidth ?: 0
|
||||
mHotspotX = handleWidth / 4f
|
||||
}
|
||||
}
|
||||
|
||||
mHandleHeight = mHandleDrawable?.intrinsicHeight ?: 0
|
||||
|
||||
mHandleWidth = handleWidth
|
||||
mTouchOffsetY = -mHandleHeight * 0.3f
|
||||
mHotspotY = 0f
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun show() {
|
||||
if (!isPositionVisible()) {
|
||||
hide()
|
||||
return
|
||||
}
|
||||
|
||||
// We remove handle from its parent first otherwise the following exception may be thrown
|
||||
// java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
|
||||
removeFromParent()
|
||||
|
||||
initHandle() // init the handle
|
||||
invalidate() // invalidate to make sure onDraw is called
|
||||
|
||||
val coords = mTempCoords
|
||||
terminalView.getLocationInWindow(coords)
|
||||
coords[0] += mPointX
|
||||
coords[1] += mPointY
|
||||
|
||||
mHandle?.showAtLocation(terminalView, 0, coords[0], coords[1])
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
mIsDragging = false
|
||||
|
||||
mHandle?.let { handle ->
|
||||
handle.dismiss()
|
||||
|
||||
// We remove handle from its parent, otherwise it may still be shown in some cases even after the dismiss call
|
||||
removeFromParent()
|
||||
mHandle = null // garbage collect the handle
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun removeFromParent() {
|
||||
if (!isParentNull) {
|
||||
(this.parent as ViewGroup).removeView(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun positionAtCursor(cx: Int, cy: Int, forceOrientationCheck: Boolean) {
|
||||
val x = terminalView.getPointX(cx)
|
||||
val y = terminalView.getPointY(cy + 1)
|
||||
moveTo(x, y, forceOrientationCheck)
|
||||
}
|
||||
|
||||
private fun moveTo(x: Int, y: Int, forceOrientationCheck: Boolean) {
|
||||
val oldHotspotX = mHotspotX
|
||||
checkChangedOrientation(x, forceOrientationCheck)
|
||||
mPointX = (x - (if (isShowing) oldHotspotX else mHotspotX)).toInt()
|
||||
mPointY = y
|
||||
|
||||
if (isPositionVisible()) {
|
||||
var coords: IntArray? = null
|
||||
|
||||
if (isShowing) {
|
||||
coords = mTempCoords
|
||||
terminalView.getLocationInWindow(coords)
|
||||
val x1 = coords[0] + mPointX
|
||||
val y1 = coords[1] + mPointY
|
||||
mHandle?.update(x1, y1, width, height)
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
|
||||
if (mIsDragging) {
|
||||
if (coords == null) {
|
||||
coords = mTempCoords
|
||||
terminalView.getLocationInWindow(coords)
|
||||
}
|
||||
if (coords[0] != mLastParentX || coords[1] != mLastParentY) {
|
||||
mTouchToWindowOffsetX += coords[0] - mLastParentX
|
||||
mTouchToWindowOffsetY += coords[1] - mLastParentY
|
||||
mLastParentX = coords[0]
|
||||
mLastParentY = coords[1]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeOrientation(orientation: Int) {
|
||||
if (mOrientation != orientation) {
|
||||
setOrientation(orientation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkChangedOrientation(posX: Int, force: Boolean) {
|
||||
if (!mIsDragging && !force) {
|
||||
return
|
||||
}
|
||||
val millis = SystemClock.currentThreadTimeMillis()
|
||||
if (millis - mLastTime < 50 && !force) {
|
||||
return
|
||||
}
|
||||
mLastTime = millis
|
||||
|
||||
val hostView = terminalView
|
||||
val left = hostView.left
|
||||
val right = hostView.width
|
||||
val top = hostView.top
|
||||
val bottom = hostView.height
|
||||
|
||||
if (mTempRect == null) {
|
||||
mTempRect = Rect()
|
||||
}
|
||||
val clip = mTempRect!!
|
||||
clip.left = left + terminalView.paddingLeft
|
||||
clip.top = top + terminalView.paddingTop
|
||||
clip.right = right - terminalView.paddingRight
|
||||
clip.bottom = bottom - terminalView.paddingBottom
|
||||
|
||||
val parent = hostView.parent
|
||||
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (posX - mHandleWidth < clip.left) {
|
||||
changeOrientation(RIGHT)
|
||||
} else if (posX + mHandleWidth > clip.right) {
|
||||
changeOrientation(LEFT)
|
||||
} else {
|
||||
changeOrientation(mInitialOrientation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPositionVisible(): Boolean {
|
||||
// Always show a dragging handle.
|
||||
if (mIsDragging) {
|
||||
return true
|
||||
}
|
||||
|
||||
val hostView = terminalView
|
||||
val left = 0
|
||||
val right = hostView.width
|
||||
val top = 0
|
||||
val bottom = hostView.height
|
||||
|
||||
if (mTempRect == null) {
|
||||
mTempRect = Rect()
|
||||
}
|
||||
val clip = mTempRect!!
|
||||
clip.left = left + terminalView.paddingLeft
|
||||
clip.top = top + terminalView.paddingTop
|
||||
clip.right = right - terminalView.paddingRight
|
||||
clip.bottom = bottom - terminalView.paddingBottom
|
||||
|
||||
val parent = hostView.parent
|
||||
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val coords = mTempCoords
|
||||
hostView.getLocationInWindow(coords)
|
||||
val posX = coords[0] + mPointX + mHotspotX.toInt()
|
||||
val posY = coords[1] + mPointY + mHotspotY.toInt()
|
||||
|
||||
return posX >= clip.left && posX <= clip.right &&
|
||||
posY >= clip.top && posY <= clip.bottom
|
||||
}
|
||||
|
||||
override fun onDraw(c: Canvas) {
|
||||
val width = mHandleDrawable?.intrinsicWidth ?: 0
|
||||
val height = mHandleDrawable?.intrinsicHeight ?: 0
|
||||
mHandleDrawable?.setBounds(0, 0, width, height)
|
||||
mHandleDrawable?.draw(c)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
terminalView.updateFloatingToolbarVisibility(event)
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
val rawX = event.rawX
|
||||
val rawY = event.rawY
|
||||
mTouchToWindowOffsetX = rawX - mPointX
|
||||
mTouchToWindowOffsetY = rawY - mPointY
|
||||
val coords = mTempCoords
|
||||
terminalView.getLocationInWindow(coords)
|
||||
mLastParentX = coords[0]
|
||||
mLastParentY = coords[1]
|
||||
mIsDragging = true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val rawX = event.rawX
|
||||
val rawY = event.rawY
|
||||
|
||||
val newPosX = rawX - mTouchToWindowOffsetX + mHotspotX
|
||||
val newPosY = rawY - mTouchToWindowOffsetY + mHotspotY + mTouchOffsetY
|
||||
|
||||
mCursorController.updatePosition(this, Math.round(newPosX), Math.round(newPosY))
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> mIsDragging = false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
setMeasuredDimension(mHandleDrawable?.intrinsicWidth ?: 0,
|
||||
mHandleDrawable?.intrinsicHeight ?: 0)
|
||||
}
|
||||
|
||||
val handleHeight: Int
|
||||
get() = mHandleHeight
|
||||
|
||||
val handleWidth: Int
|
||||
get() = mHandleWidth
|
||||
|
||||
val isShowing: Boolean
|
||||
get() = mHandle?.isShowing == true
|
||||
|
||||
val isParentNull: Boolean
|
||||
get() = this.parent == null
|
||||
|
||||
val isDragging: Boolean
|
||||
get() = mIsDragging
|
||||
|
||||
companion object {
|
||||
const val LEFT = 0
|
||||
const val RIGHT = 2
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import io.github.miuzarte.scrcpyforandroid.password.PasswordRepository
|
||||
import io.github.miuzarte.scrcpyforandroid.password.hasAuthenticatedOrigin
|
||||
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
||||
import io.github.miuzarte.scrcpyforandroid.services.AppWakeLocks
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
// 生物认证需要 FragmentActivity
|
||||
@@ -21,12 +20,7 @@ class MainActivity : FragmentActivity() {
|
||||
AppRuntime.init(applicationContext)
|
||||
AppWakeLocks.init(applicationContext)
|
||||
|
||||
val migration = PreferenceMigration(applicationContext)
|
||||
runBlocking {
|
||||
// 旧版设置迁移
|
||||
if (migration.needsMigration())
|
||||
migration.migrate(clearSharedPrefs = true)
|
||||
|
||||
PasswordRepository.refresh()
|
||||
// 认证不可用时, 清除经认证创建的密码
|
||||
if (!BiometricGate.canAuthenticate()) {
|
||||
|
||||
@@ -714,8 +714,19 @@ fun DeviceTabPage(
|
||||
val options = scrcpyOptions.toClientOptions(activeBundle).fix()
|
||||
scrcpy.stop()
|
||||
if (options.killAdbOnClose) {
|
||||
// TODO
|
||||
disconnectAdbConnection()
|
||||
currentTarget?.host?.let { sessionReconnectBlacklistHosts += it }
|
||||
disconnectAdbConnection(
|
||||
clearQuickOnlineForTarget = currentTarget,
|
||||
logMessage = "scrcpy 已停止,ADB 已断开",
|
||||
showSnackMessage = "scrcpy 已停止,ADB 已断开",
|
||||
)
|
||||
} else {
|
||||
AppWakeLocks.release()
|
||||
adbSession = adbSession.copy(
|
||||
statusLine = "${currentTarget!!.host}:${currentTarget.port}"
|
||||
)
|
||||
logEvent("scrcpy 已停止")
|
||||
snackbar.show("scrcpy 已停止")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -805,7 +816,7 @@ fun DeviceTabPage(
|
||||
) {
|
||||
if (adbConnected || !asBundle.adbAutoReconnectPairedDevice) return@LaunchedEffect
|
||||
adbBackgroundRunner.runAutoReconnectLoop(
|
||||
isConnected = { adbConnected },
|
||||
isConnected = { false },
|
||||
isForeground = { isAppInForeground },
|
||||
isAutoReconnectEnabled = { asBundle.adbAutoReconnectPairedDevice },
|
||||
isBusy = { busy },
|
||||
@@ -1093,13 +1104,7 @@ fun DeviceTabPage(
|
||||
},
|
||||
onStop = {
|
||||
runBusy("停止 scrcpy") {
|
||||
scrcpy.stop()
|
||||
AppWakeLocks.release()
|
||||
adbSession = adbSession.copy(
|
||||
statusLine = "${currentTarget!!.host}:${currentTarget.port}"
|
||||
)
|
||||
logEvent("scrcpy 已停止")
|
||||
snackbar.show("scrcpy 已停止")
|
||||
stopScrcpySession()
|
||||
}
|
||||
},
|
||||
sessionInfo = sessionInfo,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.pages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.Surface
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.background
|
||||
@@ -38,8 +41,10 @@ import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
@@ -51,6 +56,7 @@ import io.github.miuzarte.scrcpyforandroid.password.PasswordPickerPopupContent
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.TouchEventHandler
|
||||
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.AppListBottomSheet
|
||||
@@ -69,6 +75,7 @@ import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.SnackbarHost
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Composable
|
||||
fun FullscreenControlScreen(
|
||||
scrcpy: Scrcpy,
|
||||
@@ -80,6 +87,7 @@ fun FullscreenControlScreen(
|
||||
BackHandler(enabled = true, onBack = onBack)
|
||||
|
||||
val activity = LocalActivity.current
|
||||
val context = LocalContext.current
|
||||
val fragmentActivity = remember(activity) { activity as? FragmentActivity }
|
||||
|
||||
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||
@@ -122,7 +130,94 @@ fun FullscreenControlScreen(
|
||||
}
|
||||
val fullscreenDebugInfo = asBundle.fullscreenDebugInfo
|
||||
val showFullscreenVirtualButtons = asBundle.showFullscreenVirtualButtons
|
||||
val showFullscreenFloatingButton = asBundle.showFullscreenFloatingButton
|
||||
val fullscreenVirtualButtonHeight = asBundle.fullscreenVirtualButtonHeightDp.dp
|
||||
val fullscreenVirtualButtonDockSetting = remember(asBundle.fullscreenVirtualButtonDock) {
|
||||
AppSettings.FullscreenVirtualButtonDock.fromStoredValue(
|
||||
asBundle.fullscreenVirtualButtonDock
|
||||
)
|
||||
}
|
||||
var displayRotation by remember(activity) {
|
||||
mutableIntStateOf(activity?.display?.rotation ?: Surface.ROTATION_0)
|
||||
}
|
||||
DisposableEffect(activity, context) {
|
||||
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager
|
||||
if (displayManager == null || activity == null) {
|
||||
onDispose {}
|
||||
} else {
|
||||
val listener = object : DisplayManager.DisplayListener {
|
||||
override fun onDisplayAdded(displayId: Int) = Unit
|
||||
|
||||
override fun onDisplayRemoved(displayId: Int) = Unit
|
||||
|
||||
override fun onDisplayChanged(displayId: Int) {
|
||||
if (displayId == activity.display?.displayId) {
|
||||
displayRotation = activity.display?.rotation ?: Surface.ROTATION_0
|
||||
}
|
||||
}
|
||||
}
|
||||
displayRotation = activity.display?.rotation ?: Surface.ROTATION_0
|
||||
displayManager.registerDisplayListener(listener, null)
|
||||
onDispose {
|
||||
displayManager.unregisterDisplayListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
val fullscreenVirtualButtonPhysicalDock = remember(fullscreenVirtualButtonDockSetting) {
|
||||
when (fullscreenVirtualButtonDockSetting) {
|
||||
AppSettings.FullscreenVirtualButtonDock.FOLLOW_TOP,
|
||||
AppSettings.FullscreenVirtualButtonDock.FOLLOW_BOTTOM,
|
||||
AppSettings.FullscreenVirtualButtonDock.FOLLOW_LEFT,
|
||||
AppSettings.FullscreenVirtualButtonDock.FOLLOW_RIGHT -> null
|
||||
|
||||
AppSettings.FullscreenVirtualButtonDock.FIXED_TOP -> VirtualButtonBar.FullscreenDock.TOP
|
||||
AppSettings.FullscreenVirtualButtonDock.FIXED_BOTTOM -> VirtualButtonBar.FullscreenDock.BOTTOM
|
||||
AppSettings.FullscreenVirtualButtonDock.FIXED_LEFT -> VirtualButtonBar.FullscreenDock.LEFT
|
||||
AppSettings.FullscreenVirtualButtonDock.FIXED_RIGHT -> VirtualButtonBar.FullscreenDock.RIGHT
|
||||
}
|
||||
}
|
||||
val fullscreenVirtualButtonDock = remember(
|
||||
fullscreenVirtualButtonDockSetting,
|
||||
displayRotation,
|
||||
) {
|
||||
when (fullscreenVirtualButtonDockSetting) {
|
||||
AppSettings.FullscreenVirtualButtonDock.FOLLOW_TOP -> VirtualButtonBar.FullscreenDock.TOP
|
||||
AppSettings.FullscreenVirtualButtonDock.FOLLOW_BOTTOM -> VirtualButtonBar.FullscreenDock.BOTTOM
|
||||
AppSettings.FullscreenVirtualButtonDock.FOLLOW_LEFT -> VirtualButtonBar.FullscreenDock.LEFT
|
||||
AppSettings.FullscreenVirtualButtonDock.FOLLOW_RIGHT -> VirtualButtonBar.FullscreenDock.RIGHT
|
||||
AppSettings.FullscreenVirtualButtonDock.FIXED_TOP -> dockForFixedPhysicalEdge(
|
||||
physicalDock = VirtualButtonBar.FullscreenDock.TOP,
|
||||
displayRotation = displayRotation,
|
||||
)
|
||||
|
||||
AppSettings.FullscreenVirtualButtonDock.FIXED_BOTTOM -> dockForFixedPhysicalEdge(
|
||||
physicalDock = VirtualButtonBar.FullscreenDock.BOTTOM,
|
||||
displayRotation = displayRotation,
|
||||
)
|
||||
|
||||
AppSettings.FullscreenVirtualButtonDock.FIXED_LEFT -> dockForFixedPhysicalEdge(
|
||||
physicalDock = VirtualButtonBar.FullscreenDock.LEFT,
|
||||
displayRotation = displayRotation,
|
||||
)
|
||||
|
||||
AppSettings.FullscreenVirtualButtonDock.FIXED_RIGHT -> dockForFixedPhysicalEdge(
|
||||
physicalDock = VirtualButtonBar.FullscreenDock.RIGHT,
|
||||
displayRotation = displayRotation,
|
||||
)
|
||||
}
|
||||
}
|
||||
val fullscreenVirtualButtonReverseOrder = remember(
|
||||
fullscreenVirtualButtonPhysicalDock,
|
||||
displayRotation,
|
||||
) {
|
||||
fullscreenVirtualButtonPhysicalDock
|
||||
?.let { physicalDock ->
|
||||
isFixedDockOrderReversed(
|
||||
physicalDock = physicalDock,
|
||||
displayRotation = displayRotation,
|
||||
)
|
||||
}
|
||||
?: false
|
||||
}
|
||||
|
||||
val bar = remember(buttonItems) {
|
||||
VirtualButtonBar(
|
||||
@@ -269,7 +364,7 @@ fun FullscreenControlScreen(
|
||||
interactive = !isInPip,
|
||||
onVideoBoundsInWindowChanged = onVideoBoundsInWindowChanged,
|
||||
onImeCommitText = ::commitImeText,
|
||||
onInjectTouch = { action, pointerId, x, y, pressure, buttons ->
|
||||
onInjectTouch = { action, pointerId, x, y, pressure, actionButton, buttons ->
|
||||
withContext(Dispatchers.IO) {
|
||||
scrcpy.injectTouch(
|
||||
action = action,
|
||||
@@ -279,16 +374,31 @@ fun FullscreenControlScreen(
|
||||
screenWidth = session.width,
|
||||
screenHeight = session.height,
|
||||
pressure = pressure,
|
||||
actionButton = 0,
|
||||
actionButton = actionButton,
|
||||
buttons = buttons,
|
||||
)
|
||||
}
|
||||
},
|
||||
onBackOrScreenOn = { action ->
|
||||
withContext(Dispatchers.IO) {
|
||||
scrcpy.pressBackOrTurnScreenOn(action)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (showFullscreenVirtualButtons && !isInPip) {
|
||||
bar.Fullscreen(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
modifier = Modifier.align(
|
||||
when (fullscreenVirtualButtonDock) {
|
||||
VirtualButtonBar.FullscreenDock.TOP -> Alignment.TopCenter
|
||||
VirtualButtonBar.FullscreenDock.BOTTOM -> Alignment.BottomCenter
|
||||
VirtualButtonBar.FullscreenDock.LEFT -> Alignment.CenterStart
|
||||
VirtualButtonBar.FullscreenDock.RIGHT -> Alignment.CenterEnd
|
||||
}
|
||||
),
|
||||
dock = fullscreenVirtualButtonDock,
|
||||
reverseOrder = fullscreenVirtualButtonReverseOrder,
|
||||
thickness = fullscreenVirtualButtonHeight,
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
VirtualButtonAction.RECENT_TASKS -> {
|
||||
@@ -331,7 +441,7 @@ fun FullscreenControlScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (showFullscreenFloatingButton && !isInPip) {
|
||||
if (asBundle.showFullscreenFloatingButton && !isInPip) {
|
||||
bar.FloatingBall(
|
||||
actions = floatingActions,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -365,15 +475,13 @@ fun FullscreenControlScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
passwordPopupContent = if (fragmentActivity == null) {
|
||||
null
|
||||
} else {
|
||||
{ onDismissRequest ->
|
||||
passwordPopupContent =
|
||||
if (fragmentActivity == null) null
|
||||
else { onDismissRequest ->
|
||||
PasswordPickerPopupContent(
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -451,6 +559,101 @@ fun FullscreenControlScreen(
|
||||
}
|
||||
}
|
||||
|
||||
private fun dockForFixedPhysicalEdge(
|
||||
physicalDock: VirtualButtonBar.FullscreenDock,
|
||||
displayRotation: Int,
|
||||
) = when (displayRotation) {
|
||||
Surface.ROTATION_0 -> physicalDock
|
||||
Surface.ROTATION_90 -> when (physicalDock) {
|
||||
VirtualButtonBar.FullscreenDock.TOP -> VirtualButtonBar.FullscreenDock.LEFT
|
||||
VirtualButtonBar.FullscreenDock.BOTTOM -> VirtualButtonBar.FullscreenDock.RIGHT
|
||||
VirtualButtonBar.FullscreenDock.LEFT -> VirtualButtonBar.FullscreenDock.BOTTOM
|
||||
VirtualButtonBar.FullscreenDock.RIGHT -> VirtualButtonBar.FullscreenDock.TOP
|
||||
}
|
||||
|
||||
Surface.ROTATION_180 -> when (physicalDock) {
|
||||
VirtualButtonBar.FullscreenDock.TOP -> VirtualButtonBar.FullscreenDock.BOTTOM
|
||||
VirtualButtonBar.FullscreenDock.BOTTOM -> VirtualButtonBar.FullscreenDock.TOP
|
||||
VirtualButtonBar.FullscreenDock.LEFT -> VirtualButtonBar.FullscreenDock.RIGHT
|
||||
VirtualButtonBar.FullscreenDock.RIGHT -> VirtualButtonBar.FullscreenDock.LEFT
|
||||
}
|
||||
|
||||
Surface.ROTATION_270 -> when (physicalDock) {
|
||||
VirtualButtonBar.FullscreenDock.TOP -> VirtualButtonBar.FullscreenDock.RIGHT
|
||||
VirtualButtonBar.FullscreenDock.BOTTOM -> VirtualButtonBar.FullscreenDock.LEFT
|
||||
VirtualButtonBar.FullscreenDock.LEFT -> VirtualButtonBar.FullscreenDock.TOP
|
||||
VirtualButtonBar.FullscreenDock.RIGHT -> VirtualButtonBar.FullscreenDock.BOTTOM
|
||||
}
|
||||
|
||||
else -> physicalDock
|
||||
}
|
||||
|
||||
private enum class DockDirection {
|
||||
LEFT_TO_RIGHT,
|
||||
RIGHT_TO_LEFT,
|
||||
TOP_TO_BOTTOM,
|
||||
BOTTOM_TO_TOP,
|
||||
}
|
||||
|
||||
private fun isFixedDockOrderReversed(
|
||||
physicalDock: VirtualButtonBar.FullscreenDock,
|
||||
displayRotation: Int,
|
||||
): Boolean {
|
||||
val visualDock = dockForFixedPhysicalEdge(
|
||||
physicalDock = physicalDock,
|
||||
displayRotation = displayRotation,
|
||||
)
|
||||
val visualDirection = rotateDockDirection(
|
||||
direction = physicalDockBaseDirection(physicalDock),
|
||||
displayRotation = displayRotation,
|
||||
)
|
||||
return when (visualDock) {
|
||||
VirtualButtonBar.FullscreenDock.TOP,
|
||||
VirtualButtonBar.FullscreenDock.BOTTOM -> visualDirection == DockDirection.RIGHT_TO_LEFT
|
||||
|
||||
VirtualButtonBar.FullscreenDock.LEFT,
|
||||
VirtualButtonBar.FullscreenDock.RIGHT -> visualDirection == DockDirection.BOTTOM_TO_TOP
|
||||
}
|
||||
}
|
||||
|
||||
private fun physicalDockBaseDirection(
|
||||
physicalDock: VirtualButtonBar.FullscreenDock,
|
||||
) = when (physicalDock) {
|
||||
VirtualButtonBar.FullscreenDock.TOP -> DockDirection.RIGHT_TO_LEFT
|
||||
VirtualButtonBar.FullscreenDock.BOTTOM -> DockDirection.LEFT_TO_RIGHT
|
||||
VirtualButtonBar.FullscreenDock.LEFT -> DockDirection.TOP_TO_BOTTOM
|
||||
VirtualButtonBar.FullscreenDock.RIGHT -> DockDirection.BOTTOM_TO_TOP
|
||||
}
|
||||
|
||||
private fun rotateDockDirection(
|
||||
direction: DockDirection,
|
||||
displayRotation: Int,
|
||||
) = when (displayRotation) {
|
||||
Surface.ROTATION_0 -> direction
|
||||
Surface.ROTATION_90 -> when (direction) {
|
||||
DockDirection.LEFT_TO_RIGHT -> DockDirection.BOTTOM_TO_TOP
|
||||
DockDirection.RIGHT_TO_LEFT -> DockDirection.TOP_TO_BOTTOM
|
||||
DockDirection.TOP_TO_BOTTOM -> DockDirection.LEFT_TO_RIGHT
|
||||
DockDirection.BOTTOM_TO_TOP -> DockDirection.RIGHT_TO_LEFT
|
||||
}
|
||||
|
||||
Surface.ROTATION_180 -> when (direction) {
|
||||
DockDirection.LEFT_TO_RIGHT -> DockDirection.RIGHT_TO_LEFT
|
||||
DockDirection.RIGHT_TO_LEFT -> DockDirection.LEFT_TO_RIGHT
|
||||
DockDirection.TOP_TO_BOTTOM -> DockDirection.BOTTOM_TO_TOP
|
||||
DockDirection.BOTTOM_TO_TOP -> DockDirection.TOP_TO_BOTTOM
|
||||
}
|
||||
|
||||
Surface.ROTATION_270 -> when (direction) {
|
||||
DockDirection.LEFT_TO_RIGHT -> DockDirection.TOP_TO_BOTTOM
|
||||
DockDirection.RIGHT_TO_LEFT -> DockDirection.BOTTOM_TO_TOP
|
||||
DockDirection.TOP_TO_BOTTOM -> DockDirection.RIGHT_TO_LEFT
|
||||
DockDirection.BOTTOM_TO_TOP -> DockDirection.LEFT_TO_RIGHT
|
||||
}
|
||||
|
||||
else -> direction
|
||||
}
|
||||
|
||||
/**
|
||||
* FullscreenControlScreen
|
||||
*
|
||||
@@ -479,7 +682,16 @@ fun FullscreenControlPage(
|
||||
interactive: Boolean = true,
|
||||
onVideoBoundsInWindowChanged: (Rect?) -> Unit = {},
|
||||
onImeCommitText: suspend (String) -> Unit,
|
||||
onInjectTouch: suspend (action: Int, pointerId: Long, x: Int, y: Int, pressure: Float, buttons: Int) -> Unit,
|
||||
onInjectTouch: suspend (
|
||||
action: Int,
|
||||
pointerId: Long,
|
||||
x: Int,
|
||||
y: Int,
|
||||
pressure: Float,
|
||||
actionButton: Int,
|
||||
buttons: Int,
|
||||
) -> Unit,
|
||||
onBackOrScreenOn: suspend (action: Int) -> Unit,
|
||||
) {
|
||||
BackHandler(enabled = enableBackHandler, onBack = onDismiss)
|
||||
|
||||
@@ -506,7 +718,9 @@ fun FullscreenControlPage(
|
||||
activePointerDevicePositions = activePointerDevicePositions,
|
||||
pointerLabels = pointerLabels,
|
||||
nextPointerLabel = nextPointerLabel,
|
||||
mouseHoverEnabled = session.mouseHover,
|
||||
onInjectTouch = onInjectTouch,
|
||||
onBackOrScreenOn = onBackOrScreenOn,
|
||||
onActiveTouchCountChanged = { activeTouchCount = it },
|
||||
onActiveTouchDebugChanged = { activeTouchDebug = it },
|
||||
onNextPointerLabelChanged = { nextPointerLabel = it },
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.pages
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
|
||||
class ServerPicker(
|
||||
val pick: () -> Unit,
|
||||
)
|
||||
|
||||
val LocalServerPicker = staticCompositionLocalOf<ServerPicker> {
|
||||
error("No ServerPicker provided")
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import android.provider.OpenableColumns
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
@@ -39,6 +40,7 @@ import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -98,6 +100,37 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||
import top.yukonga.miuix.kmp.theme.ThemeController
|
||||
import top.yukonga.miuix.kmp.blur.layerBackdrop as miuixLayerBackdrop
|
||||
import java.io.File
|
||||
|
||||
private const val TERMINAL_FONT_RELATIVE_PATH = "terminal/font.ttf"
|
||||
|
||||
private fun terminalFontFile(context: android.content.Context): File {
|
||||
return File(context.filesDir, TERMINAL_FONT_RELATIVE_PATH)
|
||||
}
|
||||
|
||||
private fun copyTerminalFontToPrivate(context: android.content.Context, uri: Uri) {
|
||||
val target = terminalFontFile(context)
|
||||
target.parentFile?.mkdirs()
|
||||
context.contentResolver.openInputStream(uri).use { input ->
|
||||
requireNotNull(input) { "无法读取字体文件" }
|
||||
target.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryDisplayName(context: android.content.Context, uri: Uri): String? {
|
||||
return context.contentResolver
|
||||
.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
|
||||
?.use { cursor ->
|
||||
val columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (columnIndex >= 0 && cursor.moveToFirst()) {
|
||||
cursor.getString(columnIndex)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class MainBottomTabDestination(
|
||||
val label: String,
|
||||
@@ -155,6 +188,7 @@ fun MainScreen() {
|
||||
var lastExitBackPressAtMs by rememberSaveable { mutableLongStateOf(0L) }
|
||||
var fileTabCanNavigateUp by remember { mutableStateOf(false) }
|
||||
var fileTabNavigateUp by remember { mutableStateOf<(() -> Boolean)?>(null) }
|
||||
var terminalGestureLock by remember { mutableStateOf(false) }
|
||||
|
||||
// Scroll behaviors
|
||||
val devicesPageScrollBehavior = MiuixScrollBehavior(
|
||||
@@ -282,6 +316,43 @@ fun MainScreen() {
|
||||
)
|
||||
}
|
||||
}
|
||||
val terminalFontDocumentPicker = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
taskScope.launch {
|
||||
val result = runCatching {
|
||||
val displayName = queryDisplayName(context, uri)
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: "font.ttf"
|
||||
copyTerminalFontToPrivate(context, uri)
|
||||
displayName
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
result.onSuccess { displayName ->
|
||||
asBundle = asBundle.copy(terminalFontDisplayName = displayName)
|
||||
snackbarController.show("终端字体导入成功")
|
||||
}.onFailure { error ->
|
||||
snackbarController.show(
|
||||
"终端字体导入失败: ${error.message ?: error.javaClass.simpleName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val terminalFontPicker = remember(terminalFontDocumentPicker) {
|
||||
TerminalFontPicker {
|
||||
terminalFontDocumentPicker.launch(
|
||||
arrayOf(
|
||||
"font/ttf",
|
||||
"font/otf",
|
||||
"application/x-font-ttf",
|
||||
"application/x-font-otf",
|
||||
"*/*",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Derived flags
|
||||
val canNavigateBack = rootBackStack.size > 1
|
||||
@@ -451,6 +522,7 @@ fun MainScreen() {
|
||||
),
|
||||
state = pagerState,
|
||||
beyondViewportPageCount = 1,
|
||||
userScrollEnabled = !(selectedTabIndex == MainBottomTabDestination.Terminal.ordinal && terminalGestureLock),
|
||||
) { page ->
|
||||
val tab = tabs[page]
|
||||
saveableStateHolder.SaveableStateProvider(tab.name) {
|
||||
@@ -464,6 +536,10 @@ fun MainScreen() {
|
||||
|
||||
MainBottomTabDestination.Terminal -> TerminalScreen(
|
||||
bottomInnerPadding = bottomInnerPadding,
|
||||
isActive = selectedTabIndex == MainBottomTabDestination.Terminal.ordinal,
|
||||
onTerminalGestureLockChanged = { locked ->
|
||||
terminalGestureLock = locked
|
||||
},
|
||||
)
|
||||
|
||||
MainBottomTabDestination.Files -> FileManagerScreen(
|
||||
@@ -575,6 +651,7 @@ fun MainScreen() {
|
||||
LocalSnackbarController provides snackbarController,
|
||||
LocalAppHaptics provides haptics,
|
||||
LocalServerPicker provides serverPicker,
|
||||
LocalTerminalFontPicker provides terminalFontPicker,
|
||||
) {
|
||||
NavDisplay(
|
||||
entries = rootEntries,
|
||||
@@ -583,3 +660,19 @@ fun MainScreen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ServerPicker(
|
||||
val pick: () -> Unit,
|
||||
)
|
||||
|
||||
class TerminalFontPicker(
|
||||
val pick: () -> Unit,
|
||||
)
|
||||
|
||||
val LocalServerPicker = staticCompositionLocalOf<ServerPicker> {
|
||||
error("No ServerPicker provided")
|
||||
}
|
||||
|
||||
val LocalTerminalFontPicker = staticCompositionLocalOf<TerminalFontPicker> {
|
||||
error("No TerminalFontPicker provided")
|
||||
}
|
||||
|
||||
@@ -1008,11 +1008,7 @@ internal fun ScrcpyAllOptionsPage(
|
||||
soBundle = soBundle.copy(
|
||||
killAdbOnClose = it
|
||||
)
|
||||
if (it) snackbar.show(
|
||||
"未实现"
|
||||
)
|
||||
},
|
||||
enabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1615,6 +1611,16 @@ internal fun ScrcpyAllOptionsPage(
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "禁用鼠标悬停转发",
|
||||
summary = "--no-mouse-hover",
|
||||
checked = !soBundle.mouseHover,
|
||||
onCheckedChange = {
|
||||
soBundle = soBundle.copy(
|
||||
mouseHover = !it
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "禁用结束后清理",
|
||||
summary = "--no-cleanup",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.pages
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Process
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -43,17 +42,20 @@ import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
||||
import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker
|
||||
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.PreferenceMigration
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings.FullscreenVirtualButtonDock
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.MultiGroupsDropdownGroup
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.MultiGroupsDropdownPreference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
@@ -67,10 +69,21 @@ import top.yukonga.miuix.kmp.preference.ArrowPreference
|
||||
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
||||
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||
import java.io.File
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.system.exitProcess
|
||||
import android.provider.Settings as AndroidSettings
|
||||
|
||||
private const val TERMINAL_FONT_RELATIVE_PATH = "terminal/font.ttf"
|
||||
|
||||
private fun terminalFontFile(context: android.content.Context): File {
|
||||
return File(context.filesDir, TERMINAL_FONT_RELATIVE_PATH)
|
||||
}
|
||||
|
||||
private fun clearTerminalFont(context: android.content.Context): Boolean {
|
||||
val target = terminalFontFile(context)
|
||||
return target.exists() && target.delete()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
scrollBehavior: ScrollBehavior,
|
||||
@@ -117,11 +130,7 @@ fun SettingsPage(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val appContext = context.applicationContext
|
||||
var needMigration by remember { mutableStateOf(false) }
|
||||
val updateState by AppUpdateChecker.state.collectAsState()
|
||||
LaunchedEffect(Unit) {
|
||||
needMigration = PreferenceMigration(appContext).needsMigration()
|
||||
}
|
||||
|
||||
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -129,6 +138,7 @@ fun SettingsPage(
|
||||
val snackbar = LocalSnackbarController.current
|
||||
val navigator = LocalRootNavigator.current
|
||||
val serverPicker = LocalServerPicker.current
|
||||
val terminalFontPicker = LocalTerminalFontPicker.current
|
||||
val isScrcpyStreaming = AppRuntime.scrcpy?.isStarted() == true
|
||||
|
||||
val asBundleShared by appSettings.bundleState.collectAsState()
|
||||
@@ -156,6 +166,10 @@ fun SettingsPage(
|
||||
|
||||
val themeItems = rememberSaveable { ThemeModes.baseOptions.map { it.label } }
|
||||
|
||||
val fullscreenVirtualButtonDock = remember(asBundle.fullscreenVirtualButtonDock) {
|
||||
FullscreenVirtualButtonDock.fromStoredValue(asBundle.fullscreenVirtualButtonDock)
|
||||
}
|
||||
|
||||
val customServerVersionShowInput = rememberSaveable(asBundle.customServerUri) {
|
||||
asBundle.customServerUri.isNotBlank()
|
||||
}
|
||||
@@ -206,13 +220,7 @@ fun SettingsPage(
|
||||
Card {
|
||||
OverlayDropdownPreference(
|
||||
title = "外观模式",
|
||||
summary = ThemeModes.baseOptions
|
||||
.getOrNull(
|
||||
asBundle.themeBaseIndex
|
||||
.coerceIn(0, ThemeModes.baseOptions.lastIndex)
|
||||
)
|
||||
?.label
|
||||
?: "跟随系统",
|
||||
summary = "选择应用的外观模式",
|
||||
items = themeItems,
|
||||
selectedIndex = asBundle.themeBaseIndex
|
||||
.coerceIn(0, ThemeModes.baseOptions.lastIndex),
|
||||
@@ -305,11 +313,12 @@ fun SettingsPage(
|
||||
value = asBundle.devicePreviewCardHeightDp.toFloat(),
|
||||
onValueChange = {
|
||||
asBundle = asBundle.copy(
|
||||
devicePreviewCardHeightDp = it.roundToInt().coerceAtLeast(120)
|
||||
devicePreviewCardHeightDp =
|
||||
it.roundToInt().coerceAtLeast(120)
|
||||
)
|
||||
},
|
||||
valueRange = 160f..600f,
|
||||
steps = 600 - 160 - 2,
|
||||
steps = 600 - 160 - 1,
|
||||
unit = "dp",
|
||||
displayFormatter = { it.roundToInt().toString() },
|
||||
inputInitialValue = asBundle.devicePreviewCardHeightDp.toString(),
|
||||
@@ -340,22 +349,178 @@ fun SettingsPage(
|
||||
context.startActivity(LockscreenPasswordActivity.createIntent(context))
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "全屏时不跟随系统旋转锁定",
|
||||
summary = "启用后使用传感器方向,忽略系统自动旋转锁定状态",
|
||||
checked = asBundle.fullscreenControlIgnoreSystemRotationLock,
|
||||
onCheckedChange = {
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenControlIgnoreSystemRotationLock = it
|
||||
)
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
title = "全屏显示虚拟按钮",
|
||||
summary = "在全屏控制页底部显示返回键、主页键等虚拟按钮",
|
||||
summary = "在全屏控制页中显示返回键、主页键等虚拟按钮",
|
||||
checked = asBundle.showFullscreenVirtualButtons,
|
||||
onCheckedChange = {
|
||||
asBundle = asBundle.copy(showFullscreenVirtualButtons = it)
|
||||
},
|
||||
)
|
||||
AnimatedVisibility(asBundle.showFullscreenVirtualButtons) {
|
||||
Column {
|
||||
MultiGroupsDropdownPreference(
|
||||
title = "全屏虚拟按钮方向",
|
||||
summary = fullscreenVirtualButtonDock.summary,
|
||||
groups = listOf(
|
||||
MultiGroupsDropdownGroup(
|
||||
options = FullscreenVirtualButtonDock.modeItems,
|
||||
selectedIndex = fullscreenVirtualButtonDock.modeIndex,
|
||||
onSelectedIndexChange = { modeIndex ->
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenVirtualButtonDock = FullscreenVirtualButtonDock
|
||||
.fromModeAndDirection(
|
||||
modeIndex = modeIndex,
|
||||
directionIndex = fullscreenVirtualButtonDock.directionIndex,
|
||||
)
|
||||
.toStoredValue()
|
||||
)
|
||||
},
|
||||
),
|
||||
MultiGroupsDropdownGroup(
|
||||
options = FullscreenVirtualButtonDock.directionItems,
|
||||
selectedIndex = fullscreenVirtualButtonDock.directionIndex,
|
||||
onSelectedIndexChange = { directionIndex ->
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenVirtualButtonDock = FullscreenVirtualButtonDock
|
||||
.fromModeAndDirection(
|
||||
modeIndex = fullscreenVirtualButtonDock.modeIndex,
|
||||
directionIndex = directionIndex,
|
||||
)
|
||||
.toStoredValue()
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
SuperSlider(
|
||||
title = "全屏虚拟按钮高度",
|
||||
summary = "调整全屏控制页中虚拟按钮栏的高度",
|
||||
value = asBundle.fullscreenVirtualButtonHeightDp.toFloat(),
|
||||
onValueChange = {
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenVirtualButtonHeightDp =
|
||||
it.roundToInt().coerceIn(16, 80)
|
||||
)
|
||||
},
|
||||
valueRange = 16f..80f,
|
||||
steps = 80 - 16 - 1,
|
||||
unit = "dp",
|
||||
displayFormatter = { it.roundToInt().toString() },
|
||||
inputInitialValue = asBundle.fullscreenVirtualButtonHeightDp.toString(),
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 1f..160f,
|
||||
onInputConfirm = { input ->
|
||||
input.toIntOrNull()?.let {
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenVirtualButtonHeightDp =
|
||||
it.coerceIn(1, 160)
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
SwitchPreference(
|
||||
title = "全屏显示悬浮球",
|
||||
summary = "在全屏控制页显示可拖动的悬浮球,点击后弹出完整虚拟按键菜单",
|
||||
summary = "在全屏控制页中显示可拖动的悬浮球,点击后弹出完整虚拟按键菜单",
|
||||
checked = asBundle.showFullscreenFloatingButton,
|
||||
onCheckedChange = {
|
||||
asBundle = asBundle.copy(showFullscreenFloatingButton = it)
|
||||
},
|
||||
)
|
||||
AnimatedVisibility(asBundle.showFullscreenFloatingButton) {
|
||||
Column {
|
||||
SuperSlider(
|
||||
title = "全屏悬浮球尺寸",
|
||||
summary = "调整全屏控制页悬浮球大小",
|
||||
value = asBundle.fullscreenFloatingButtonSizeDp.toFloat(),
|
||||
onValueChange = {
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenFloatingButtonSizeDp =
|
||||
it.roundToInt().coerceIn(32, 64)
|
||||
)
|
||||
},
|
||||
valueRange = 32f..64f,
|
||||
steps = 64 - 32 - 1,
|
||||
unit = "dp",
|
||||
displayFormatter = { it.roundToInt().toString() },
|
||||
inputInitialValue = asBundle.fullscreenFloatingButtonSizeDp.toString(),
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 16f..96f,
|
||||
onInputConfirm = { input ->
|
||||
input.toIntOrNull()?.let {
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenFloatingButtonSizeDp =
|
||||
it.coerceIn(16, 96)
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
SuperSlider(
|
||||
title = "悬浮球背景透明度",
|
||||
summary = "调整悬浮球背景透明度",
|
||||
value = asBundle.fullscreenFloatingButtonBackgroundAlphaPercent.toFloat(),
|
||||
onValueChange = {
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenFloatingButtonBackgroundAlphaPercent =
|
||||
it.roundToInt().coerceIn(10, 100)
|
||||
)
|
||||
},
|
||||
valueRange = 10f..100f,
|
||||
steps = 100 - 10 - 1,
|
||||
unit = "%",
|
||||
displayFormatter = { it.roundToInt().toString() },
|
||||
inputInitialValue = asBundle.fullscreenFloatingButtonBackgroundAlphaPercent.toString(),
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 10f..100f,
|
||||
onInputConfirm = { input ->
|
||||
input.toIntOrNull()?.let {
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenFloatingButtonBackgroundAlphaPercent =
|
||||
it.coerceIn(10, 100)
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
SuperSlider(
|
||||
title = "白环透明度",
|
||||
summary = "调整悬浮球白环透明度",
|
||||
value = asBundle.fullscreenFloatingButtonRingAlphaPercent.toFloat(),
|
||||
onValueChange = {
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenFloatingButtonRingAlphaPercent =
|
||||
it.roundToInt().coerceIn(0, 100)
|
||||
)
|
||||
},
|
||||
valueRange = 0f..100f,
|
||||
steps = 100 - 0 - 1,
|
||||
unit = "%",
|
||||
displayFormatter = { it.roundToInt().toString() },
|
||||
inputInitialValue = asBundle.fullscreenFloatingButtonRingAlphaPercent.toString(),
|
||||
inputFilter = { it.filter(Char::isDigit) },
|
||||
inputValueRange = 0f..100f,
|
||||
onInputConfirm = { input ->
|
||||
input.toIntOrNull()?.let {
|
||||
asBundle = asBundle.copy(
|
||||
fullscreenFloatingButtonRingAlphaPercent =
|
||||
it.coerceIn(0, 100)
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
SwitchPreference(
|
||||
title = "实时同步剪贴板到受控机",
|
||||
summary =
|
||||
@@ -366,7 +531,9 @@ fun SettingsPage(
|
||||
""".trimIndent(),
|
||||
checked = asBundle.realtimeClipboardSyncToDevice,
|
||||
onCheckedChange = {
|
||||
asBundle = asBundle.copy(realtimeClipboardSyncToDevice = it)
|
||||
asBundle = asBundle.copy(
|
||||
realtimeClipboardSyncToDevice = it
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -592,38 +759,90 @@ fun SettingsPage(
|
||||
}
|
||||
}
|
||||
|
||||
if (needMigration) item {
|
||||
// 这部分应该不会显示出来,
|
||||
// 应用启动时就会执行迁移与旧数据的删除
|
||||
SectionSmallTitle("应用")
|
||||
item {
|
||||
SectionSmallTitle("终端")
|
||||
Card {
|
||||
ArrowPreference(
|
||||
title = "恢复旧版本配置",
|
||||
summary = "从旧版本的 SharedPreferences 恢复至 DataStore",
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val migration = PreferenceMigration(appContext)
|
||||
migration.migrate(clearSharedPrefs = false)
|
||||
snackbar.show("迁移完成,应用将重启")
|
||||
|
||||
delay(1000)
|
||||
|
||||
val intent = context.packageManager.getLaunchIntentForPackage(
|
||||
context.packageName
|
||||
SuperSlider(
|
||||
title = "终端字号",
|
||||
summary = "也可以在终端上双指缩放调整",
|
||||
value = asBundle.terminalFontSizeSp,
|
||||
onValueChange = {
|
||||
asBundle = asBundle.copy(
|
||||
terminalFontSizeSp = it.roundToInt().toFloat()
|
||||
)
|
||||
},
|
||||
valueRange = 1f..32f,
|
||||
steps = 32 - 1 - 1,
|
||||
unit = "sp",
|
||||
displayFormatter = { it.roundToInt().toString() },
|
||||
inputInitialValue = asBundle.terminalFontSizeSp.roundToInt().toString(),
|
||||
inputFilter = { input -> input.filter(Char::isDigit) },
|
||||
inputValueRange = 1f..32f,
|
||||
onInputConfirm = { input ->
|
||||
input.toIntOrNull()?.let {
|
||||
asBundle = asBundle.copy(
|
||||
terminalFontSizeSp = it.coerceIn(1, 32).toFloat()
|
||||
)
|
||||
intent?.apply {
|
||||
addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(0)
|
||||
}
|
||||
},
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = UiSpacing.Large),
|
||||
verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = UiSpacing.Large),
|
||||
verticalArrangement = Arrangement.spacedBy(UiSpacing.Medium),
|
||||
) {
|
||||
Text(
|
||||
text = "自定义终端字体",
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
TextField(
|
||||
value = asBundle.terminalFontDisplayName,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = "系统等宽字体",
|
||||
useLabelAsPlaceholder = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = {
|
||||
Row(
|
||||
modifier = Modifier.padding(end = UiSpacing.Medium),
|
||||
) {
|
||||
if (asBundle.terminalFontDisplayName.isNotBlank()) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val cleared = withContext(Dispatchers.IO) {
|
||||
clearTerminalFont(context)
|
||||
}
|
||||
asBundle = asBundle.copy(
|
||||
terminalFontDisplayName = ""
|
||||
)
|
||||
snackbar.show(
|
||||
if (cleared) "已恢复默认终端字体"
|
||||
else "当前没有可清除的自定义字体"
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Clear,
|
||||
contentDescription = "清空",
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = terminalFontPicker.pick) {
|
||||
Icon(
|
||||
Icons.Rounded.FileOpen,
|
||||
contentDescription = "选择字体",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,12 @@ fun StreamScreen(activity: StreamActivity) {
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(activity) {
|
||||
onDispose {
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(
|
||||
activity, isInPip,
|
||||
currentSession?.width, currentSession?.height,
|
||||
@@ -112,11 +118,20 @@ fun StreamScreen(activity: StreamActivity) {
|
||||
onBack = activity::finish,
|
||||
isInPip = isInPip,
|
||||
onVideoSizeChanged = { width, height ->
|
||||
// 只在全屏时跟随视频方向
|
||||
if (!isInPip)
|
||||
if (!isInPip) {
|
||||
activity.requestedOrientation =
|
||||
if (width >= height) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
if (width >= height) {
|
||||
if (asBundle.fullscreenControlIgnoreSystemRotationLock)
|
||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
else
|
||||
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||
} else {
|
||||
if (asBundle.fullscreenControlIgnoreSystemRotationLock)
|
||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
||||
else
|
||||
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
||||
}
|
||||
}
|
||||
},
|
||||
onVideoBoundsInWindowChanged = {
|
||||
// 记录下一次进入 PiP 时可用的 sourceRectHint
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.pages
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Typeface
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.ViewConfiguration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ContentCopy
|
||||
import androidx.compose.material.icons.rounded.MoreVert
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -30,31 +32,40 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.termux.terminal.KeyHandler
|
||||
import com.termux.terminal.TerminalSession
|
||||
import com.termux.terminal.TextStyle
|
||||
import com.termux.view.TerminalView
|
||||
import com.termux.view.TerminalViewClient
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.UiSpacing
|
||||
import io.github.miuzarte.scrcpyforandroid.nativecore.AdbSocketStream
|
||||
import io.github.miuzarte.scrcpyforandroid.nativecore.NativeAdbService
|
||||
import io.github.miuzarte.scrcpyforandroid.services.LocalInputService
|
||||
import io.github.miuzarte.scrcpyforandroid.services.LocalSnackbarController
|
||||
import io.github.miuzarte.scrcpyforandroid.storage.Storage.appSettings
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.BlurredBar
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.LocalEnableBlur
|
||||
import io.github.miuzarte.scrcpyforandroid.ui.rememberBlurBackdrop
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.PopupMenuItem
|
||||
import io.github.miuzarte.scrcpyforandroid.widgets.TerminalInputView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.ListPopupColumn
|
||||
@@ -69,19 +80,33 @@ import top.yukonga.miuix.kmp.blur.layerBackdrop
|
||||
import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet
|
||||
import top.yukonga.miuix.kmp.overlay.OverlayListPopup
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import android.view.KeyEvent as AndroidKeyEvent
|
||||
|
||||
private const val DEFAULT_TERMINAL_FONT_SIZE_SP = 14f
|
||||
private const val MIN_TERMINAL_FONT_SIZE_SP = 1f
|
||||
private const val MAX_TERMINAL_FONT_SIZE_SP = 32f
|
||||
private const val FONT_SCALE_STEP_THRESHOLD = 1.08f
|
||||
private const val LOG_TAG = "TerminalScreen"
|
||||
private const val TERMINAL_FONT_RELATIVE_PATH = "terminal/font.ttf"
|
||||
|
||||
private fun terminalFontFile(context: android.content.Context): File {
|
||||
return File(context.filesDir, TERMINAL_FONT_RELATIVE_PATH)
|
||||
}
|
||||
|
||||
private fun loadTerminalTypeface(context: android.content.Context): Typeface? {
|
||||
val file = terminalFontFile(context)
|
||||
if (!file.exists()) return null
|
||||
return runCatching { Typeface.createFromFile(file) }.getOrNull()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TerminalScreen(
|
||||
bottomInnerPadding: Dp,
|
||||
isActive: Boolean,
|
||||
onTerminalGestureLockChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val snackbar = LocalSnackbarController.current
|
||||
@@ -126,7 +151,7 @@ fun TerminalScreen(
|
||||
},
|
||||
)
|
||||
PopupMenuItem(
|
||||
text = "清空输出",
|
||||
text = "清屏",
|
||||
optionSize = 2,
|
||||
index = 1,
|
||||
onSelectedIndexChange = {
|
||||
@@ -150,6 +175,8 @@ fun TerminalScreen(
|
||||
TerminalPage(
|
||||
contentPadding = pagePadding,
|
||||
bottomInnerPadding = bottomInnerPadding,
|
||||
isActive = isActive,
|
||||
onTerminalGestureLockChanged = onTerminalGestureLockChanged,
|
||||
output = output,
|
||||
onOutputChange = { output = it },
|
||||
)
|
||||
@@ -173,104 +200,133 @@ fun TerminalScreen(
|
||||
private fun TerminalPage(
|
||||
contentPadding: PaddingValues,
|
||||
bottomInnerPadding: Dp,
|
||||
isActive: Boolean,
|
||||
onTerminalGestureLockChanged: (Boolean) -> Unit,
|
||||
output: String,
|
||||
onOutputChange: (String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val snackbar = LocalSnackbarController.current
|
||||
val density = LocalDensity.current
|
||||
val snackbar = LocalSnackbarController.current
|
||||
val asBundleShared by appSettings.bundleState.collectAsState()
|
||||
val uiScope = rememberCoroutineScope()
|
||||
val taskScope = remember { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||
val touchSlop = remember(context) { ViewConfiguration.get(context).scaledTouchSlop }
|
||||
val outputScrollState = rememberScrollState()
|
||||
var outputBuffer by rememberSaveable { mutableStateOf(output) }
|
||||
var terminalFontSizeSp by rememberSaveable { mutableFloatStateOf(DEFAULT_TERMINAL_FONT_SIZE_SP) }
|
||||
val shellWriteMutex = remember { Mutex() }
|
||||
var terminalFontSizeSp by rememberSaveable(asBundleShared.terminalFontSizeSp) {
|
||||
mutableFloatStateOf(asBundleShared.terminalFontSizeSp)
|
||||
}
|
||||
var shellReady by remember { mutableStateOf(false) }
|
||||
var shellConnecting by remember { mutableStateOf(false) }
|
||||
var shellStream by remember { mutableStateOf<AdbSocketStream?>(null) }
|
||||
var terminalInputView by remember { mutableStateOf<TerminalInputView?>(null) }
|
||||
var showKeyboardWhenReady by remember { mutableStateOf(false) }
|
||||
var pinchInProgress by remember { mutableStateOf(false) }
|
||||
var touchDownX by remember { mutableFloatStateOf(0f) }
|
||||
var touchDownY by remember { mutableFloatStateOf(0f) }
|
||||
var tapPending by remember { mutableStateOf(false) }
|
||||
var terminalView by remember { mutableStateOf<TerminalView?>(null) }
|
||||
val imeBottomDp = with(density) { WindowInsets.ime.getBottom(this).toDp() }
|
||||
var ctrlLatched by rememberSaveable { mutableStateOf(false) }
|
||||
var altLatched by rememberSaveable { mutableStateOf(false) }
|
||||
var pendingLatchedConsume by remember { mutableStateOf(false) }
|
||||
var pinchGestureLock by remember { mutableStateOf(false) }
|
||||
var customTypeface by remember { mutableStateOf(loadTerminalTypeface(context)) }
|
||||
val sessionHolder = remember { arrayOfNulls<TerminalSession>(1) }
|
||||
val terminalSurfaceColorArgb = colorScheme.surface.toArgb()
|
||||
val terminalOnSurfaceColorArgb = colorScheme.onSurface.toArgb()
|
||||
val terminalCursorColorArgb =
|
||||
if (colorScheme.surface.luminance() < 0.5f) 0xffffffff.toInt()
|
||||
else 0xff000000.toInt()
|
||||
|
||||
fun commitOutput(text: String) {
|
||||
outputBuffer = text
|
||||
onOutputChange(text)
|
||||
}
|
||||
|
||||
fun appendRawOutput(text: String) {
|
||||
if (text.isEmpty()) {
|
||||
return
|
||||
LaunchedEffect(asBundleShared.terminalFontSizeSp) {
|
||||
if (terminalFontSizeSp != asBundleShared.terminalFontSizeSp) {
|
||||
terminalFontSizeSp = asBundleShared.terminalFontSizeSp
|
||||
}
|
||||
val builder = StringBuilder(outputBuffer)
|
||||
text.forEach { ch ->
|
||||
when (ch) {
|
||||
'\b',
|
||||
'\u007F' -> if (builder.isNotEmpty()) builder.deleteCharAt(builder.lastIndex)
|
||||
|
||||
else -> builder.append(ch)
|
||||
}
|
||||
}
|
||||
commitOutput(builder.toString())
|
||||
}
|
||||
|
||||
fun appendOutputLine(text: String) {
|
||||
commitOutput(
|
||||
buildString {
|
||||
append(outputBuffer)
|
||||
if (isNotEmpty() && !endsWith("\n")) {
|
||||
append("\n")
|
||||
}
|
||||
append(text)
|
||||
if (!endsWith("\n")) {
|
||||
append("\n")
|
||||
}
|
||||
}
|
||||
)
|
||||
fun extractTranscript(session: TerminalSession): String {
|
||||
val screen = session.emulator.getScreen()
|
||||
return screen.getSelectedText(
|
||||
selX1 = 0,
|
||||
selY1 = -screen.activeTranscriptRows,
|
||||
selX2 = session.emulator.mColumns,
|
||||
selY2 = session.emulator.mRows - 1,
|
||||
joinBackLines = false,
|
||||
joinFullLines = false,
|
||||
).trim('\n')
|
||||
}
|
||||
|
||||
fun writeToShell(text: String) {
|
||||
fun syncOutput() {
|
||||
val session = sessionHolder[0] ?: return
|
||||
onOutputChange(extractTranscript(session))
|
||||
terminalView?.onScreenUpdated()
|
||||
}
|
||||
|
||||
fun writeBytesToShell(
|
||||
data: ByteArray,
|
||||
offset: Int,
|
||||
count: Int,
|
||||
) {
|
||||
val stream = shellStream
|
||||
if (stream == null || !shellReady || stream.closed) {
|
||||
snackbar.show("终端会话尚未就绪")
|
||||
return
|
||||
}
|
||||
val payload = data.copyOfRange(offset, offset + count)
|
||||
taskScope.launch {
|
||||
val result = runCatching {
|
||||
stream.outputStream.write(text.toByteArray(StandardCharsets.UTF_8))
|
||||
stream.outputStream.flush()
|
||||
shellWriteMutex.withLock {
|
||||
stream.outputStream.write(payload)
|
||||
stream.outputStream.flush()
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
result.onFailure { error ->
|
||||
appendOutputLine("错误: ${error.message ?: error.javaClass.simpleName}")
|
||||
snackbar.show("终端输入失败")
|
||||
snackbar.show("终端输入失败: ${error.message ?: error.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun mapKeyEventToShellText(event: AndroidKeyEvent): String? {
|
||||
if (event.action != AndroidKeyEvent.ACTION_DOWN) {
|
||||
return ""
|
||||
}
|
||||
return when (event.keyCode) {
|
||||
AndroidKeyEvent.KEYCODE_ENTER -> "\n"
|
||||
AndroidKeyEvent.KEYCODE_DEL -> "\b"
|
||||
AndroidKeyEvent.KEYCODE_TAB -> "\t"
|
||||
AndroidKeyEvent.KEYCODE_DPAD_UP -> "\u001B[A"
|
||||
AndroidKeyEvent.KEYCODE_DPAD_DOWN -> "\u001B[B"
|
||||
AndroidKeyEvent.KEYCODE_DPAD_RIGHT -> "\u001B[C"
|
||||
AndroidKeyEvent.KEYCODE_DPAD_LEFT -> "\u001B[D"
|
||||
else -> null
|
||||
fun writeClipboardToShell() {
|
||||
val text = LocalInputService.getClipboardText(context)
|
||||
if (!text.isNullOrBlank()) {
|
||||
val bytes = text.toByteArray(StandardCharsets.UTF_8)
|
||||
writeBytesToShell(bytes, 0, bytes.size)
|
||||
}
|
||||
}
|
||||
|
||||
fun showKeyboard() {
|
||||
val inputView = terminalInputView ?: return
|
||||
LocalInputService.showSoftKeyboard(inputView)
|
||||
val terminalSession = remember {
|
||||
TerminalSession(
|
||||
shellWriter = ::writeBytesToShell,
|
||||
onScreenUpdated = ::syncOutput,
|
||||
onCopyTextToClipboardRequested = { text ->
|
||||
LocalInputService.setClipboardText(context, text)
|
||||
uiScope.launch {
|
||||
snackbar.show("已复制到剪贴板")
|
||||
}
|
||||
},
|
||||
onPasteTextFromClipboardRequested = ::writeClipboardToShell,
|
||||
onBellRequested = {},
|
||||
)
|
||||
}
|
||||
sessionHolder[0] = terminalSession
|
||||
|
||||
fun applyTerminalThemeColors() {
|
||||
val colors = terminalSession.emulator.mColors.mCurrentColors
|
||||
colors[TextStyle.COLOR_INDEX_BACKGROUND] = terminalSurfaceColorArgb
|
||||
colors[TextStyle.COLOR_INDEX_FOREGROUND] = terminalOnSurfaceColorArgb
|
||||
colors[TextStyle.COLOR_INDEX_CURSOR] = terminalCursorColorArgb
|
||||
}
|
||||
|
||||
fun updateTerminalFontSize(newValue: Float) {
|
||||
val clamped = newValue.coerceIn(MIN_TERMINAL_FONT_SIZE_SP, MAX_TERMINAL_FONT_SIZE_SP)
|
||||
if (clamped == terminalFontSizeSp) {
|
||||
return
|
||||
}
|
||||
terminalFontSizeSp = clamped
|
||||
terminalView?.setTextSize(with(density) { clamped.sp.roundToPx() })
|
||||
uiScope.launch {
|
||||
val latest = appSettings.bundleState.value
|
||||
if (latest.terminalFontSizeSp != clamped) {
|
||||
appSettings.updateBundle {
|
||||
it.copy(terminalFontSizeSp = clamped)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showFontSizeSnackbar() {
|
||||
@@ -283,68 +339,46 @@ private fun TerminalPage(
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
terminalFontSizeSp = DEFAULT_TERMINAL_FONT_SIZE_SP
|
||||
updateTerminalFontSize(DEFAULT_TERMINAL_FONT_SIZE_SP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val scaleGestureDetector = remember {
|
||||
var scaleAccumulator = 1f
|
||||
ScaleGestureDetector(
|
||||
context,
|
||||
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
||||
pinchInProgress = true
|
||||
scaleAccumulator = 1f
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
scaleAccumulator *= detector.scaleFactor
|
||||
var updated = false
|
||||
while (scaleAccumulator >= FONT_SCALE_STEP_THRESHOLD) {
|
||||
val nextSize = (terminalFontSizeSp + 1f)
|
||||
.coerceAtMost(MAX_TERMINAL_FONT_SIZE_SP)
|
||||
if (nextSize == terminalFontSizeSp) {
|
||||
scaleAccumulator = 1f
|
||||
break
|
||||
}
|
||||
terminalFontSizeSp = nextSize
|
||||
scaleAccumulator /= FONT_SCALE_STEP_THRESHOLD
|
||||
updated = true
|
||||
}
|
||||
while (scaleAccumulator <= 1f / FONT_SCALE_STEP_THRESHOLD) {
|
||||
val nextSize = (terminalFontSizeSp - 1f)
|
||||
.coerceAtLeast(MIN_TERMINAL_FONT_SIZE_SP)
|
||||
if (nextSize == terminalFontSizeSp) {
|
||||
scaleAccumulator = 1f
|
||||
break
|
||||
}
|
||||
terminalFontSizeSp = nextSize
|
||||
scaleAccumulator *= FONT_SCALE_STEP_THRESHOLD
|
||||
updated = true
|
||||
}
|
||||
if (updated) {
|
||||
showFontSizeSnackbar()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScaleEnd(detector: ScaleGestureDetector) {
|
||||
pinchInProgress = false
|
||||
}
|
||||
},
|
||||
)
|
||||
fun handleTerminalTouchInterception(
|
||||
view: TerminalView,
|
||||
event: MotionEvent,
|
||||
): Boolean {
|
||||
val shouldLock = event.pointerCount > 1 || event.actionMasked == MotionEvent.ACTION_POINTER_DOWN
|
||||
if (shouldLock) {
|
||||
view.parent?.requestDisallowInterceptTouchEvent(true)
|
||||
if (!pinchGestureLock) {
|
||||
pinchGestureLock = true
|
||||
onTerminalGestureLockChanged(true)
|
||||
}
|
||||
}
|
||||
val shouldUnlock = event.actionMasked == MotionEvent.ACTION_UP ||
|
||||
event.actionMasked == MotionEvent.ACTION_CANCEL ||
|
||||
(event.actionMasked == MotionEvent.ACTION_POINTER_UP && event.pointerCount <= 2)
|
||||
if (shouldUnlock) {
|
||||
view.parent?.requestDisallowInterceptTouchEvent(false)
|
||||
if (pinchGestureLock) {
|
||||
pinchGestureLock = false
|
||||
onTerminalGestureLockChanged(false)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun openShellSession(showKeyboardAfterConnect: Boolean) {
|
||||
if (showKeyboardAfterConnect) {
|
||||
showKeyboardWhenReady = true
|
||||
}
|
||||
if (shellStream != null || shellConnecting) {
|
||||
if (shellReady && showKeyboardAfterConnect) {
|
||||
showKeyboard()
|
||||
showKeyboardWhenReady = false
|
||||
terminalView?.requestFocusFromTouch()
|
||||
terminalView?.requestFocus()
|
||||
terminalView?.post {
|
||||
terminalView?.requestFocusFromTouch()
|
||||
terminalView?.requestFocus()
|
||||
terminalView?.let(LocalInputService::showSoftKeyboard)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -357,9 +391,7 @@ private fun TerminalPage(
|
||||
withContext(Dispatchers.Main) {
|
||||
shellConnecting = false
|
||||
shellReady = false
|
||||
showKeyboardWhenReady = false
|
||||
appendOutputLine("错误: ${error.message ?: error.javaClass.simpleName}")
|
||||
snackbar.show("终端会话创建失败")
|
||||
snackbar.show("终端会话创建失败: ${error.message ?: error.javaClass.simpleName}")
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
@@ -368,9 +400,14 @@ private fun TerminalPage(
|
||||
shellStream = stream
|
||||
shellReady = true
|
||||
shellConnecting = false
|
||||
if (showKeyboardWhenReady) {
|
||||
showKeyboard()
|
||||
showKeyboardWhenReady = false
|
||||
if (showKeyboardAfterConnect) {
|
||||
terminalView?.requestFocusFromTouch()
|
||||
terminalView?.requestFocus()
|
||||
terminalView?.post {
|
||||
terminalView?.requestFocusFromTouch()
|
||||
terminalView?.requestFocus()
|
||||
terminalView?.let(LocalInputService::showSoftKeyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,14 +418,13 @@ private fun TerminalPage(
|
||||
if (count <= 0) {
|
||||
break
|
||||
}
|
||||
val chunk = String(buffer, 0, count, StandardCharsets.UTF_8)
|
||||
withContext(Dispatchers.Main) {
|
||||
appendRawOutput(chunk)
|
||||
terminalSession.append(buffer, count)
|
||||
}
|
||||
}
|
||||
} catch (error: Throwable) {
|
||||
withContext(Dispatchers.Main) {
|
||||
appendOutputLine("错误: ${error.message ?: error.javaClass.simpleName}")
|
||||
snackbar.show("终端输出失败: ${error.message ?: error.javaClass.simpleName}")
|
||||
}
|
||||
} finally {
|
||||
runCatching { stream.close() }
|
||||
@@ -398,30 +434,223 @@ private fun TerminalPage(
|
||||
}
|
||||
shellReady = false
|
||||
shellConnecting = false
|
||||
showKeyboardWhenReady = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeLatchedModifiers(): Int {
|
||||
var modifiers = 0
|
||||
if (ctrlLatched) modifiers = modifiers or KeyHandler.KEYMOD_CTRL
|
||||
if (altLatched) modifiers = modifiers or KeyHandler.KEYMOD_ALT
|
||||
ctrlLatched = false
|
||||
altLatched = false
|
||||
pendingLatchedConsume = false
|
||||
return modifiers
|
||||
}
|
||||
|
||||
fun consumeLatchedVisualState() {
|
||||
ctrlLatched = false
|
||||
altLatched = false
|
||||
pendingLatchedConsume = false
|
||||
}
|
||||
|
||||
fun applyCtrlModifier(ch: Char): Char {
|
||||
return when (ch) {
|
||||
in 'a'..'z' -> (ch.code - 'a'.code + 1).toChar()
|
||||
in 'A'..'Z' -> (ch.code - 'A'.code + 1).toChar()
|
||||
' ' -> 0.toChar()
|
||||
'[' -> 27.toChar()
|
||||
'\\' -> 28.toChar()
|
||||
']' -> 29.toChar()
|
||||
'^' -> 30.toChar()
|
||||
'_', '/' -> 31.toChar()
|
||||
else -> ch
|
||||
}
|
||||
}
|
||||
|
||||
fun writeLiteralKey(text: String) {
|
||||
var payload = text
|
||||
val modifiers = consumeLatchedModifiers()
|
||||
if ((modifiers and KeyHandler.KEYMOD_CTRL) != 0) {
|
||||
payload = payload.map(::applyCtrlModifier).joinToString(separator = "")
|
||||
}
|
||||
if ((modifiers and KeyHandler.KEYMOD_ALT) != 0) {
|
||||
payload = "\u001B$payload"
|
||||
}
|
||||
val bytes = payload.toByteArray(StandardCharsets.UTF_8)
|
||||
writeBytesToShell(bytes, 0, bytes.size)
|
||||
}
|
||||
|
||||
fun writeSpecialKey(keyCode: Int) {
|
||||
val modifiers = consumeLatchedModifiers()
|
||||
val sequence = KeyHandler.getCode(
|
||||
keyCode,
|
||||
modifiers,
|
||||
terminalSession.emulator.isCursorKeysApplicationMode(),
|
||||
terminalSession.emulator.isKeypadApplicationMode(),
|
||||
) ?: return
|
||||
val bytes = sequence.toByteArray(StandardCharsets.UTF_8)
|
||||
writeBytesToShell(bytes, 0, bytes.size)
|
||||
}
|
||||
|
||||
val terminalViewClient = remember {
|
||||
object : TerminalViewClient {
|
||||
override fun onScale(scale: Float): Float {
|
||||
when {
|
||||
scale >= FONT_SCALE_STEP_THRESHOLD -> {
|
||||
updateTerminalFontSize(terminalFontSizeSp + 1f)
|
||||
showFontSizeSnackbar()
|
||||
return 1f
|
||||
}
|
||||
|
||||
scale <= 1f / FONT_SCALE_STEP_THRESHOLD -> {
|
||||
updateTerminalFontSize(terminalFontSizeSp - 1f)
|
||||
showFontSizeSnackbar()
|
||||
return 1f
|
||||
}
|
||||
}
|
||||
return scale
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent) {
|
||||
openShellSession(showKeyboardAfterConnect = true)
|
||||
}
|
||||
|
||||
override fun shouldBackButtonBeMappedToEscape(): Boolean = false
|
||||
|
||||
override fun shouldEnforceCharBasedInput(): Boolean = false
|
||||
|
||||
override fun shouldUseCtrlSpaceWorkaround(): Boolean = false
|
||||
|
||||
override fun isTerminalViewSelected(): Boolean = true
|
||||
|
||||
override fun copyModeChanged(copyMode: Boolean) = Unit
|
||||
|
||||
override fun onKeyDown(
|
||||
keyCode: Int,
|
||||
e: KeyEvent,
|
||||
session: TerminalSession,
|
||||
): Boolean {
|
||||
if (e.action == KeyEvent.ACTION_DOWN && (ctrlLatched || altLatched)) {
|
||||
pendingLatchedConsume = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onKeyUp(keyCode: Int, e: KeyEvent): Boolean {
|
||||
if (e.action == KeyEvent.ACTION_UP && pendingLatchedConsume) {
|
||||
consumeLatchedVisualState()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLongPress(event: MotionEvent): Boolean = false
|
||||
|
||||
override fun readControlKey(): Boolean = ctrlLatched
|
||||
|
||||
override fun readAltKey(): Boolean = altLatched
|
||||
|
||||
override fun readShiftKey(): Boolean = false
|
||||
|
||||
override fun readFnKey(): Boolean = false
|
||||
|
||||
override fun onCodePoint(
|
||||
codePoint: Int,
|
||||
ctrlDown: Boolean,
|
||||
session: TerminalSession,
|
||||
): Boolean {
|
||||
if (ctrlLatched || altLatched || pendingLatchedConsume) {
|
||||
consumeLatchedVisualState()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onEmulatorSet() {
|
||||
terminalView?.setTerminalCursorBlinkerRate(500)
|
||||
terminalView?.setTerminalCursorBlinkerState(
|
||||
start = true,
|
||||
startOnlyIfCursorEnabled = true
|
||||
)
|
||||
syncOutput()
|
||||
}
|
||||
|
||||
override fun logError(tag: String?, message: String?) {
|
||||
Log.e(tag ?: LOG_TAG, message.orEmpty())
|
||||
}
|
||||
|
||||
override fun logWarn(tag: String?, message: String?) {
|
||||
Log.w(tag ?: LOG_TAG, message.orEmpty())
|
||||
}
|
||||
|
||||
override fun logInfo(tag: String?, message: String?) {
|
||||
Log.i(tag ?: LOG_TAG, message.orEmpty())
|
||||
}
|
||||
|
||||
override fun logDebug(tag: String?, message: String?) {
|
||||
Log.d(tag ?: LOG_TAG, message.orEmpty())
|
||||
}
|
||||
|
||||
override fun logVerbose(tag: String?, message: String?) {
|
||||
Log.v(tag ?: LOG_TAG, message.orEmpty())
|
||||
}
|
||||
|
||||
override fun logStackTraceWithMessage(
|
||||
tag: String?,
|
||||
message: String?,
|
||||
e: Exception?,
|
||||
) {
|
||||
Log.e(tag ?: LOG_TAG, message, e)
|
||||
}
|
||||
|
||||
override fun logStackTrace(tag: String?, e: Exception?) {
|
||||
Log.e(tag ?: LOG_TAG, e?.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(output) {
|
||||
if (output != outputBuffer) {
|
||||
outputBuffer = output
|
||||
if (output.isEmpty() && extractTranscript(terminalSession).isNotEmpty()) {
|
||||
terminalSession.reset()
|
||||
applyTerminalThemeColors()
|
||||
terminalView?.mEmulator = terminalSession.emulator
|
||||
terminalView?.onScreenUpdated()
|
||||
syncOutput()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(outputBuffer) {
|
||||
outputScrollState.scrollTo(outputScrollState.maxValue)
|
||||
LaunchedEffect(isActive) {
|
||||
if (!isActive) {
|
||||
onTerminalGestureLockChanged(false)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
customTypeface = loadTerminalTypeface(context)
|
||||
if (shellStream == null && !shellConnecting) {
|
||||
val connected = runCatching { NativeAdbService.isConnected() }.getOrDefault(false)
|
||||
if (connected) {
|
||||
openShellSession(showKeyboardAfterConnect = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(imeBottomDp.value) {
|
||||
if (imeBottomDp > 0.dp) {
|
||||
outputScrollState.scrollTo(outputScrollState.maxValue)
|
||||
}
|
||||
LaunchedEffect(colorScheme.surface, colorScheme.onSurface) {
|
||||
applyTerminalThemeColors()
|
||||
terminalView?.onScreenUpdated()
|
||||
syncOutput()
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
val latestFontSize = terminalFontSizeSp
|
||||
onTerminalGestureLockChanged(false)
|
||||
taskScope.launch {
|
||||
val latest = appSettings.bundleState.value
|
||||
if (latest.terminalFontSizeSp != latestFontSize) {
|
||||
appSettings.updateBundle {
|
||||
it.copy(terminalFontSizeSp = latestFontSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
runCatching { shellStream?.close() }
|
||||
taskScope.cancel()
|
||||
}
|
||||
@@ -435,141 +664,142 @@ private fun TerminalPage(
|
||||
start = UiSpacing.PageHorizontal,
|
||||
top = UiSpacing.PageHorizontal,
|
||||
end = UiSpacing.PageVertical,
|
||||
bottom = UiSpacing.PageVertical
|
||||
+ max(bottomInnerPadding.value, imeBottomDp.value).dp,
|
||||
bottom = UiSpacing.PageVertical +
|
||||
max(bottomInnerPadding.value, imeBottomDp.value).dp,
|
||||
),
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(outputScrollState)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
) {
|
||||
Text(
|
||||
text = outputBuffer.ifBlank {
|
||||
"""
|
||||
可用缩放手势调整字体大小
|
||||
轻触以连接终端
|
||||
""".trimIndent()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = terminalFontSizeSp.sp,
|
||||
color =
|
||||
if (outputBuffer.isBlank()) colorScheme.onSurfaceVariantSummary
|
||||
else colorScheme.onSurface,
|
||||
AndroidView(
|
||||
factory = { viewContext ->
|
||||
TerminalView(viewContext, null).apply {
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
requestFocus()
|
||||
setTerminalViewClient(terminalViewClient)
|
||||
setIsTerminalViewKeyLoggingEnabled(false)
|
||||
setTextSize(with(density) { terminalFontSizeSp.sp.roundToPx() })
|
||||
setTypeface(customTypeface ?: Typeface.MONOSPACE)
|
||||
attachSession(terminalSession)
|
||||
applyTerminalThemeColors()
|
||||
setTerminalCursorBlinkerRate(500)
|
||||
setTerminalCursorBlinkerState(
|
||||
start = true,
|
||||
startOnlyIfCursorEnabled = true,
|
||||
)
|
||||
setOnTouchListener { _, event ->
|
||||
handleTerminalTouchInterception(this, event)
|
||||
}
|
||||
terminalView = this
|
||||
}
|
||||
},
|
||||
update = {
|
||||
terminalView = it
|
||||
it.setTextSize(with(density) { terminalFontSizeSp.sp.roundToPx() })
|
||||
it.setTypeface(customTypeface ?: Typeface.MONOSPACE)
|
||||
if (it.currentSession !== terminalSession) {
|
||||
it.attachSession(terminalSession)
|
||||
}
|
||||
it.setOnTouchListener { _, event ->
|
||||
handleTerminalTouchInterception(it, event)
|
||||
}
|
||||
applyTerminalThemeColors()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
)
|
||||
|
||||
AndroidView(
|
||||
factory = {
|
||||
TerminalInputView(it).apply {
|
||||
alpha = 0f
|
||||
setInputEnabled(true)
|
||||
setOnTouchListener { _, event ->
|
||||
scaleGestureDetector.onTouchEvent(event)
|
||||
when {
|
||||
event.actionMasked == MotionEvent.ACTION_DOWN -> {
|
||||
pinchInProgress = false
|
||||
tapPending = true
|
||||
touchDownX = event.x
|
||||
touchDownY = event.y
|
||||
parent?.requestDisallowInterceptTouchEvent(false)
|
||||
true
|
||||
}
|
||||
|
||||
event.pointerCount >= 2 -> {
|
||||
pinchInProgress = true
|
||||
tapPending = false
|
||||
parent?.requestDisallowInterceptTouchEvent(true)
|
||||
true
|
||||
}
|
||||
|
||||
event.actionMasked == MotionEvent.ACTION_MOVE -> {
|
||||
val movedFar =
|
||||
kotlin.math.abs(event.x - touchDownX) > touchSlop ||
|
||||
kotlin.math.abs(event.y - touchDownY) > touchSlop
|
||||
if (movedFar) {
|
||||
tapPending = false
|
||||
}
|
||||
if (pinchInProgress) {
|
||||
parent?.requestDisallowInterceptTouchEvent(true)
|
||||
} else {
|
||||
parent?.requestDisallowInterceptTouchEvent(false)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
event.actionMasked == MotionEvent.ACTION_UP -> {
|
||||
if (!pinchInProgress && tapPending) {
|
||||
performClick()
|
||||
openShellSession(showKeyboardAfterConnect = true)
|
||||
}
|
||||
pinchInProgress = false
|
||||
tapPending = false
|
||||
parent?.requestDisallowInterceptTouchEvent(false)
|
||||
true
|
||||
}
|
||||
|
||||
event.actionMasked == MotionEvent.ACTION_UP ||
|
||||
event.actionMasked == MotionEvent.ACTION_CANCEL -> {
|
||||
pinchInProgress = false
|
||||
tapPending = false
|
||||
parent?.requestDisallowInterceptTouchEvent(false)
|
||||
true
|
||||
}
|
||||
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
inputCallbacks = object : TerminalInputView.InputCallbacks {
|
||||
override fun handleCommitText(text: CharSequence): Boolean {
|
||||
if (text.isEmpty()) {
|
||||
return true
|
||||
}
|
||||
writeToShell(text.toString())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun handleDeleteSurroundingText(
|
||||
beforeLength: Int,
|
||||
afterLength: Int,
|
||||
): Boolean {
|
||||
val deleteCount = beforeLength.coerceAtLeast(1)
|
||||
writeToShell("\b".repeat(deleteCount))
|
||||
return true
|
||||
}
|
||||
|
||||
override fun handleKeyEvent(event: AndroidKeyEvent): Boolean {
|
||||
val shellText = mapKeyEventToShellText(event) ?: return false
|
||||
if (shellText.isNotEmpty()) {
|
||||
writeToShell(shellText)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
terminalInputView = this
|
||||
}
|
||||
},
|
||||
update = {
|
||||
terminalInputView = it
|
||||
if (showKeyboardWhenReady && shellReady) {
|
||||
LocalInputService.showSoftKeyboard(it)
|
||||
showKeyboardWhenReady = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
TerminalExtraKeyButton(
|
||||
label = "ESC",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeLiteralKey("\u001B") }
|
||||
TerminalExtraKeyButton(
|
||||
label = "/",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeLiteralKey("/") }
|
||||
TerminalExtraKeyButton(
|
||||
label = "-",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeLiteralKey("-") }
|
||||
TerminalExtraKeyButton(
|
||||
label = "HOME",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeSpecialKey(KeyEvent.KEYCODE_MOVE_HOME) }
|
||||
TerminalExtraKeyButton(
|
||||
label = "↑",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeSpecialKey(KeyEvent.KEYCODE_DPAD_UP) }
|
||||
TerminalExtraKeyButton(
|
||||
label = "END",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeSpecialKey(KeyEvent.KEYCODE_MOVE_END) }
|
||||
TerminalExtraKeyButton(
|
||||
label = "PGUP",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeSpecialKey(KeyEvent.KEYCODE_PAGE_UP) }
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
TerminalExtraKeyButton(
|
||||
label = "TAB",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeSpecialKey(KeyEvent.KEYCODE_TAB) }
|
||||
TerminalExtraKeyButton(
|
||||
label = "CTRL",
|
||||
active = ctrlLatched,
|
||||
modifier = Modifier.weight(1f),
|
||||
) { ctrlLatched = !ctrlLatched }
|
||||
TerminalExtraKeyButton(
|
||||
label = "ALT",
|
||||
active = altLatched,
|
||||
modifier = Modifier.weight(1f),
|
||||
) { altLatched = !altLatched }
|
||||
TerminalExtraKeyButton(
|
||||
label = "←",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeSpecialKey(KeyEvent.KEYCODE_DPAD_LEFT) }
|
||||
TerminalExtraKeyButton(
|
||||
label = "↓",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeSpecialKey(KeyEvent.KEYCODE_DPAD_DOWN) }
|
||||
TerminalExtraKeyButton(
|
||||
label = "→",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeSpecialKey(KeyEvent.KEYCODE_DPAD_RIGHT) }
|
||||
TerminalExtraKeyButton(
|
||||
label = "PGDN",
|
||||
modifier = Modifier.weight(1f),
|
||||
) { writeSpecialKey(KeyEvent.KEYCODE_PAGE_DOWN) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TerminalExtraKeyButton(
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
active: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val content =
|
||||
if (active) colorScheme.primary
|
||||
else colorScheme.onSurface
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(32.dp)
|
||||
.clickable(onClick = onClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = content,
|
||||
fontSize = 15.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ data class ClientOptions(
|
||||
// var window: Boolean,
|
||||
|
||||
// --no-mouse-hover
|
||||
// var mouseHover: Boolean = true,
|
||||
var mouseHover: Boolean = true,
|
||||
|
||||
// --audio-dup
|
||||
var audioDup: Boolean = false, // to server
|
||||
|
||||
@@ -166,7 +166,11 @@ class Scrcpy(
|
||||
}
|
||||
|
||||
// Create session info
|
||||
_currentSessionState.value = info.copy(legacyPaste = options.legacyPaste)
|
||||
_currentSessionState.value = info.copy(
|
||||
legacyPaste = options.legacyPaste,
|
||||
mouseHover = options.mouseHover,
|
||||
killAdbOnClose = options.killAdbOnClose,
|
||||
)
|
||||
isRunning = true
|
||||
startClipboardSync()
|
||||
|
||||
@@ -297,6 +301,9 @@ class Scrcpy(
|
||||
buttons = buttons,
|
||||
)
|
||||
|
||||
suspend fun pressBackOrTurnScreenOn(action: Int = KeyEvent.ACTION_DOWN) =
|
||||
session.pressBackOrTurnScreenOn(action)
|
||||
|
||||
fun updateCurrentSessionSize(width: Int, height: Int) {
|
||||
val current = _currentSessionState.value ?: return
|
||||
if (current.width == width && current.height == height) return
|
||||
@@ -1362,6 +1369,8 @@ class Scrcpy(
|
||||
val audioCodecId: Int = 0,
|
||||
val controlEnabled: Boolean,
|
||||
val legacyPaste: Boolean = false,
|
||||
val mouseHover: Boolean = true,
|
||||
val killAdbOnClose: Boolean = false,
|
||||
val host: String = "",
|
||||
val port: Int = Defaults.ADB_PORT,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.scrcpy
|
||||
|
||||
import android.util.Log
|
||||
import android.view.InputDevice
|
||||
import android.view.MotionEvent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
@@ -24,13 +25,24 @@ class TouchEventHandler(
|
||||
private val activePointerDevicePositions: LinkedHashMap<Int, Pair<Int, Int>>,
|
||||
private val pointerLabels: LinkedHashMap<Int, Int>,
|
||||
private var nextPointerLabel: Int,
|
||||
private val onInjectTouch: suspend (action: Int, pointerId: Long, x: Int, y: Int, pressure: Float, buttons: Int) -> Unit,
|
||||
private val mouseHoverEnabled: Boolean,
|
||||
private val onInjectTouch: suspend (
|
||||
action: Int,
|
||||
pointerId: Long,
|
||||
x: Int,
|
||||
y: Int,
|
||||
pressure: Float,
|
||||
actionButton: Int,
|
||||
buttons: Int,
|
||||
) -> Unit,
|
||||
private val onBackOrScreenOn: suspend (action: Int) -> Unit,
|
||||
private val onActiveTouchCountChanged: (Int) -> Unit,
|
||||
private val onActiveTouchDebugChanged: (String) -> Unit,
|
||||
private val onNextPointerLabelChanged: (Int) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
private const val FULLSCREEN_TOUCH_LOG_TAG = "FullscreenTouch"
|
||||
private const val POINTER_ID_MOUSE = -1L
|
||||
}
|
||||
|
||||
private object UiMotionActions {
|
||||
@@ -54,6 +66,10 @@ class TouchEventHandler(
|
||||
|
||||
val bounds = calculateContentBounds()
|
||||
|
||||
if (isMouseLikeEvent(event)) {
|
||||
return handleMouseEvent(event, bounds)
|
||||
}
|
||||
|
||||
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
return handleCancelAction(bounds)
|
||||
}
|
||||
@@ -71,6 +87,14 @@ class TouchEventHandler(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isMouseLikeEvent(event: MotionEvent): Boolean {
|
||||
return event.isFromSource(InputDevice.SOURCE_MOUSE) ||
|
||||
event.actionMasked == MotionEvent.ACTION_HOVER_ENTER ||
|
||||
event.actionMasked == MotionEvent.ACTION_HOVER_MOVE ||
|
||||
event.actionMasked == MotionEvent.ACTION_HOVER_EXIT ||
|
||||
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE
|
||||
}
|
||||
|
||||
private data class ContentBounds(
|
||||
val width: Float,
|
||||
val height: Float,
|
||||
@@ -149,13 +173,97 @@ class TouchEventHandler(
|
||||
onActiveTouchDebugChanged(debug)
|
||||
}
|
||||
|
||||
private fun handleMouseEvent(
|
||||
event: MotionEvent,
|
||||
bounds: ContentBounds,
|
||||
): Boolean {
|
||||
val rawX = event.getX(0)
|
||||
val rawY = event.getY(0)
|
||||
val (x, y) = mapToDevice(rawX, rawY, bounds)
|
||||
val pressure = event.getPressure(0).coerceIn(0f, 1f)
|
||||
val buttons = event.buttonState
|
||||
val actionButton = event.actionButton
|
||||
val isHoverMotion = when (event.actionMasked) {
|
||||
MotionEvent.ACTION_HOVER_ENTER,
|
||||
MotionEvent.ACTION_HOVER_MOVE,
|
||||
MotionEvent.ACTION_HOVER_EXIT -> true
|
||||
|
||||
MotionEvent.ACTION_MOVE -> buttons == 0
|
||||
else -> false
|
||||
}
|
||||
|
||||
if (!mouseHoverEnabled && isHoverMotion) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (actionButton == MotionEvent.BUTTON_SECONDARY ||
|
||||
buttons and MotionEvent.BUTTON_SECONDARY != 0
|
||||
) {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_BUTTON_PRESS -> {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
onBackOrScreenOn(0)
|
||||
}.onFailure { e ->
|
||||
Log.w(FULLSCREEN_TOUCH_LOG_TAG, "mouse secondary down failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_BUTTON_RELEASE -> {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
onBackOrScreenOn(1)
|
||||
}.onFailure { e ->
|
||||
Log.w(FULLSCREEN_TOUCH_LOG_TAG, "mouse secondary up failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val injectAction = when (event.actionMasked) {
|
||||
MotionEvent.ACTION_HOVER_ENTER -> MotionEvent.ACTION_HOVER_ENTER
|
||||
MotionEvent.ACTION_HOVER_MOVE -> MotionEvent.ACTION_HOVER_MOVE
|
||||
MotionEvent.ACTION_HOVER_EXIT -> MotionEvent.ACTION_HOVER_EXIT
|
||||
MotionEvent.ACTION_DOWN -> MotionEvent.ACTION_DOWN
|
||||
MotionEvent.ACTION_UP -> MotionEvent.ACTION_UP
|
||||
MotionEvent.ACTION_MOVE -> MotionEvent.ACTION_MOVE
|
||||
|
||||
MotionEvent.ACTION_BUTTON_PRESS,
|
||||
MotionEvent.ACTION_BUTTON_RELEASE -> return true
|
||||
|
||||
else -> return true
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
onInjectTouch(
|
||||
injectAction,
|
||||
POINTER_ID_MOUSE,
|
||||
x,
|
||||
y,
|
||||
pressure,
|
||||
actionButton,
|
||||
buttons,
|
||||
)
|
||||
}.onFailure { e ->
|
||||
Log.w(FULLSCREEN_TOUCH_LOG_TAG, "handleMouseEvent failed", e)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun releasePointer(pointerId: Int, bounds: ContentBounds) {
|
||||
if (!activePointerIds.contains(pointerId)) return
|
||||
val pos = activePointerPositions[pointerId] ?: Offset.Zero
|
||||
val (x, y) = mapToDevice(pos.x, pos.y, bounds)
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
onInjectTouch(UiMotionActions.UP, pointerId.toLong(), x, y, 0f, 0)
|
||||
onInjectTouch(UiMotionActions.UP, pointerId.toLong(), x, y, 0f, 0, 0)
|
||||
}.onFailure { e ->
|
||||
Log.w(FULLSCREEN_TOUCH_LOG_TAG, "releasePointer failed for pointerId=$pointerId", e)
|
||||
}
|
||||
@@ -222,7 +330,7 @@ class TouchEventHandler(
|
||||
justPressedPointerIds += pointerId
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
onInjectTouch(UiMotionActions.DOWN, pointerId.toLong(), x, y, pressure, 0)
|
||||
onInjectTouch(UiMotionActions.DOWN, pointerId.toLong(), x, y, pressure, 0, 0)
|
||||
}.onFailure { e ->
|
||||
Log.w(
|
||||
FULLSCREEN_TOUCH_LOG_TAG,
|
||||
@@ -252,7 +360,7 @@ class TouchEventHandler(
|
||||
activePointerDevicePositions[pointerId] = x to y
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
onInjectTouch(UiMotionActions.MOVE, pointerId.toLong(), x, y, pressure, 0)
|
||||
onInjectTouch(UiMotionActions.MOVE, pointerId.toLong(), x, y, pressure, 0, 0)
|
||||
}.onFailure { e ->
|
||||
Log.w(
|
||||
FULLSCREEN_TOUCH_LOG_TAG,
|
||||
|
||||
@@ -29,9 +29,9 @@ class AdbClientData(context: Context) : Settings(context, "AdbClient") {
|
||||
) : Parcelable {
|
||||
}
|
||||
|
||||
private val bundleFields = arrayOf(
|
||||
bundleField(RSA_PRIVATE_KEY) { bundle: Bundle -> bundle.rsaPrivateKey },
|
||||
bundleField(RSA_PUBLIC_KEY_X509) { bundle: Bundle -> bundle.rsaPublicKeyX509 },
|
||||
private val bundleFields = arrayOf<BundleField<Bundle>>(
|
||||
bundleField(RSA_PRIVATE_KEY) { it.rsaPrivateKey },
|
||||
bundleField(RSA_PUBLIC_KEY_X509) { it.rsaPublicKeyX509 },
|
||||
)
|
||||
|
||||
val bundleState: StateFlow<Bundle> = createBundleState(::bundleFromPreferences)
|
||||
|
||||
@@ -13,14 +13,68 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
||||
enum class FullscreenVirtualButtonDock(
|
||||
val rawValue: String,
|
||||
val isFixed: Boolean,
|
||||
val directionLabel: String,
|
||||
) {
|
||||
FOLLOW_TOP("FOLLOW_TOP", false, "上方"),
|
||||
FOLLOW_BOTTOM("FOLLOW_BOTTOM", false, "下方"),
|
||||
FOLLOW_LEFT("FOLLOW_LEFT", false, "左侧"),
|
||||
FOLLOW_RIGHT("FOLLOW_RIGHT", false, "右侧"),
|
||||
FIXED_TOP("FIXED_TOP", true, "上方"),
|
||||
FIXED_BOTTOM("FIXED_BOTTOM", true, "下方"),
|
||||
FIXED_LEFT("FIXED_LEFT", true, "左侧"),
|
||||
FIXED_RIGHT("FIXED_RIGHT", true, "右侧");
|
||||
|
||||
fun toStoredValue(): String = rawValue
|
||||
|
||||
val summary: String
|
||||
get() = "${if (!isFixed) "跟随" else "固定"}显示在屏幕$directionLabel"
|
||||
|
||||
val modeIndex: Int
|
||||
get() = if (!isFixed) 0 else 1
|
||||
|
||||
val directionIndex: Int
|
||||
get() = when (this) {
|
||||
FOLLOW_TOP, FIXED_TOP -> 0
|
||||
FOLLOW_BOTTOM, FIXED_BOTTOM -> 1
|
||||
FOLLOW_LEFT, FIXED_LEFT -> 2
|
||||
FOLLOW_RIGHT, FIXED_RIGHT -> 3
|
||||
}
|
||||
|
||||
companion object {
|
||||
val modeItems = listOf("跟随", "固定")
|
||||
val directionItems = listOf("上方", "下方", "左侧", "右侧")
|
||||
|
||||
fun fromBundle(bundle: Bundle) =
|
||||
entries.firstOrNull { it.rawValue == bundle.fullscreenVirtualButtonDock }
|
||||
?: FOLLOW_BOTTOM
|
||||
|
||||
fun fromStoredValue(value: String) =
|
||||
entries.firstOrNull { it.rawValue == value }
|
||||
?: FOLLOW_BOTTOM
|
||||
|
||||
fun fromModeAndDirection(modeIndex: Int, directionIndex: Int) =
|
||||
when (directionIndex) {
|
||||
0 -> if (modeIndex == 0) FOLLOW_TOP else FIXED_TOP
|
||||
1 -> if (modeIndex == 0) FOLLOW_BOTTOM else FIXED_BOTTOM
|
||||
2 -> if (modeIndex == 0) FOLLOW_LEFT else FIXED_LEFT
|
||||
3 -> if (modeIndex == 0) FOLLOW_RIGHT else FIXED_RIGHT
|
||||
else -> if (modeIndex == 0) FOLLOW_BOTTOM else FIXED_BOTTOM
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Theme
|
||||
val THEME_BASE_INDEX = Pair(
|
||||
intPreferencesKey("theme_base_index"),
|
||||
0
|
||||
0,
|
||||
)
|
||||
val MONET = Pair(
|
||||
booleanPreferencesKey("monet"),
|
||||
false
|
||||
false,
|
||||
)
|
||||
val BLUR = Pair(
|
||||
booleanPreferencesKey("blur"),
|
||||
@@ -38,29 +92,15 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
||||
booleanPreferencesKey("smooth_corner"),
|
||||
false,
|
||||
)
|
||||
|
||||
// Scrcpy
|
||||
val LOW_LATENCY = Pair(
|
||||
booleanPreferencesKey("low_latency"),
|
||||
false,
|
||||
)
|
||||
val FULLSCREEN_DEBUG_INFO = Pair(
|
||||
booleanPreferencesKey("fullscreen_debug_info"),
|
||||
false
|
||||
)
|
||||
val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = Pair(
|
||||
booleanPreferencesKey("show_fullscreen_virtual_buttons"),
|
||||
false
|
||||
)
|
||||
val SHOW_FULLSCREEN_FLOATING_BUTTON = Pair(
|
||||
booleanPreferencesKey("show_fullscreen_floating_button"),
|
||||
true
|
||||
)
|
||||
val FULLSCREEN_FLOATING_BUTTON_X_FRACTION = Pair(
|
||||
floatPreferencesKey("fullscreen_floating_button_x_fraction"),
|
||||
0.84f
|
||||
)
|
||||
val FULLSCREEN_FLOATING_BUTTON_Y_FRACTION = Pair(
|
||||
floatPreferencesKey("fullscreen_floating_button_y_fraction"),
|
||||
0.72f
|
||||
false,
|
||||
)
|
||||
val HIDE_SIMPLE_CONFIG_ITEMS = Pair(
|
||||
booleanPreferencesKey("hide_simple_config_items"),
|
||||
@@ -68,11 +108,56 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
||||
)
|
||||
val DEVICE_PREVIEW_CARD_HEIGHT_DP = Pair(
|
||||
intPreferencesKey("device_preview_card_height_dp"),
|
||||
1080 / 3
|
||||
1080 / 3,
|
||||
)
|
||||
val FULLSCREEN_CONTROL_IGNORE_SYSTEM_ROTATION_LOCK = Pair(
|
||||
booleanPreferencesKey("fullscreen_control_ignore_system_rotation_lock"),
|
||||
true,
|
||||
)
|
||||
val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = Pair(
|
||||
booleanPreferencesKey("show_fullscreen_virtual_buttons"),
|
||||
true,
|
||||
)
|
||||
val FULLSCREEN_VIRTUAL_BUTTON_HEIGHT_DP = Pair(
|
||||
intPreferencesKey("fullscreen_virtual_button_height_dp"),
|
||||
16,
|
||||
)
|
||||
val FULLSCREEN_VIRTUAL_BUTTON_DOCK = Pair(
|
||||
stringPreferencesKey("fullscreen_virtual_button_dock"),
|
||||
FullscreenVirtualButtonDock.FIXED_BOTTOM.toStoredValue(),
|
||||
)
|
||||
val SHOW_FULLSCREEN_FLOATING_BUTTON = Pair(
|
||||
booleanPreferencesKey("show_fullscreen_floating_button"),
|
||||
true,
|
||||
)
|
||||
val FULLSCREEN_FLOATING_BUTTON_SIZE_DP = Pair(
|
||||
intPreferencesKey("fullscreen_floating_button_size_dp"),
|
||||
48,
|
||||
)
|
||||
val FULLSCREEN_FLOATING_BUTTON_BACKGROUND_ALPHA_PERCENT = Pair(
|
||||
intPreferencesKey("fullscreen_floating_button_background_alpha_percent"),
|
||||
25,
|
||||
)
|
||||
val FULLSCREEN_FLOATING_BUTTON_RING_ALPHA_PERCENT = Pair(
|
||||
intPreferencesKey("fullscreen_floating_button_ring_alpha_percent"),
|
||||
100,
|
||||
)
|
||||
val REALTIME_CLIPBOARD_SYNC_TO_DEVICE = Pair(
|
||||
booleanPreferencesKey("realtime_clipboard_sync_to_device"),
|
||||
true,
|
||||
)
|
||||
|
||||
val FULLSCREEN_FLOATING_BUTTON_X_FRACTION = Pair(
|
||||
floatPreferencesKey("fullscreen_floating_button_x_fraction"),
|
||||
0.84f,
|
||||
)
|
||||
val FULLSCREEN_FLOATING_BUTTON_Y_FRACTION = Pair(
|
||||
floatPreferencesKey("fullscreen_floating_button_y_fraction"),
|
||||
0.72f,
|
||||
)
|
||||
val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = Pair(
|
||||
booleanPreferencesKey("preview_virtual_button_show_text"),
|
||||
true
|
||||
true,
|
||||
)
|
||||
val VIRTUAL_BUTTONS_LAYOUT = Pair(
|
||||
stringPreferencesKey("virtual_buttons_layout"),
|
||||
@@ -90,197 +175,233 @@ class AppSettings(context: Context) : Settings(context, "AppSettings") {
|
||||
",volume_up:0,volume_down:0,volume_mute:0" +
|
||||
",power:0,screenshot:0" +
|
||||
|
||||
""
|
||||
"",
|
||||
)
|
||||
|
||||
// Scrcpy Server
|
||||
val CUSTOM_SERVER_URI = Pair(
|
||||
stringPreferencesKey("custom_server_uri"),
|
||||
""
|
||||
"",
|
||||
)
|
||||
val CUSTOM_SERVER_VERSION = Pair(
|
||||
stringPreferencesKey("custom_server_version"),
|
||||
""
|
||||
"",
|
||||
)
|
||||
val SERVER_REMOTE_PATH = Pair(
|
||||
stringPreferencesKey("server_remote_path"),
|
||||
Scrcpy.DEFAULT_REMOTE_PATH,
|
||||
)
|
||||
|
||||
// ADB
|
||||
val ADB_KEY_NAME = Pair(
|
||||
stringPreferencesKey("adb_key_name"),
|
||||
"scrcpy"
|
||||
"scrcpy",
|
||||
)
|
||||
val ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN = Pair(
|
||||
booleanPreferencesKey("adb_pairing_auto_discover_on_dialog_open"),
|
||||
true
|
||||
true,
|
||||
)
|
||||
val ADB_AUTO_RECONNECT_PAIRED_DEVICE = Pair(
|
||||
booleanPreferencesKey("adb_auto_reconnect_paired_device"),
|
||||
true
|
||||
true,
|
||||
)
|
||||
val ADB_MDNS_LAN_DISCOVERY = Pair(
|
||||
// 没必要加开关, 保持启用
|
||||
booleanPreferencesKey("adb_mdns_lan_discovery"),
|
||||
true
|
||||
true,
|
||||
)
|
||||
val ADB_AUTO_LOAD_APP_LIST_ON_CONNECT = Pair(
|
||||
booleanPreferencesKey("adb_auto_load_app_list_on_connect"),
|
||||
false
|
||||
false,
|
||||
)
|
||||
|
||||
// Terminal
|
||||
val TERMINAL_FONT_SIZE_SP = Pair(
|
||||
floatPreferencesKey("terminal_font_size_sp"),
|
||||
12f,
|
||||
)
|
||||
val TERMINAL_FONT_DISPLAY_NAME = Pair(
|
||||
stringPreferencesKey("terminal_font_display_name"),
|
||||
"",
|
||||
)
|
||||
|
||||
val PASSWORD_REQUIRE_AUTH = Pair(
|
||||
booleanPreferencesKey("password_require_auth"),
|
||||
true
|
||||
)
|
||||
val REALTIME_CLIPBOARD_SYNC_TO_DEVICE = Pair(
|
||||
booleanPreferencesKey("realtime_clipboard_sync_to_device"),
|
||||
true
|
||||
true,
|
||||
)
|
||||
|
||||
val FILE_MANAGER_SORT_BY = Pair(
|
||||
stringPreferencesKey("file_manager_sort_by"),
|
||||
"NAME"
|
||||
"NAME",
|
||||
)
|
||||
val FILE_MANAGER_SORT_DESCENDING = Pair(
|
||||
booleanPreferencesKey("file_manager_sort_descending"),
|
||||
false
|
||||
false,
|
||||
)
|
||||
val LAST_UPDATE_CHECK_AT = Pair(
|
||||
longPreferencesKey("last_update_check_at"),
|
||||
0L
|
||||
0L,
|
||||
)
|
||||
}
|
||||
|
||||
// Theme Settings
|
||||
val themeBaseIndex by setting(THEME_BASE_INDEX)
|
||||
val monet by setting(MONET)
|
||||
val blur by setting(BLUR)
|
||||
val floatingBottomBar by setting(FLOATING_BOTTOM_BAR)
|
||||
val floatingBottomBarBlur by setting(FLOATING_BOTTOM_BAR_BLUR)
|
||||
val smoothCorner by setting(SMOOTH_CORNER)
|
||||
|
||||
// Scrcpy Settings
|
||||
val lowLatency by setting(LOW_LATENCY)
|
||||
val fullscreenDebugInfo by setting(FULLSCREEN_DEBUG_INFO)
|
||||
val showFullscreenVirtualButtons by setting(SHOW_FULLSCREEN_VIRTUAL_BUTTONS)
|
||||
val showFullscreenFloatingButton by setting(SHOW_FULLSCREEN_FLOATING_BUTTON)
|
||||
val fullscreenFloatingButtonXFraction by setting(FULLSCREEN_FLOATING_BUTTON_X_FRACTION)
|
||||
val fullscreenFloatingButtonYFraction by setting(FULLSCREEN_FLOATING_BUTTON_Y_FRACTION)
|
||||
val hideSimpleConfigItems by setting(HIDE_SIMPLE_CONFIG_ITEMS)
|
||||
val devicePreviewCardHeightDp by setting(DEVICE_PREVIEW_CARD_HEIGHT_DP)
|
||||
val previewVirtualButtonShowText by setting(PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT)
|
||||
val virtualButtonsLayout by setting(VIRTUAL_BUTTONS_LAYOUT)
|
||||
|
||||
// Scrcpy Server Settings
|
||||
val customServerUri by setting(CUSTOM_SERVER_URI)
|
||||
val customServerVersion by setting(CUSTOM_SERVER_VERSION)
|
||||
val serverRemotePath by setting(SERVER_REMOTE_PATH)
|
||||
|
||||
// ADB Settings
|
||||
val adbKeyName by setting(ADB_KEY_NAME)
|
||||
val adbPairingAutoDiscoverOnDialogOpen by setting(ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN)
|
||||
val adbAutoReconnectPairedDevice by setting(ADB_AUTO_RECONNECT_PAIRED_DEVICE)
|
||||
val adbMdnsLanDiscovery by setting(ADB_MDNS_LAN_DISCOVERY)
|
||||
val adbAutoLoadAppListOnConnect by setting(ADB_AUTO_LOAD_APP_LIST_ON_CONNECT)
|
||||
val passwordRequireAuth by setting(PASSWORD_REQUIRE_AUTH)
|
||||
val realtimeClipboardSyncToDevice by setting(REALTIME_CLIPBOARD_SYNC_TO_DEVICE)
|
||||
val fileManagerSortBy by setting(FILE_MANAGER_SORT_BY)
|
||||
val fileManagerSortDescending by setting(FILE_MANAGER_SORT_DESCENDING)
|
||||
val lastUpdateCheckAt by setting(LAST_UPDATE_CHECK_AT)
|
||||
|
||||
@Parcelize
|
||||
data class Bundle(
|
||||
// Theme
|
||||
val themeBaseIndex: Int,
|
||||
val monet: Boolean,
|
||||
val blur: Boolean,
|
||||
val floatingBottomBar: Boolean,
|
||||
val floatingBottomBarBlur: Boolean,
|
||||
val smoothCorner: Boolean,
|
||||
|
||||
// Scrcpy
|
||||
val lowLatency: Boolean,
|
||||
val fullscreenDebugInfo: Boolean,
|
||||
val showFullscreenVirtualButtons: Boolean,
|
||||
val showFullscreenFloatingButton: Boolean,
|
||||
val fullscreenFloatingButtonXFraction: Float,
|
||||
val fullscreenFloatingButtonYFraction: Float,
|
||||
val hideSimpleConfigItems: Boolean,
|
||||
val devicePreviewCardHeightDp: Int,
|
||||
val fullscreenControlIgnoreSystemRotationLock: Boolean,
|
||||
val showFullscreenVirtualButtons: Boolean,
|
||||
val fullscreenVirtualButtonHeightDp: Int,
|
||||
val fullscreenVirtualButtonDock: String,
|
||||
val showFullscreenFloatingButton: Boolean,
|
||||
val fullscreenFloatingButtonSizeDp: Int,
|
||||
val fullscreenFloatingButtonBackgroundAlphaPercent: Int,
|
||||
val fullscreenFloatingButtonRingAlphaPercent: Int,
|
||||
val realtimeClipboardSyncToDevice: Boolean,
|
||||
|
||||
val fullscreenFloatingButtonXFraction: Float,
|
||||
val fullscreenFloatingButtonYFraction: Float,
|
||||
val previewVirtualButtonShowText: Boolean,
|
||||
val virtualButtonsLayout: String,
|
||||
|
||||
// Scrcpy Server
|
||||
val customServerUri: String,
|
||||
val customServerVersion: String,
|
||||
val serverRemotePath: String,
|
||||
|
||||
// ADB
|
||||
val adbKeyName: String,
|
||||
val adbPairingAutoDiscoverOnDialogOpen: Boolean,
|
||||
val adbAutoReconnectPairedDevice: Boolean,
|
||||
val adbMdnsLanDiscovery: Boolean,
|
||||
val adbAutoLoadAppListOnConnect: Boolean,
|
||||
|
||||
// Terminal
|
||||
val terminalFontSizeSp: Float,
|
||||
val terminalFontDisplayName: String,
|
||||
|
||||
val passwordRequireAuth: Boolean,
|
||||
val realtimeClipboardSyncToDevice: Boolean,
|
||||
|
||||
val fileManagerSortBy: String,
|
||||
val fileManagerSortDescending: Boolean,
|
||||
val lastUpdateCheckAt: Long,
|
||||
) : Parcelable {
|
||||
}
|
||||
|
||||
private val bundleFields = arrayOf(
|
||||
bundleField(THEME_BASE_INDEX) { bundle: Bundle -> bundle.themeBaseIndex },
|
||||
bundleField(MONET) { bundle: Bundle -> bundle.monet },
|
||||
bundleField(BLUR) { bundle: Bundle -> bundle.blur },
|
||||
bundleField(FLOATING_BOTTOM_BAR) { bundle: Bundle -> bundle.floatingBottomBar },
|
||||
bundleField(FLOATING_BOTTOM_BAR_BLUR) { bundle: Bundle -> bundle.floatingBottomBarBlur },
|
||||
bundleField(SMOOTH_CORNER) { bundle: Bundle -> bundle.smoothCorner },
|
||||
bundleField(LOW_LATENCY) { bundle: Bundle -> bundle.lowLatency },
|
||||
bundleField(FULLSCREEN_DEBUG_INFO) { bundle: Bundle -> bundle.fullscreenDebugInfo },
|
||||
bundleField(SHOW_FULLSCREEN_VIRTUAL_BUTTONS) { bundle: Bundle -> bundle.showFullscreenVirtualButtons },
|
||||
bundleField(SHOW_FULLSCREEN_FLOATING_BUTTON) { bundle: Bundle -> bundle.showFullscreenFloatingButton },
|
||||
bundleField(FULLSCREEN_FLOATING_BUTTON_X_FRACTION) { bundle: Bundle -> bundle.fullscreenFloatingButtonXFraction },
|
||||
bundleField(FULLSCREEN_FLOATING_BUTTON_Y_FRACTION) { bundle: Bundle -> bundle.fullscreenFloatingButtonYFraction },
|
||||
bundleField(HIDE_SIMPLE_CONFIG_ITEMS) { bundle: Bundle -> bundle.hideSimpleConfigItems },
|
||||
bundleField(DEVICE_PREVIEW_CARD_HEIGHT_DP) { bundle: Bundle -> bundle.devicePreviewCardHeightDp },
|
||||
bundleField(PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT) { bundle: Bundle -> bundle.previewVirtualButtonShowText },
|
||||
bundleField(VIRTUAL_BUTTONS_LAYOUT) { bundle: Bundle -> bundle.virtualButtonsLayout },
|
||||
bundleField(CUSTOM_SERVER_URI) { bundle: Bundle -> bundle.customServerUri },
|
||||
bundleField(CUSTOM_SERVER_VERSION) { bundle: Bundle -> bundle.customServerVersion },
|
||||
bundleField(SERVER_REMOTE_PATH) { bundle: Bundle -> bundle.serverRemotePath },
|
||||
bundleField(ADB_KEY_NAME) { bundle: Bundle -> bundle.adbKeyName },
|
||||
bundleField(ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN) { bundle: Bundle -> bundle.adbPairingAutoDiscoverOnDialogOpen },
|
||||
bundleField(ADB_AUTO_RECONNECT_PAIRED_DEVICE) { bundle: Bundle -> bundle.adbAutoReconnectPairedDevice },
|
||||
bundleField(ADB_MDNS_LAN_DISCOVERY) { bundle: Bundle -> bundle.adbMdnsLanDiscovery },
|
||||
bundleField(ADB_AUTO_LOAD_APP_LIST_ON_CONNECT) { bundle: Bundle -> bundle.adbAutoLoadAppListOnConnect },
|
||||
bundleField(PASSWORD_REQUIRE_AUTH) { bundle: Bundle -> bundle.passwordRequireAuth },
|
||||
bundleField(REALTIME_CLIPBOARD_SYNC_TO_DEVICE) { bundle: Bundle -> bundle.realtimeClipboardSyncToDevice },
|
||||
bundleField(FILE_MANAGER_SORT_BY) { bundle: Bundle -> bundle.fileManagerSortBy },
|
||||
bundleField(FILE_MANAGER_SORT_DESCENDING) { bundle: Bundle -> bundle.fileManagerSortDescending },
|
||||
bundleField(LAST_UPDATE_CHECK_AT) { bundle: Bundle -> bundle.lastUpdateCheckAt },
|
||||
private val bundleFields = arrayOf<BundleField<Bundle>>(
|
||||
// Theme
|
||||
bundleField(THEME_BASE_INDEX) { it.themeBaseIndex },
|
||||
bundleField(MONET) { it.monet },
|
||||
bundleField(BLUR) { it.blur },
|
||||
bundleField(FLOATING_BOTTOM_BAR) { it.floatingBottomBar },
|
||||
bundleField(FLOATING_BOTTOM_BAR_BLUR) { it.floatingBottomBarBlur },
|
||||
bundleField(SMOOTH_CORNER) { it.smoothCorner },
|
||||
|
||||
// Scrcpy
|
||||
bundleField(LOW_LATENCY) { it.lowLatency },
|
||||
bundleField(FULLSCREEN_DEBUG_INFO) { it.fullscreenDebugInfo },
|
||||
bundleField(HIDE_SIMPLE_CONFIG_ITEMS) { it.hideSimpleConfigItems },
|
||||
bundleField(DEVICE_PREVIEW_CARD_HEIGHT_DP) { it.devicePreviewCardHeightDp },
|
||||
bundleField(FULLSCREEN_CONTROL_IGNORE_SYSTEM_ROTATION_LOCK) { it.fullscreenControlIgnoreSystemRotationLock },
|
||||
bundleField(SHOW_FULLSCREEN_VIRTUAL_BUTTONS) { it.showFullscreenVirtualButtons },
|
||||
bundleField(FULLSCREEN_VIRTUAL_BUTTON_HEIGHT_DP) { it.fullscreenVirtualButtonHeightDp },
|
||||
bundleField(FULLSCREEN_VIRTUAL_BUTTON_DOCK) { it.fullscreenVirtualButtonDock },
|
||||
bundleField(SHOW_FULLSCREEN_FLOATING_BUTTON) { it.showFullscreenFloatingButton },
|
||||
bundleField(FULLSCREEN_FLOATING_BUTTON_SIZE_DP) { it.fullscreenFloatingButtonSizeDp },
|
||||
bundleField(FULLSCREEN_FLOATING_BUTTON_BACKGROUND_ALPHA_PERCENT) { it.fullscreenFloatingButtonBackgroundAlphaPercent },
|
||||
bundleField(FULLSCREEN_FLOATING_BUTTON_RING_ALPHA_PERCENT) { it.fullscreenFloatingButtonRingAlphaPercent },
|
||||
bundleField(REALTIME_CLIPBOARD_SYNC_TO_DEVICE) { it.realtimeClipboardSyncToDevice },
|
||||
|
||||
bundleField(FULLSCREEN_FLOATING_BUTTON_X_FRACTION) { it.fullscreenFloatingButtonXFraction },
|
||||
bundleField(FULLSCREEN_FLOATING_BUTTON_Y_FRACTION) { it.fullscreenFloatingButtonYFraction },
|
||||
bundleField(PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT) { it.previewVirtualButtonShowText },
|
||||
bundleField(VIRTUAL_BUTTONS_LAYOUT) { it.virtualButtonsLayout },
|
||||
|
||||
// Scrcpy Server
|
||||
bundleField(CUSTOM_SERVER_URI) { it.customServerUri },
|
||||
bundleField(CUSTOM_SERVER_VERSION) { it.customServerVersion },
|
||||
bundleField(SERVER_REMOTE_PATH) { it.serverRemotePath },
|
||||
|
||||
// ADB
|
||||
bundleField(ADB_KEY_NAME) { it.adbKeyName },
|
||||
bundleField(ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN) { it.adbPairingAutoDiscoverOnDialogOpen },
|
||||
bundleField(ADB_AUTO_RECONNECT_PAIRED_DEVICE) { it.adbAutoReconnectPairedDevice },
|
||||
bundleField(ADB_MDNS_LAN_DISCOVERY) { it.adbMdnsLanDiscovery },
|
||||
bundleField(ADB_AUTO_LOAD_APP_LIST_ON_CONNECT) { it.adbAutoLoadAppListOnConnect },
|
||||
|
||||
// Terminal
|
||||
bundleField(TERMINAL_FONT_SIZE_SP) { it.terminalFontSizeSp },
|
||||
bundleField(TERMINAL_FONT_DISPLAY_NAME) { it.terminalFontDisplayName },
|
||||
|
||||
bundleField(PASSWORD_REQUIRE_AUTH) { it.passwordRequireAuth },
|
||||
|
||||
bundleField(FILE_MANAGER_SORT_BY) { it.fileManagerSortBy },
|
||||
bundleField(FILE_MANAGER_SORT_DESCENDING) { it.fileManagerSortDescending },
|
||||
bundleField(LAST_UPDATE_CHECK_AT) { it.lastUpdateCheckAt },
|
||||
)
|
||||
|
||||
val bundleState: StateFlow<Bundle> = createBundleState(::bundleFromPreferences)
|
||||
|
||||
private fun bundleFromPreferences(preferences: Preferences) = Bundle(
|
||||
// Theme
|
||||
themeBaseIndex = preferences.read(THEME_BASE_INDEX),
|
||||
monet = preferences.read(MONET),
|
||||
blur = preferences.read(BLUR),
|
||||
floatingBottomBar = preferences.read(FLOATING_BOTTOM_BAR),
|
||||
floatingBottomBarBlur = preferences.read(FLOATING_BOTTOM_BAR_BLUR),
|
||||
smoothCorner = preferences.read(SMOOTH_CORNER),
|
||||
|
||||
// Scrcpy
|
||||
lowLatency = preferences.read(LOW_LATENCY),
|
||||
fullscreenDebugInfo = preferences.read(FULLSCREEN_DEBUG_INFO),
|
||||
showFullscreenVirtualButtons = preferences.read(SHOW_FULLSCREEN_VIRTUAL_BUTTONS),
|
||||
showFullscreenFloatingButton = preferences.read(SHOW_FULLSCREEN_FLOATING_BUTTON),
|
||||
fullscreenFloatingButtonXFraction = preferences.read(FULLSCREEN_FLOATING_BUTTON_X_FRACTION),
|
||||
fullscreenFloatingButtonYFraction = preferences.read(FULLSCREEN_FLOATING_BUTTON_Y_FRACTION),
|
||||
hideSimpleConfigItems = preferences.read(HIDE_SIMPLE_CONFIG_ITEMS),
|
||||
devicePreviewCardHeightDp = preferences.read(DEVICE_PREVIEW_CARD_HEIGHT_DP),
|
||||
fullscreenControlIgnoreSystemRotationLock =
|
||||
preferences.read(FULLSCREEN_CONTROL_IGNORE_SYSTEM_ROTATION_LOCK),
|
||||
showFullscreenVirtualButtons = preferences.read(SHOW_FULLSCREEN_VIRTUAL_BUTTONS),
|
||||
fullscreenVirtualButtonHeightDp = preferences.read(FULLSCREEN_VIRTUAL_BUTTON_HEIGHT_DP),
|
||||
fullscreenVirtualButtonDock = preferences.read(FULLSCREEN_VIRTUAL_BUTTON_DOCK),
|
||||
showFullscreenFloatingButton = preferences.read(SHOW_FULLSCREEN_FLOATING_BUTTON),
|
||||
fullscreenFloatingButtonSizeDp = preferences.read(FULLSCREEN_FLOATING_BUTTON_SIZE_DP),
|
||||
fullscreenFloatingButtonBackgroundAlphaPercent =
|
||||
preferences.read(FULLSCREEN_FLOATING_BUTTON_BACKGROUND_ALPHA_PERCENT),
|
||||
fullscreenFloatingButtonRingAlphaPercent =
|
||||
preferences.read(FULLSCREEN_FLOATING_BUTTON_RING_ALPHA_PERCENT),
|
||||
realtimeClipboardSyncToDevice = preferences.read(REALTIME_CLIPBOARD_SYNC_TO_DEVICE),
|
||||
|
||||
fullscreenFloatingButtonXFraction = preferences.read(FULLSCREEN_FLOATING_BUTTON_X_FRACTION),
|
||||
fullscreenFloatingButtonYFraction = preferences.read(FULLSCREEN_FLOATING_BUTTON_Y_FRACTION),
|
||||
previewVirtualButtonShowText = preferences.read(PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT),
|
||||
virtualButtonsLayout = preferences.read(VIRTUAL_BUTTONS_LAYOUT),
|
||||
|
||||
// Scrcpy Server
|
||||
customServerUri = preferences.read(CUSTOM_SERVER_URI),
|
||||
customServerVersion = preferences.read(CUSTOM_SERVER_VERSION),
|
||||
serverRemotePath = preferences.read(SERVER_REMOTE_PATH),
|
||||
|
||||
// ADB
|
||||
adbKeyName = preferences.read(ADB_KEY_NAME),
|
||||
adbPairingAutoDiscoverOnDialogOpen = preferences.read(
|
||||
ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN
|
||||
),
|
||||
adbPairingAutoDiscoverOnDialogOpen =
|
||||
preferences.read(ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN),
|
||||
adbAutoReconnectPairedDevice = preferences.read(ADB_AUTO_RECONNECT_PAIRED_DEVICE),
|
||||
adbMdnsLanDiscovery = preferences.read(ADB_MDNS_LAN_DISCOVERY),
|
||||
adbAutoLoadAppListOnConnect = preferences.read(ADB_AUTO_LOAD_APP_LIST_ON_CONNECT),
|
||||
|
||||
// Terminal
|
||||
terminalFontSizeSp = preferences.read(TERMINAL_FONT_SIZE_SP),
|
||||
terminalFontDisplayName = preferences.read(TERMINAL_FONT_DISPLAY_NAME),
|
||||
|
||||
passwordRequireAuth = preferences.read(PASSWORD_REQUIRE_AUTH),
|
||||
realtimeClipboardSyncToDevice = preferences.read(REALTIME_CLIPBOARD_SYNC_TO_DEVICE),
|
||||
fileManagerSortBy = preferences.read(FILE_MANAGER_SORT_BY),
|
||||
fileManagerSortDescending = preferences.read(FILE_MANAGER_SORT_DESCENDING),
|
||||
lastUpdateCheckAt = preferences.read(LAST_UPDATE_CHECK_AT),
|
||||
|
||||
@@ -1,564 +0,0 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val TAG = "PreferenceMigration"
|
||||
|
||||
/**
|
||||
* 从旧的 SharedPreferences 迁移到新的 DataStore
|
||||
*/
|
||||
class PreferenceMigration(private val appContext: Context) {
|
||||
private val appSharedPrefs: SharedPreferences by lazy {
|
||||
appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
private val sharedPrefs: SharedPreferences by lazy {
|
||||
appContext.getSharedPreferences("nativecore_adb_rsa", Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要迁移(SharedPreferences 是否包含数据)
|
||||
*/
|
||||
suspend fun needsMigration(): Boolean = withContext(Dispatchers.IO) {
|
||||
appSharedPrefs.all.isNotEmpty() || sharedPrefs.all.isNotEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行完整迁移
|
||||
*/
|
||||
suspend fun migrate(
|
||||
clearSharedPrefs: Boolean = false,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
if (!needsMigration()) {
|
||||
Log.i(TAG, "No data to migrate, skipping")
|
||||
return@withContext
|
||||
} else {
|
||||
val appList = appSharedPrefs.all.entries.joinToString { (k, v) -> "\"$k\" to $v" }
|
||||
val adbList = sharedPrefs.all.entries.joinToString { (k, v) -> "\"$k\" to $v" }
|
||||
Log.d(TAG, "Migrating appSharedPrefs ($appList)")
|
||||
}
|
||||
|
||||
Log.i(TAG, "Starting migration from SharedPreferences to DataStore")
|
||||
|
||||
// 迁移 AppSettings
|
||||
migrateAppSettings()
|
||||
|
||||
// 迁移 ScrcpyOptions
|
||||
migrateScrcpyOptions()
|
||||
|
||||
// 迁移 QuickDevices
|
||||
migrateQuickDevices()
|
||||
|
||||
// 迁移 ADB 密钥
|
||||
migrateAdbClientData()
|
||||
|
||||
// 清空 SharedPreferences
|
||||
if (clearSharedPrefs) {
|
||||
appSharedPrefs.edit { clear() }
|
||||
sharedPrefs.edit { clear() }
|
||||
Log.d(TAG, "SharedPreferences cleared")
|
||||
}
|
||||
|
||||
Log.i(
|
||||
TAG, "Migration completed successfully" +
|
||||
" and SharedPreferences ${if (clearSharedPrefs) "" else "is not "}cleared"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移应用设置
|
||||
*/
|
||||
private suspend fun migrateAppSettings() {
|
||||
val appSettings = Storage.appSettings
|
||||
|
||||
// Theme Settings
|
||||
migrateInt(
|
||||
THEME_BASE_INDEX,
|
||||
AppSettings.THEME_BASE_INDEX.defaultValue,
|
||||
appSettings.themeBaseIndex,
|
||||
)
|
||||
migrateBoolean(
|
||||
MONET,
|
||||
AppSettings.MONET.defaultValue,
|
||||
appSettings.monet,
|
||||
)
|
||||
|
||||
// Scrcpy Settings
|
||||
migrateBoolean(
|
||||
FULLSCREEN_DEBUG_INFO,
|
||||
AppSettings.FULLSCREEN_DEBUG_INFO.defaultValue,
|
||||
appSettings.fullscreenDebugInfo,
|
||||
)
|
||||
migrateBoolean(
|
||||
SHOW_FULLSCREEN_VIRTUAL_BUTTONS,
|
||||
AppSettings.SHOW_FULLSCREEN_VIRTUAL_BUTTONS.defaultValue,
|
||||
appSettings.showFullscreenVirtualButtons,
|
||||
)
|
||||
migrateInt(
|
||||
DEVICE_PREVIEW_CARD_HEIGHT_DP,
|
||||
AppSettings.DEVICE_PREVIEW_CARD_HEIGHT_DP.defaultValue,
|
||||
appSettings.devicePreviewCardHeightDp,
|
||||
)
|
||||
migrateBoolean(
|
||||
PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT,
|
||||
AppSettings.PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT.defaultValue,
|
||||
appSettings.previewVirtualButtonShowText,
|
||||
)
|
||||
migrateString(
|
||||
VIRTUAL_BUTTONS_LAYOUT,
|
||||
AppSettings.VIRTUAL_BUTTONS_LAYOUT.defaultValue,
|
||||
appSettings.virtualButtonsLayout,
|
||||
)
|
||||
|
||||
// Scrcpy Server Settings
|
||||
migrateString(
|
||||
CUSTOM_SERVER_URI,
|
||||
AppSettings.CUSTOM_SERVER_URI.defaultValue,
|
||||
appSettings.customServerUri,
|
||||
)
|
||||
migrateString(
|
||||
SERVER_REMOTE_PATH,
|
||||
AppSettings.SERVER_REMOTE_PATH.defaultValue,
|
||||
appSettings.serverRemotePath,
|
||||
)
|
||||
|
||||
// ADB Settings
|
||||
migrateString(
|
||||
ADB_KEY_NAME,
|
||||
AppSettings.ADB_KEY_NAME.defaultValue,
|
||||
appSettings.adbKeyName,
|
||||
)
|
||||
migrateBoolean(
|
||||
ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN,
|
||||
AppSettings.ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN.defaultValue,
|
||||
appSettings.adbPairingAutoDiscoverOnDialogOpen,
|
||||
)
|
||||
migrateBoolean(
|
||||
ADB_AUTO_RECONNECT_PAIRED_DEVICE,
|
||||
AppSettings.ADB_AUTO_RECONNECT_PAIRED_DEVICE.defaultValue,
|
||||
appSettings.adbAutoReconnectPairedDevice,
|
||||
)
|
||||
migrateBoolean(
|
||||
ADB_MDNS_LAN_DISCOVERY,
|
||||
AppSettings.ADB_MDNS_LAN_DISCOVERY.defaultValue,
|
||||
appSettings.adbMdnsLanDiscovery,
|
||||
)
|
||||
|
||||
Log.d(TAG, "AppSettings migration completed")
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移 Scrcpy 选项
|
||||
*/
|
||||
private suspend fun migrateScrcpyOptions() {
|
||||
val scrcpyOptions = Storage.scrcpyOptions
|
||||
|
||||
// Audio & Video Codecs
|
||||
migrateString(
|
||||
AUDIO_CODEC,
|
||||
ScrcpyOptions.AUDIO_CODEC.defaultValue,
|
||||
scrcpyOptions.audioCodec,
|
||||
)
|
||||
migrateString(
|
||||
VIDEO_CODEC,
|
||||
ScrcpyOptions.VIDEO_CODEC.defaultValue,
|
||||
scrcpyOptions.videoCodec,
|
||||
)
|
||||
|
||||
// Bit Rates
|
||||
val audioBitRateKbps = sharedPrefs.getInt(
|
||||
AUDIO_BIT_RATE_KBPS,
|
||||
ScrcpyOptions.AUDIO_BIT_RATE.defaultValue / 1_000,
|
||||
)
|
||||
scrcpyOptions.audioBitRate.set(audioBitRateKbps * 1_000) // Convert to bps
|
||||
|
||||
val videoBitRateMbps = sharedPrefs.getFloat(
|
||||
VIDEO_BIT_RATE_MBPS,
|
||||
(ScrcpyOptions.VIDEO_BIT_RATE.defaultValue / 1_000_000).toFloat(),
|
||||
)
|
||||
scrcpyOptions.videoBitRate.set((videoBitRateMbps * 1_000_000).toInt())
|
||||
|
||||
// Control Options
|
||||
migrateBoolean(
|
||||
TURN_SCREEN_OFF,
|
||||
ScrcpyOptions.TURN_SCREEN_OFF.defaultValue,
|
||||
scrcpyOptions.turnScreenOff,
|
||||
)
|
||||
migrateBoolean(
|
||||
NO_CONTROL,
|
||||
!ScrcpyOptions.CONTROL.defaultValue,
|
||||
) { value ->
|
||||
scrcpyOptions.control.set(!value) // Invert logic
|
||||
}
|
||||
migrateBoolean(
|
||||
NO_VIDEO,
|
||||
!ScrcpyOptions.VIDEO.defaultValue,
|
||||
) { value ->
|
||||
scrcpyOptions.video.set(!value) // Invert logic
|
||||
}
|
||||
|
||||
// Video Source
|
||||
val videoSourcePreset = sharedPrefs.getString(
|
||||
VIDEO_SOURCE_PRESET,
|
||||
ScrcpyOptions.VIDEO_SOURCE.defaultValue,
|
||||
).orEmpty().ifBlank { ScrcpyOptions.VIDEO_SOURCE.defaultValue }
|
||||
scrcpyOptions.videoSource.set(videoSourcePreset)
|
||||
|
||||
migrateString(
|
||||
DISPLAY_ID,
|
||||
ScrcpyOptions.DISPLAY_ID.defaultValue.toString(),
|
||||
) { value ->
|
||||
value.toIntOrNull()?.let { scrcpyOptions.displayId.set(it) }
|
||||
}
|
||||
|
||||
// Camera Settings
|
||||
migrateString(
|
||||
CAMERA_ID,
|
||||
ScrcpyOptions.CAMERA_ID.defaultValue,
|
||||
scrcpyOptions.cameraId,
|
||||
)
|
||||
migrateString(
|
||||
CAMERA_FACING_PRESET,
|
||||
ScrcpyOptions.CAMERA_FACING.defaultValue,
|
||||
scrcpyOptions.cameraFacing,
|
||||
)
|
||||
migrateString(
|
||||
CAMERA_SIZE_PRESET,
|
||||
ScrcpyOptions.CAMERA_SIZE.defaultValue,
|
||||
) { value ->
|
||||
if (value == "custom") {
|
||||
val customSize = sharedPrefs.getString(
|
||||
CAMERA_SIZE_CUSTOM,
|
||||
ScrcpyOptions.CAMERA_SIZE_CUSTOM.defaultValue,
|
||||
).orEmpty()
|
||||
scrcpyOptions.cameraSizeCustom.set(customSize)
|
||||
scrcpyOptions.cameraSizeUseCustom.set(true)
|
||||
} else {
|
||||
scrcpyOptions.cameraSize.set(value)
|
||||
}
|
||||
}
|
||||
migrateString(
|
||||
CAMERA_AR,
|
||||
ScrcpyOptions.CAMERA_AR.defaultValue,
|
||||
scrcpyOptions.cameraAr,
|
||||
)
|
||||
migrateString(
|
||||
CAMERA_FPS,
|
||||
ScrcpyOptions.CAMERA_FPS.defaultValue.toString(),
|
||||
) { value ->
|
||||
value.toIntOrNull()?.let { scrcpyOptions.cameraFps.set(it) }
|
||||
}
|
||||
migrateBoolean(
|
||||
CAMERA_HIGH_SPEED,
|
||||
ScrcpyOptions.CAMERA_HIGH_SPEED.defaultValue,
|
||||
scrcpyOptions.cameraHighSpeed,
|
||||
)
|
||||
|
||||
// Audio Source
|
||||
val audioSourcePreset = sharedPrefs.getString(
|
||||
AUDIO_SOURCE_PRESET,
|
||||
"auto",
|
||||
).orEmpty().ifBlank { "auto" }
|
||||
|
||||
if (audioSourcePreset == "custom") {
|
||||
val customSource = sharedPrefs.getString(
|
||||
AUDIO_SOURCE_CUSTOM,
|
||||
ScrcpyOptions.AUDIO_SOURCE.defaultValue,
|
||||
).orEmpty()
|
||||
scrcpyOptions.audioSource.set(customSource)
|
||||
} else {
|
||||
scrcpyOptions.audioSource.set(audioSourcePreset)
|
||||
}
|
||||
|
||||
migrateBoolean(
|
||||
AUDIO_DUP,
|
||||
ScrcpyOptions.AUDIO_DUP.defaultValue,
|
||||
scrcpyOptions.audioDup,
|
||||
)
|
||||
migrateBoolean(
|
||||
NO_AUDIO_PLAYBACK,
|
||||
!ScrcpyOptions.AUDIO_PLAYBACK.defaultValue,
|
||||
) { value ->
|
||||
scrcpyOptions.audioPlayback.set(!value) // Invert logic
|
||||
}
|
||||
migrateBoolean(
|
||||
REQUIRE_AUDIO,
|
||||
ScrcpyOptions.REQUIRE_AUDIO.defaultValue,
|
||||
scrcpyOptions.requireAudio,
|
||||
)
|
||||
|
||||
// Max Size & FPS
|
||||
migrateString(
|
||||
MAX_SIZE_INPUT,
|
||||
ScrcpyOptions.MAX_SIZE.defaultValue.toString(),
|
||||
) { value ->
|
||||
value.toIntOrNull()?.let { scrcpyOptions.maxSize.set(it) }
|
||||
}
|
||||
migrateString(
|
||||
MAX_FPS_INPUT,
|
||||
ScrcpyOptions.MAX_FPS.defaultValue,
|
||||
scrcpyOptions.maxFps,
|
||||
)
|
||||
|
||||
// Encoders & Codec Options
|
||||
migrateString(
|
||||
VIDEO_ENCODER,
|
||||
ScrcpyOptions.VIDEO_ENCODER.defaultValue,
|
||||
scrcpyOptions.videoEncoder,
|
||||
)
|
||||
migrateString(
|
||||
VIDEO_CODEC_OPTION,
|
||||
ScrcpyOptions.VIDEO_CODEC_OPTIONS.defaultValue,
|
||||
scrcpyOptions.videoCodecOptions,
|
||||
)
|
||||
migrateString(
|
||||
AUDIO_ENCODER,
|
||||
ScrcpyOptions.AUDIO_ENCODER.defaultValue,
|
||||
scrcpyOptions.audioEncoder,
|
||||
)
|
||||
migrateString(
|
||||
AUDIO_CODEC_OPTION,
|
||||
ScrcpyOptions.AUDIO_CODEC_OPTIONS.defaultValue,
|
||||
scrcpyOptions.audioCodecOptions,
|
||||
)
|
||||
|
||||
// New Display
|
||||
val newDisplayWidth = sharedPrefs.getString(
|
||||
NEW_DISPLAY_WIDTH,
|
||||
"",
|
||||
).orEmpty()
|
||||
val newDisplayHeight = sharedPrefs.getString(
|
||||
NEW_DISPLAY_HEIGHT,
|
||||
"",
|
||||
).orEmpty()
|
||||
val newDisplayDpi = sharedPrefs.getString(
|
||||
NEW_DISPLAY_DPI,
|
||||
"",
|
||||
).orEmpty()
|
||||
|
||||
if (newDisplayWidth.isNotBlank() && newDisplayHeight.isNotBlank()) {
|
||||
val newDisplay = if (newDisplayDpi.isNotBlank()) {
|
||||
"${newDisplayWidth}x${newDisplayHeight}/${newDisplayDpi}"
|
||||
} else {
|
||||
"${newDisplayWidth}x${newDisplayHeight}"
|
||||
}
|
||||
scrcpyOptions.newDisplay.set(newDisplay)
|
||||
}
|
||||
|
||||
// Crop
|
||||
val cropWidth = sharedPrefs.getString(
|
||||
CROP_WIDTH,
|
||||
"",
|
||||
).orEmpty()
|
||||
val cropHeight = sharedPrefs.getString(
|
||||
CROP_HEIGHT,
|
||||
"",
|
||||
).orEmpty()
|
||||
val cropX = sharedPrefs.getString(
|
||||
CROP_X,
|
||||
"",
|
||||
).orEmpty()
|
||||
val cropY = sharedPrefs.getString(
|
||||
CROP_Y,
|
||||
"",
|
||||
).orEmpty()
|
||||
|
||||
if (cropWidth.isNotBlank() && cropHeight.isNotBlank()
|
||||
&& cropX.isNotBlank() && cropY.isNotBlank()
|
||||
) {
|
||||
scrcpyOptions.crop.set("${cropWidth}:${cropHeight}:${cropX}:${cropY}")
|
||||
}
|
||||
|
||||
migrateBoolean(
|
||||
AUDIO_ENABLED,
|
||||
ScrcpyOptions.AUDIO.defaultValue,
|
||||
scrcpyOptions.audio,
|
||||
)
|
||||
|
||||
Log.d(TAG, "ScrcpyOptions migration completed")
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移快速设备列表
|
||||
*/
|
||||
private suspend fun migrateQuickDevices() {
|
||||
val quickDevices = Storage.quickDevices
|
||||
|
||||
// Migrate quick devices list
|
||||
val quickDevicesRaw = appSharedPrefs.getString(
|
||||
QUICK_DEVICES,
|
||||
""
|
||||
).orEmpty()
|
||||
if (quickDevicesRaw.isNotBlank()) {
|
||||
quickDevices.quickDevicesList.set(quickDevicesRaw)
|
||||
}
|
||||
|
||||
// Migrate quick connect input
|
||||
migrateString(
|
||||
QUICK_CONNECT_INPUT,
|
||||
QuickDevices.QUICK_CONNECT_INPUT.defaultValue,
|
||||
quickDevices.quickConnectInput,
|
||||
)
|
||||
|
||||
Log.d(TAG, "QuickDevices migration completed")
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移 ADB 客户端数据(RSA 密钥)
|
||||
*/
|
||||
private suspend fun migrateAdbClientData() {
|
||||
val adbClientData = Storage.adbClientData
|
||||
|
||||
// 迁移 RSA 私钥
|
||||
val privKey = sharedPrefs.getString("priv", null)
|
||||
if (privKey != null) {
|
||||
adbClientData.rsaPrivateKey.set(privKey)
|
||||
Log.d(TAG, "ADB RSA private key migrated")
|
||||
}
|
||||
|
||||
Log.d(TAG, "AdbClientData migration completed")
|
||||
}
|
||||
|
||||
// Helper methods for different data types
|
||||
|
||||
private suspend fun migrateString(
|
||||
key: String,
|
||||
defaultValue: String,
|
||||
settingProperty: Settings.SettingProperty<String>
|
||||
) {
|
||||
val value = appSharedPrefs.getString(key, defaultValue)
|
||||
.orEmpty()
|
||||
.ifBlank { defaultValue }
|
||||
settingProperty.set(value)
|
||||
}
|
||||
|
||||
private suspend fun migrateString(
|
||||
key: String,
|
||||
defaultValue: String,
|
||||
action: suspend (String) -> Unit
|
||||
) {
|
||||
val value = appSharedPrefs.getString(key, defaultValue)
|
||||
.orEmpty()
|
||||
.ifBlank { defaultValue }
|
||||
action(value)
|
||||
}
|
||||
|
||||
private suspend fun migrateInt(
|
||||
key: String,
|
||||
defaultValue: Int,
|
||||
settingProperty: Settings.SettingProperty<Int>
|
||||
) {
|
||||
val value = appSharedPrefs.getInt(key, defaultValue)
|
||||
settingProperty.set(value)
|
||||
}
|
||||
|
||||
private suspend fun migrateInt(
|
||||
key: String,
|
||||
defaultValue: Int,
|
||||
action: suspend (Int) -> Unit
|
||||
) {
|
||||
val value = appSharedPrefs.getInt(key, defaultValue)
|
||||
action(value)
|
||||
}
|
||||
|
||||
private suspend fun migrateBoolean(
|
||||
key: String,
|
||||
defaultValue: Boolean,
|
||||
settingProperty: Settings.SettingProperty<Boolean>
|
||||
) {
|
||||
val value = appSharedPrefs.getBoolean(key, defaultValue)
|
||||
settingProperty.set(value)
|
||||
}
|
||||
|
||||
private suspend fun migrateBoolean(
|
||||
key: String,
|
||||
defaultValue: Boolean,
|
||||
action: suspend (Boolean) -> Unit
|
||||
) {
|
||||
val value = appSharedPrefs.getBoolean(key, defaultValue)
|
||||
action(value)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREFS_NAME = "scrcpy_app_prefs"
|
||||
|
||||
// Devices
|
||||
const val QUICK_DEVICES = "quick_devices"
|
||||
const val QUICK_CONNECT_INPUT = "quick_connect_input"
|
||||
|
||||
const val PAIR_HOST = "pair_host"
|
||||
const val PAIR_PORT = "pair_port"
|
||||
const val PAIR_CODE = "pair_code"
|
||||
|
||||
const val AUDIO_ENABLED = "audio_enabled"
|
||||
const val AUDIO_CODEC = "audio_codec"
|
||||
const val AUDIO_BIT_RATE_INPUT = "audio_bit_rate_input"
|
||||
const val AUDIO_BIT_RATE_KBPS = "audio_bit_rate_kbps"
|
||||
const val VIDEO_CODEC = "video_codec"
|
||||
const val VIDEO_BIT_RATE_MBPS = "video_bit_rate_mbps"
|
||||
const val VIDEO_BIT_RATE_INPUT = "video_bit_rate_input"
|
||||
|
||||
const val TURN_SCREEN_OFF = "turn_screen_off"
|
||||
const val NO_CONTROL = "no_control"
|
||||
const val NO_VIDEO = "no_video"
|
||||
|
||||
const val VIDEO_SOURCE_PRESET = "video_source_preset"
|
||||
const val DISPLAY_ID = "display_id"
|
||||
|
||||
const val CAMERA_ID = "camera_id"
|
||||
const val CAMERA_FACING_PRESET = "camera_facing_preset"
|
||||
const val CAMERA_SIZE_PRESET = "camera_size_preset"
|
||||
const val CAMERA_SIZE_CUSTOM = "camera_size_custom"
|
||||
const val CAMERA_AR = "camera_ar"
|
||||
const val CAMERA_FPS = "camera_fps"
|
||||
const val CAMERA_HIGH_SPEED = "camera_high_speed"
|
||||
|
||||
const val AUDIO_SOURCE_PRESET = "audio_source_preset"
|
||||
const val AUDIO_SOURCE_CUSTOM = "audio_source_custom"
|
||||
const val AUDIO_DUP = "audio_dup"
|
||||
const val NO_AUDIO_PLAYBACK = "no_audio_playback"
|
||||
const val REQUIRE_AUDIO = "require_audio"
|
||||
|
||||
const val MAX_SIZE_INPUT = "max_size_input"
|
||||
const val MAX_FPS_INPUT = "max_fps_input"
|
||||
|
||||
const val VIDEO_ENCODER = "video_encoder"
|
||||
const val VIDEO_CODEC_OPTION = "video_codec_options"
|
||||
const val AUDIO_ENCODER = "audio_encoder"
|
||||
const val AUDIO_CODEC_OPTION = "audio_codec_options"
|
||||
|
||||
const val NEW_DISPLAY_WIDTH = "new_display_width"
|
||||
const val NEW_DISPLAY_HEIGHT = "new_display_height"
|
||||
const val NEW_DISPLAY_DPI = "new_display_dpi"
|
||||
|
||||
const val CROP_WIDTH = "crop_width"
|
||||
const val CROP_HEIGHT = "crop_height"
|
||||
const val CROP_X = "crop_x"
|
||||
const val CROP_Y = "crop_y"
|
||||
|
||||
// Settings
|
||||
const val THEME_BASE_INDEX = "theme_base_index"
|
||||
const val MONET = "monet"
|
||||
|
||||
const val FULLSCREEN_DEBUG_INFO = "fullscreen_debug_info"
|
||||
const val SHOW_FULLSCREEN_VIRTUAL_BUTTONS = "show_fullscreen_virtual_buttons"
|
||||
const val DEVICE_PREVIEW_CARD_HEIGHT_DP = "device_preview_card_height_dp"
|
||||
const val PREVIEW_VIRTUAL_BUTTON_SHOW_TEXT = "preview_virtual_button_show_text"
|
||||
const val VIRTUAL_BUTTONS_LAYOUT = "virtual_buttons_layout"
|
||||
|
||||
const val CUSTOM_SERVER_URI = "custom_server_uri"
|
||||
|
||||
const val SERVER_REMOTE_PATH = "server_remote_path"
|
||||
|
||||
const val ADB_KEY_NAME = "adb_key_name"
|
||||
const val ADB_PAIRING_AUTO_DISCOVER_ON_DIALOG_OPEN =
|
||||
"adb_pairing_auto_discover_on_dialog_open"
|
||||
const val ADB_AUTO_RECONNECT_PAIRED_DEVICE = "adb_auto_reconnect_paired_device"
|
||||
const val ADB_MDNS_LAN_DISCOVERY = "adb_mdns_lan_discovery"
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,9 @@ class QuickDevices(context: Context) : Settings(context, "QuickDevices") {
|
||||
) : Parcelable {
|
||||
}
|
||||
|
||||
private val bundleFields = arrayOf(
|
||||
bundleField(QUICK_DEVICES_LIST) { bundle: Bundle -> bundle.quickDevicesList },
|
||||
bundleField(QUICK_CONNECT_INPUT) { bundle: Bundle -> bundle.quickConnectInput },
|
||||
private val bundleFields = arrayOf<BundleField<Bundle>>(
|
||||
bundleField(QUICK_DEVICES_LIST) { it.quickDevicesList },
|
||||
bundleField(QUICK_CONNECT_INPUT) { it.quickConnectInput },
|
||||
)
|
||||
|
||||
val bundleState: StateFlow<Bundle> = createBundleState(::bundleFromPreferences)
|
||||
|
||||
@@ -200,6 +200,10 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
booleanPreferencesKey("downsize_on_error"),
|
||||
true,
|
||||
)
|
||||
val MOUSE_HOVER = Pair(
|
||||
booleanPreferencesKey("mouse_hover"),
|
||||
true,
|
||||
)
|
||||
val CLEANUP = Pair(
|
||||
booleanPreferencesKey("cleanup"),
|
||||
true,
|
||||
@@ -305,6 +309,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
legacyPaste = LEGACY_PASTE.defaultValue,
|
||||
clipboardAutosync = CLIPBOARD_AUTOSYNC.defaultValue,
|
||||
downsizeOnError = DOWNSIZE_ON_ERROR.defaultValue,
|
||||
mouseHover = MOUSE_HOVER.defaultValue,
|
||||
cleanup = CLEANUP.defaultValue,
|
||||
powerOn = POWER_ON.defaultValue,
|
||||
video = VIDEO.defaultValue,
|
||||
@@ -323,62 +328,6 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
)
|
||||
}
|
||||
|
||||
val crop by setting(CROP)
|
||||
val recordFilename by setting(RECORD_FILENAME)
|
||||
val videoCodecOptions by setting(VIDEO_CODEC_OPTIONS)
|
||||
val audioCodecOptions by setting(AUDIO_CODEC_OPTIONS)
|
||||
val videoEncoder by setting(VIDEO_ENCODER)
|
||||
val audioEncoder by setting(AUDIO_ENCODER)
|
||||
val cameraId by setting(CAMERA_ID)
|
||||
val cameraSize by setting(CAMERA_SIZE)
|
||||
val cameraSizeCustom by setting(CAMERA_SIZE_CUSTOM)
|
||||
val cameraSizeUseCustom by setting(CAMERA_SIZE_USE_CUSTOM)
|
||||
val cameraAr by setting(CAMERA_AR)
|
||||
val cameraFps by setting(CAMERA_FPS)
|
||||
val logLevel by setting(LOG_LEVEL)
|
||||
val videoCodec by setting(VIDEO_CODEC)
|
||||
val audioCodec by setting(AUDIO_CODEC)
|
||||
val videoSource by setting(VIDEO_SOURCE)
|
||||
val audioSource by setting(AUDIO_SOURCE)
|
||||
val recordFormat by setting(RECORD_FORMAT)
|
||||
val cameraFacing by setting(CAMERA_FACING)
|
||||
val maxSize by setting(MAX_SIZE)
|
||||
val videoBitRate by setting(VIDEO_BIT_RATE)
|
||||
val audioBitRate by setting(AUDIO_BIT_RATE)
|
||||
val maxFps by setting(MAX_FPS)
|
||||
val angle by setting(ANGLE)
|
||||
val captureOrientation by setting(CAPTURE_ORIENTATION)
|
||||
val captureOrientationLock by setting(CAPTURE_ORIENTATION_LOCK)
|
||||
val displayOrientation by setting(DISPLAY_ORIENTATION)
|
||||
val recordOrientation by setting(RECORD_ORIENTATION)
|
||||
val displayImePolicy by setting(DISPLAY_IME_POLICY)
|
||||
val displayId by setting(DISPLAY_ID)
|
||||
val screenOffTimeout by setting(SCREEN_OFF_TIMEOUT)
|
||||
val showTouches by setting(SHOW_TOUCHES)
|
||||
val fullscreen by setting(FULLSCREEN)
|
||||
val control by setting(CONTROL)
|
||||
val videoPlayback by setting(VIDEO_PLAYBACK)
|
||||
val audioPlayback by setting(AUDIO_PLAYBACK)
|
||||
val turnScreenOff by setting(TURN_SCREEN_OFF)
|
||||
val stayAwake by setting(STAY_AWAKE)
|
||||
val disableScreensaver by setting(DISABLE_SCREENSAVER)
|
||||
val powerOffOnClose by setting(POWER_OFF_ON_CLOSE)
|
||||
val legacyPaste by setting(LEGACY_PASTE)
|
||||
val clipboardAutosync by setting(CLIPBOARD_AUTOSYNC)
|
||||
val cleanup by setting(CLEANUP)
|
||||
val powerOn by setting(POWER_ON)
|
||||
val video by setting(VIDEO)
|
||||
val audio by setting(AUDIO)
|
||||
val requireAudio by setting(REQUIRE_AUDIO)
|
||||
val killAdbOnClose by setting(KILL_ADB_ON_CLOSE)
|
||||
val cameraHighSpeed by setting(CAMERA_HIGH_SPEED)
|
||||
val list by setting(LIST)
|
||||
val audioDup by setting(AUDIO_DUP)
|
||||
val newDisplay by setting(NEW_DISPLAY)
|
||||
val startApp by setting(START_APP)
|
||||
val vdDestroyContent by setting(VD_DESTROY_CONTENT)
|
||||
val vdSystemDecorations by setting(VD_SYSTEM_DECORATIONS)
|
||||
|
||||
@Parcelize
|
||||
data class Bundle(
|
||||
val crop: String,
|
||||
@@ -424,6 +373,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
val legacyPaste: Boolean,
|
||||
val clipboardAutosync: Boolean,
|
||||
val downsizeOnError: Boolean,
|
||||
val mouseHover: Boolean,
|
||||
val cleanup: Boolean,
|
||||
val powerOn: Boolean,
|
||||
val video: Boolean,
|
||||
@@ -442,64 +392,65 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
) : Parcelable {
|
||||
}
|
||||
|
||||
private val bundleFields = arrayOf(
|
||||
bundleField(CROP) { bundle: Bundle -> bundle.crop },
|
||||
bundleField(RECORD_FILENAME) { bundle: Bundle -> bundle.recordFilename },
|
||||
bundleField(VIDEO_CODEC_OPTIONS) { bundle: Bundle -> bundle.videoCodecOptions },
|
||||
bundleField(AUDIO_CODEC_OPTIONS) { bundle: Bundle -> bundle.audioCodecOptions },
|
||||
bundleField(VIDEO_ENCODER) { bundle: Bundle -> bundle.videoEncoder },
|
||||
bundleField(AUDIO_ENCODER) { bundle: Bundle -> bundle.audioEncoder },
|
||||
bundleField(CAMERA_ID) { bundle: Bundle -> bundle.cameraId },
|
||||
bundleField(CAMERA_SIZE) { bundle: Bundle -> bundle.cameraSize },
|
||||
bundleField(CAMERA_SIZE_CUSTOM) { bundle: Bundle -> bundle.cameraSizeCustom },
|
||||
bundleField(CAMERA_SIZE_USE_CUSTOM) { bundle: Bundle -> bundle.cameraSizeUseCustom },
|
||||
bundleField(CAMERA_AR) { bundle: Bundle -> bundle.cameraAr },
|
||||
bundleField(CAMERA_FPS) { bundle: Bundle -> bundle.cameraFps },
|
||||
bundleField(LOG_LEVEL) { bundle: Bundle -> bundle.logLevel },
|
||||
bundleField(VIDEO_CODEC) { bundle: Bundle -> bundle.videoCodec },
|
||||
bundleField(AUDIO_CODEC) { bundle: Bundle -> bundle.audioCodec },
|
||||
bundleField(VIDEO_SOURCE) { bundle: Bundle -> bundle.videoSource },
|
||||
bundleField(AUDIO_SOURCE) { bundle: Bundle -> bundle.audioSource },
|
||||
bundleField(RECORD_FORMAT) { bundle: Bundle -> bundle.recordFormat },
|
||||
bundleField(CAMERA_FACING) { bundle: Bundle -> bundle.cameraFacing },
|
||||
bundleField(MAX_SIZE) { bundle: Bundle -> bundle.maxSize },
|
||||
bundleField(VIDEO_BIT_RATE) { bundle: Bundle -> bundle.videoBitRate },
|
||||
bundleField(AUDIO_BIT_RATE) { bundle: Bundle -> bundle.audioBitRate },
|
||||
bundleField(MAX_FPS) { bundle: Bundle -> bundle.maxFps },
|
||||
bundleField(ANGLE) { bundle: Bundle -> bundle.angle },
|
||||
bundleField(CAPTURE_ORIENTATION) { bundle: Bundle -> bundle.captureOrientation },
|
||||
bundleField(CAPTURE_ORIENTATION_LOCK) { bundle: Bundle -> bundle.captureOrientationLock },
|
||||
bundleField(DISPLAY_ORIENTATION) { bundle: Bundle -> bundle.displayOrientation },
|
||||
bundleField(RECORD_ORIENTATION) { bundle: Bundle -> bundle.recordOrientation },
|
||||
bundleField(DISPLAY_IME_POLICY) { bundle: Bundle -> bundle.displayImePolicy },
|
||||
bundleField(DISPLAY_ID) { bundle: Bundle -> bundle.displayId },
|
||||
bundleField(SCREEN_OFF_TIMEOUT) { bundle: Bundle -> bundle.screenOffTimeout },
|
||||
bundleField(SHOW_TOUCHES) { bundle: Bundle -> bundle.showTouches },
|
||||
bundleField(FULLSCREEN) { bundle: Bundle -> bundle.fullscreen },
|
||||
bundleField(CONTROL) { bundle: Bundle -> bundle.control },
|
||||
bundleField(VIDEO_PLAYBACK) { bundle: Bundle -> bundle.videoPlayback },
|
||||
bundleField(AUDIO_PLAYBACK) { bundle: Bundle -> bundle.audioPlayback },
|
||||
bundleField(TURN_SCREEN_OFF) { bundle: Bundle -> bundle.turnScreenOff },
|
||||
bundleField(STAY_AWAKE) { bundle: Bundle -> bundle.stayAwake },
|
||||
bundleField(DISABLE_SCREENSAVER) { bundle: Bundle -> bundle.disableScreensaver },
|
||||
bundleField(POWER_OFF_ON_CLOSE) { bundle: Bundle -> bundle.powerOffOnClose },
|
||||
bundleField(LEGACY_PASTE) { bundle: Bundle -> bundle.legacyPaste },
|
||||
bundleField(CLIPBOARD_AUTOSYNC) { bundle: Bundle -> bundle.clipboardAutosync },
|
||||
bundleField(CLEANUP) { bundle: Bundle -> bundle.cleanup },
|
||||
bundleField(POWER_ON) { bundle: Bundle -> bundle.powerOn },
|
||||
bundleField(VIDEO) { bundle: Bundle -> bundle.video },
|
||||
bundleField(AUDIO) { bundle: Bundle -> bundle.audio },
|
||||
bundleField(REQUIRE_AUDIO) { bundle: Bundle -> bundle.requireAudio },
|
||||
bundleField(KILL_ADB_ON_CLOSE) { bundle: Bundle -> bundle.killAdbOnClose },
|
||||
bundleField(CAMERA_HIGH_SPEED) { bundle: Bundle -> bundle.cameraHighSpeed },
|
||||
bundleField(LIST) { bundle: Bundle -> bundle.list },
|
||||
bundleField(AUDIO_DUP) { bundle: Bundle -> bundle.audioDup },
|
||||
bundleField(NEW_DISPLAY) { bundle: Bundle -> bundle.newDisplay },
|
||||
bundleField(START_APP) { bundle: Bundle -> bundle.startApp },
|
||||
bundleField(START_APP_CUSTOM) { bundle: Bundle -> bundle.startAppCustom },
|
||||
bundleField(START_APP_USE_CUSTOM) { bundle: Bundle -> bundle.startAppUseCustom },
|
||||
bundleField(VD_DESTROY_CONTENT) { bundle: Bundle -> bundle.vdDestroyContent },
|
||||
bundleField(VD_SYSTEM_DECORATIONS) { bundle: Bundle -> bundle.vdSystemDecorations },
|
||||
private val bundleFields = arrayOf<BundleField<Bundle>>(
|
||||
bundleField(CROP) { it.crop },
|
||||
bundleField(RECORD_FILENAME) { it.recordFilename },
|
||||
bundleField(VIDEO_CODEC_OPTIONS) { it.videoCodecOptions },
|
||||
bundleField(AUDIO_CODEC_OPTIONS) { it.audioCodecOptions },
|
||||
bundleField(VIDEO_ENCODER) { it.videoEncoder },
|
||||
bundleField(AUDIO_ENCODER) { it.audioEncoder },
|
||||
bundleField(CAMERA_ID) { it.cameraId },
|
||||
bundleField(CAMERA_SIZE) { it.cameraSize },
|
||||
bundleField(CAMERA_SIZE_CUSTOM) { it.cameraSizeCustom },
|
||||
bundleField(CAMERA_SIZE_USE_CUSTOM) { it.cameraSizeUseCustom },
|
||||
bundleField(CAMERA_AR) { it.cameraAr },
|
||||
bundleField(CAMERA_FPS) { it.cameraFps },
|
||||
bundleField(LOG_LEVEL) { it.logLevel },
|
||||
bundleField(VIDEO_CODEC) { it.videoCodec },
|
||||
bundleField(AUDIO_CODEC) { it.audioCodec },
|
||||
bundleField(VIDEO_SOURCE) { it.videoSource },
|
||||
bundleField(AUDIO_SOURCE) { it.audioSource },
|
||||
bundleField(RECORD_FORMAT) { it.recordFormat },
|
||||
bundleField(CAMERA_FACING) { it.cameraFacing },
|
||||
bundleField(MAX_SIZE) { it.maxSize },
|
||||
bundleField(VIDEO_BIT_RATE) { it.videoBitRate },
|
||||
bundleField(AUDIO_BIT_RATE) { it.audioBitRate },
|
||||
bundleField(MAX_FPS) { it.maxFps },
|
||||
bundleField(ANGLE) { it.angle },
|
||||
bundleField(CAPTURE_ORIENTATION) { it.captureOrientation },
|
||||
bundleField(CAPTURE_ORIENTATION_LOCK) { it.captureOrientationLock },
|
||||
bundleField(DISPLAY_ORIENTATION) { it.displayOrientation },
|
||||
bundleField(RECORD_ORIENTATION) { it.recordOrientation },
|
||||
bundleField(DISPLAY_IME_POLICY) { it.displayImePolicy },
|
||||
bundleField(DISPLAY_ID) { it.displayId },
|
||||
bundleField(SCREEN_OFF_TIMEOUT) { it.screenOffTimeout },
|
||||
bundleField(SHOW_TOUCHES) { it.showTouches },
|
||||
bundleField(FULLSCREEN) { it.fullscreen },
|
||||
bundleField(CONTROL) { it.control },
|
||||
bundleField(VIDEO_PLAYBACK) { it.videoPlayback },
|
||||
bundleField(AUDIO_PLAYBACK) { it.audioPlayback },
|
||||
bundleField(TURN_SCREEN_OFF) { it.turnScreenOff },
|
||||
bundleField(STAY_AWAKE) { it.stayAwake },
|
||||
bundleField(DISABLE_SCREENSAVER) { it.disableScreensaver },
|
||||
bundleField(POWER_OFF_ON_CLOSE) { it.powerOffOnClose },
|
||||
bundleField(LEGACY_PASTE) { it.legacyPaste },
|
||||
bundleField(CLIPBOARD_AUTOSYNC) { it.clipboardAutosync },
|
||||
bundleField(MOUSE_HOVER) { it.mouseHover },
|
||||
bundleField(CLEANUP) { it.cleanup },
|
||||
bundleField(POWER_ON) { it.powerOn },
|
||||
bundleField(VIDEO) { it.video },
|
||||
bundleField(AUDIO) { it.audio },
|
||||
bundleField(REQUIRE_AUDIO) { it.requireAudio },
|
||||
bundleField(KILL_ADB_ON_CLOSE) { it.killAdbOnClose },
|
||||
bundleField(CAMERA_HIGH_SPEED) { it.cameraHighSpeed },
|
||||
bundleField(LIST) { it.list },
|
||||
bundleField(AUDIO_DUP) { it.audioDup },
|
||||
bundleField(NEW_DISPLAY) { it.newDisplay },
|
||||
bundleField(START_APP) { it.startApp },
|
||||
bundleField(START_APP_CUSTOM) { it.startAppCustom },
|
||||
bundleField(START_APP_USE_CUSTOM) { it.startAppUseCustom },
|
||||
bundleField(VD_DESTROY_CONTENT) { it.vdDestroyContent },
|
||||
bundleField(VD_SYSTEM_DECORATIONS) { it.vdSystemDecorations },
|
||||
)
|
||||
|
||||
val bundleState: StateFlow<Bundle> = createBundleState(::bundleFromPreferences)
|
||||
@@ -548,6 +499,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
legacyPaste = preferences.read(LEGACY_PASTE),
|
||||
clipboardAutosync = preferences.read(CLIPBOARD_AUTOSYNC),
|
||||
downsizeOnError = preferences.read(DOWNSIZE_ON_ERROR),
|
||||
mouseHover = preferences.read(MOUSE_HOVER),
|
||||
cleanup = preferences.read(CLEANUP),
|
||||
powerOn = preferences.read(POWER_ON),
|
||||
video = preferences.read(VIDEO),
|
||||
@@ -625,6 +577,7 @@ class ScrcpyOptions(context: Context) : Settings(context, "ScrcpyOptions") {
|
||||
legacyPaste = bundle.legacyPaste,
|
||||
clipboardAutosync = bundle.clipboardAutosync,
|
||||
downsizeOnError = bundle.downsizeOnError,
|
||||
mouseHover = bundle.mouseHover,
|
||||
cleanup = bundle.cleanup,
|
||||
powerOn = bundle.powerOn,
|
||||
video = bundle.video,
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
package io.github.miuzarte.scrcpyforandroid.widgets
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.yukonga.miuix.kmp.basic.DropdownColors
|
||||
import top.yukonga.miuix.kmp.basic.DropdownDefaults
|
||||
import top.yukonga.miuix.kmp.basic.BasicComponent
|
||||
import top.yukonga.miuix.kmp.basic.BasicComponentColors
|
||||
import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
|
||||
import top.yukonga.miuix.kmp.basic.DropdownArrowEndAction
|
||||
import top.yukonga.miuix.kmp.basic.DropdownImpl
|
||||
import top.yukonga.miuix.kmp.basic.HorizontalDivider
|
||||
import top.yukonga.miuix.kmp.basic.ListPopupColumn
|
||||
import top.yukonga.miuix.kmp.basic.PopupPositionProvider
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.overlay.OverlayListPopup
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
|
||||
data class MultiGroupsDropdownGroup(
|
||||
val options: List<String>,
|
||||
@@ -13,10 +33,91 @@ data class MultiGroupsDropdownGroup(
|
||||
val onSelectedIndexChange: (Int) -> Unit,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MultiGroupsDropdownPreference(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
summary: String? = null,
|
||||
groups: List<MultiGroupsDropdownGroup>,
|
||||
titleColor: BasicComponentColors = BasicComponentDefaults.titleColor(),
|
||||
summaryColor: BasicComponentColors = BasicComponentDefaults.summaryColor(),
|
||||
insideMargin: PaddingValues = BasicComponentDefaults.InsideMargin,
|
||||
dropdownColors: DropdownColors = DropdownDefaults.dropdownColors(),
|
||||
enabled: Boolean = true,
|
||||
showValue: Boolean = true,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isExpanded = remember { mutableStateOf(false) }
|
||||
val isHoldDown = remember { mutableStateOf(false) }
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val currentHapticFeedback by rememberUpdatedState(hapticFeedback)
|
||||
|
||||
val hasOptions = groups.any { it.options.isNotEmpty() }
|
||||
val actualEnabled = enabled && hasOptions
|
||||
val actionColor = if (actualEnabled) {
|
||||
MiuixTheme.colorScheme.onSurfaceVariantActions
|
||||
} else {
|
||||
MiuixTheme.colorScheme.disabledOnSecondaryVariant
|
||||
}
|
||||
val selectedValueText = groups.joinToString("\n") { group ->
|
||||
group.options.getOrNull(group.selectedIndex).orEmpty()
|
||||
}.ifBlank { null }
|
||||
|
||||
BasicComponent(
|
||||
modifier = modifier,
|
||||
interactionSource = interactionSource,
|
||||
insideMargin = insideMargin,
|
||||
title = title,
|
||||
titleColor = titleColor,
|
||||
summary = summary,
|
||||
summaryColor = summaryColor,
|
||||
endActions = {
|
||||
if (showValue && !selectedValueText.isNullOrBlank()) {
|
||||
Text(
|
||||
text = selectedValueText,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
fontSize = MiuixTheme.textStyles.body2.fontSize,
|
||||
color = actionColor,
|
||||
textAlign = TextAlign.End,
|
||||
lineHeight = MiuixTheme.textStyles.body2.lineHeight,
|
||||
)
|
||||
}
|
||||
DropdownArrowEndAction(actionColor = actionColor)
|
||||
if (hasOptions) {
|
||||
OverlayListPopup(
|
||||
show = isExpanded.value,
|
||||
alignment = PopupPositionProvider.Align.End,
|
||||
onDismissRequest = { isExpanded.value = false },
|
||||
onDismissFinished = { isHoldDown.value = false },
|
||||
) {
|
||||
ListPopupColumn {
|
||||
MultiGroupsDropdown(
|
||||
groups = groups,
|
||||
dropdownColors = dropdownColors,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (actualEnabled) {
|
||||
isExpanded.value = !isExpanded.value
|
||||
if (isExpanded.value) {
|
||||
isHoldDown.value = true
|
||||
currentHapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick)
|
||||
}
|
||||
}
|
||||
},
|
||||
holdDownState = isHoldDown.value,
|
||||
enabled = actualEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MultiGroupsDropdown(
|
||||
groups: List<MultiGroupsDropdownGroup>,
|
||||
modifier: Modifier = Modifier,
|
||||
dropdownColors: DropdownColors = DropdownDefaults.dropdownColors(),
|
||||
) {
|
||||
groups.forEachIndexed { groupIndex, group ->
|
||||
group.options.forEachIndexed { optionIndex, option ->
|
||||
@@ -24,6 +125,7 @@ fun MultiGroupsDropdown(
|
||||
text = option,
|
||||
optionSize = group.options.size,
|
||||
isSelected = optionIndex == group.selectedIndex,
|
||||
dropdownColors = dropdownColors,
|
||||
index = optionIndex,
|
||||
onSelectedIndexChange = group.onSelectedIndexChange,
|
||||
)
|
||||
|
||||
@@ -5,11 +5,14 @@ import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -47,6 +50,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.miuzarte.scrcpyforandroid.constants.UiAndroidKeycodes
|
||||
@@ -229,6 +233,13 @@ class VirtualButtonBar(
|
||||
private val outsideActions: List<VirtualButtonAction>,
|
||||
private val moreActions: List<VirtualButtonAction>,
|
||||
) {
|
||||
enum class FullscreenDock {
|
||||
TOP,
|
||||
BOTTOM,
|
||||
LEFT,
|
||||
RIGHT,
|
||||
}
|
||||
|
||||
private enum class ActionPopupDestination {
|
||||
Actions,
|
||||
Passwords,
|
||||
@@ -331,6 +342,9 @@ class VirtualButtonBar(
|
||||
fun Fullscreen(
|
||||
onAction: suspend (VirtualButtonAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
dock: FullscreenDock = FullscreenDock.BOTTOM,
|
||||
reverseOrder: Boolean = false,
|
||||
thickness: Dp = 16.dp,
|
||||
passwordPopupContent: (@Composable (onDismissRequest: () -> Unit) -> Unit)? = null,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -338,11 +352,30 @@ class VirtualButtonBar(
|
||||
var showMorePopup by remember { mutableStateOf(false) }
|
||||
var showPasswordPopup by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
||||
val isVertical = dock == FullscreenDock.LEFT || dock == FullscreenDock.RIGHT
|
||||
val visibleActions =
|
||||
if (reverseOrder) outsideActions.asReversed()
|
||||
else outsideActions
|
||||
val containerModifier =
|
||||
if (isVertical) modifier
|
||||
.width(thickness)
|
||||
.fillMaxHeight()
|
||||
else modifier
|
||||
.fillMaxWidth()
|
||||
.height(thickness)
|
||||
|
||||
val buttonModifier =
|
||||
if (isVertical) Modifier
|
||||
.fillMaxSize()
|
||||
else Modifier
|
||||
.fillMaxWidth()
|
||||
.height(thickness)
|
||||
|
||||
if (isVertical) Column(
|
||||
modifier = containerModifier,
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
) {
|
||||
outsideActions.forEach { action ->
|
||||
visibleActions.forEach { action ->
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
Button(
|
||||
onClick = {
|
||||
@@ -360,9 +393,62 @@ class VirtualButtonBar(
|
||||
else -> scope.launch { onAction(action) }
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = buttonModifier,
|
||||
cornerRadius = 0.dp,
|
||||
minHeight = 16.dp,
|
||||
minHeight = thickness,
|
||||
insideMargin = PaddingValues(0.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
color = Color.Black.copy(alpha = 0.1f),
|
||||
),
|
||||
) {
|
||||
Icon(action.icon, contentDescription = action.title, tint = Color.White)
|
||||
}
|
||||
|
||||
if (action == VirtualButtonAction.MORE) {
|
||||
ActionPopup(
|
||||
show = showMorePopup,
|
||||
actions = moreActions,
|
||||
onDismiss = { showMorePopup = false },
|
||||
onAction = {
|
||||
if (it == VirtualButtonAction.PASSWORD_INPUT
|
||||
&& passwordPopupContent != null
|
||||
) showPasswordPopup = true
|
||||
else onAction(it)
|
||||
|
||||
showMorePopup = false
|
||||
},
|
||||
passwordPopupContent = passwordPopupContent,
|
||||
renderInRootScaffold = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else Row(
|
||||
modifier = containerModifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
||||
) {
|
||||
visibleActions.forEach { action ->
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
Button(
|
||||
onClick = {
|
||||
haptics.contextClick()
|
||||
when (action) {
|
||||
VirtualButtonAction.MORE -> {
|
||||
showMorePopup = true
|
||||
}
|
||||
|
||||
VirtualButtonAction.PASSWORD_INPUT
|
||||
if passwordPopupContent != null -> {
|
||||
showPasswordPopup = true
|
||||
}
|
||||
|
||||
else -> scope.launch { onAction(action) }
|
||||
}
|
||||
},
|
||||
modifier = buttonModifier,
|
||||
cornerRadius = 0.dp,
|
||||
minHeight = thickness,
|
||||
insideMargin = PaddingValues(0.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
color = Color.Black.copy(alpha = 0.1f),
|
||||
@@ -448,9 +534,15 @@ class VirtualButtonBar(
|
||||
BoxWithConstraints(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
val ballSize = 48.dp
|
||||
val ringSize = 24.dp
|
||||
val ringWidth = 2.dp
|
||||
val ballSize = asBundleShared.fullscreenFloatingButtonSizeDp.dp
|
||||
val ringSize = ballSize / 2
|
||||
val ringWidth = ballSize / 24
|
||||
val backgroundAlpha =
|
||||
(asBundleShared.fullscreenFloatingButtonBackgroundAlphaPercent / 100f)
|
||||
.coerceIn(0.1f, 1f)
|
||||
val ringAlpha =
|
||||
(asBundleShared.fullscreenFloatingButtonRingAlphaPercent / 100f)
|
||||
.coerceIn(0f, 1f)
|
||||
val maxX = (maxWidth - ballSize).coerceAtLeast(0.dp)
|
||||
val maxY = (maxHeight - ballSize).coerceAtLeast(0.dp)
|
||||
val currentX =
|
||||
@@ -507,14 +599,24 @@ class VirtualButtonBar(
|
||||
minHeight = ballSize,
|
||||
insideMargin = PaddingValues(0.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
color = Color.Black.copy(alpha = 0.24f),
|
||||
color = Color.Black.copy(alpha = backgroundAlpha),
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(ringSize)
|
||||
.clip(CircleShape)
|
||||
.border(ringWidth, Color.White, CircleShape),
|
||||
.then(
|
||||
if (ringAlpha > 0f) {
|
||||
Modifier.border(
|
||||
ringWidth,
|
||||
Color.White.copy(alpha = ringAlpha),
|
||||
CircleShape,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="132"
|
||||
android:viewportHeight="66">
|
||||
<path
|
||||
android:pathData="M52.3,1.6c-5.7,2.1 -12.9,8.6 -16,14.8 -2.2,4.1 -2.8,6.9 -3.1,14.3 -0.6,12.6 1.3,17.8 9.3,25.8 8,8 13.2,9.9 25.8,9.3 11.1,-0.5 17.3,-3.2 23.5,-10.3 6.5,-7.4 7.2,-10.8 7.2,-34.7l0,-20.8 -21.2,0.1c-16.1,-0 -22.3,0.4 -25.5,1.5z"
|
||||
android:fillColor="#2196F3"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="132"
|
||||
android:viewportHeight="66">
|
||||
<path
|
||||
android:pathData="M33,20.8c0,23.9 0.7,27.3 7.2,34.7 6.2,7.1 12.4,9.8 23.5,10.3 12.6,0.6 17.8,-1.3 25.8,-9.3 8,-8 9.9,-13.2 9.3,-25.8 -0.5,-11.1 -3.2,-17.3 -10.3,-23.5 -7.4,-6.5 -10.8,-7.2 -34.7,-7.2l-20.8,-0 0,20.8z"
|
||||
android:fillColor="#2196F3"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
Reference in New Issue
Block a user