This commit is contained in:
@@ -69,4 +69,7 @@ dependencies {
|
|||||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
implementation 'androidx.compose.material3:material3'
|
implementation 'androidx.compose.material3:material3'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
|
||||||
|
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import org.lib3270j.protocol.DS3270Constants.AID_ENTER
|
import android.content.Context
|
||||||
|
import android.view.KeyCharacterMap
|
||||||
|
import org.lib3270j.protocol.DS3270Constants.*
|
||||||
import org.pubvm.a3270.service.TerminalService
|
import org.pubvm.a3270.service.TerminalService
|
||||||
import org.pubvm.a3270.storage.HostStorage
|
import org.pubvm.a3270.storage.HostStorage
|
||||||
import org.pubvm.a3270.ui.ConnectDialog
|
import org.pubvm.a3270.ui.ConnectDialog
|
||||||
@@ -97,68 +99,280 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val log = java.util.logging.Logger.getLogger("a3270-MainActivity")
|
||||||
|
private var deadKey: Int = 0
|
||||||
|
|
||||||
|
private fun getEffectiveUnicodeChar(event: KeyEvent): Int {
|
||||||
|
val cleanMeta = event.metaState and (
|
||||||
|
KeyEvent.META_SHIFT_ON or
|
||||||
|
KeyEvent.META_SHIFT_LEFT_ON or
|
||||||
|
KeyEvent.META_SHIFT_RIGHT_ON or
|
||||||
|
KeyEvent.META_CAPS_LOCK_ON
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1. Try getUnicodeChar with cleanMeta
|
||||||
|
var u = event.getUnicodeChar(cleanMeta)
|
||||||
|
if (u > 0) return u
|
||||||
|
|
||||||
|
// 2. Try unicodeChar property
|
||||||
|
u = event.unicodeChar
|
||||||
|
if (u > 0) return u
|
||||||
|
|
||||||
|
// 3. Try KeyCharacterMap with cleanMeta
|
||||||
|
val kcm = event.keyCharacterMap ?: KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD)
|
||||||
|
u = kcm.get(event.keyCode, cleanMeta)
|
||||||
|
if (u > 0) return u
|
||||||
|
|
||||||
|
// 4. Try KeyCharacterMap with meta = 0
|
||||||
|
u = kcm.get(event.keyCode, 0)
|
||||||
|
if (u > 0) {
|
||||||
|
val shift = event.isShiftPressed || (event.metaState and KeyEvent.META_SHIFT_ON != 0)
|
||||||
|
if (shift) {
|
||||||
|
val c = u.toChar()
|
||||||
|
if (c.isLowerCase()) return c.uppercaseChar().code
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Try displayLabel as fallback
|
||||||
|
val label = event.displayLabel
|
||||||
|
if (label >= ' ') {
|
||||||
|
val shift = event.isShiftPressed || (event.metaState and KeyEvent.META_SHIFT_ON != 0)
|
||||||
|
return if (shift) label.uppercaseChar().code else label.lowercaseChar().code
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
val shift = event.isShiftPressed || (event.metaState and KeyEvent.META_SHIFT_ON != 0)
|
val shift = event.isShiftPressed || (event.metaState and KeyEvent.META_SHIFT_ON != 0)
|
||||||
if (isShiftPressedState.value != shift) {
|
if (isShiftPressedState.value != shift) {
|
||||||
isShiftPressedState.value = shift
|
isShiftPressedState.value = shift
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle string / multi-character input (e.g. adb input text, barcode scanners, fast text input)
|
||||||
|
if (event.action == KeyEvent.ACTION_MULTIPLE) {
|
||||||
|
if (event.keyCode == KeyEvent.KEYCODE_UNKNOWN) {
|
||||||
|
val chars = event.characters
|
||||||
|
if (!chars.isNullOrEmpty()) {
|
||||||
|
viewModel.typeString(chars)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val unicode = getEffectiveUnicodeChar(event)
|
||||||
|
if (unicode >= 32) {
|
||||||
|
val count = maxOf(1, event.repeatCount)
|
||||||
|
viewModel.typeString(unicode.toChar().toString().repeat(count))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||||
val keyCode = event.keyCode
|
val keyCode = event.keyCode
|
||||||
|
val ctrl = event.isCtrlPressed || (event.metaState and KeyEvent.META_CTRL_ON != 0)
|
||||||
|
val alt = event.isAltPressed || (event.metaState and KeyEvent.META_ALT_ON != 0)
|
||||||
|
|
||||||
|
// 1. Function Keys (F1..F12 -> PF1..PF12 or PF13..PF24 with Shift or Alt)
|
||||||
if (keyCode in KeyEvent.KEYCODE_F1..KeyEvent.KEYCODE_F12) {
|
if (keyCode in KeyEvent.KEYCODE_F1..KeyEvent.KEYCODE_F12) {
|
||||||
val fNum = keyCode - KeyEvent.KEYCODE_F1 + 1
|
val fNum = keyCode - KeyEvent.KEYCODE_F1 + 1
|
||||||
val pfNum = if (shift) fNum + 12 else fNum
|
val pfNum = if (shift || alt) fNum + 12 else fNum
|
||||||
val pfAids = intArrayOf(
|
val pfAids = intArrayOf(
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF1, org.lib3270j.protocol.DS3270Constants.AID_PF2,
|
AID_PF1, AID_PF2, AID_PF3, AID_PF4, AID_PF5, AID_PF6,
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF3, org.lib3270j.protocol.DS3270Constants.AID_PF4,
|
AID_PF7, AID_PF8, AID_PF9, AID_PF10, AID_PF11, AID_PF12,
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF5, org.lib3270j.protocol.DS3270Constants.AID_PF6,
|
AID_PF13, AID_PF14, AID_PF15, AID_PF16, AID_PF17, AID_PF18,
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF7, org.lib3270j.protocol.DS3270Constants.AID_PF8,
|
AID_PF19, AID_PF20, AID_PF21, AID_PF22, AID_PF23, AID_PF24
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF9, org.lib3270j.protocol.DS3270Constants.AID_PF10,
|
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF11, org.lib3270j.protocol.DS3270Constants.AID_PF12,
|
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF13, org.lib3270j.protocol.DS3270Constants.AID_PF14,
|
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF15, org.lib3270j.protocol.DS3270Constants.AID_PF16,
|
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF17, org.lib3270j.protocol.DS3270Constants.AID_PF18,
|
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF19, org.lib3270j.protocol.DS3270Constants.AID_PF20,
|
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF21, org.lib3270j.protocol.DS3270Constants.AID_PF22,
|
|
||||||
org.lib3270j.protocol.DS3270Constants.AID_PF23, org.lib3270j.protocol.DS3270Constants.AID_PF24
|
|
||||||
)
|
)
|
||||||
viewModel.sendAid(pfAids[pfNum - 1])
|
viewModel.sendAid(pfAids[pfNum - 1])
|
||||||
if (shift) {
|
if (shift) {
|
||||||
isShiftPressedState.value = false
|
isShiftPressedState.value = false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
}
|
||||||
viewModel.cursorLeft()
|
|
||||||
return true
|
// 2. Ctrl Shortcuts
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
|
if (ctrl) {
|
||||||
viewModel.cursorUp()
|
when (keyCode) {
|
||||||
return true
|
KeyEvent.KEYCODE_V -> {
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
|
pasteFromClipboard()
|
||||||
viewModel.cursorDown()
|
return true
|
||||||
return true
|
}
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
KeyEvent.KEYCODE_R -> {
|
||||||
viewModel.cursorRight()
|
viewModel.resetKeyboard()
|
||||||
return true
|
return true
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER) {
|
}
|
||||||
viewModel.sendAid(AID_ENTER)
|
KeyEvent.KEYCODE_L -> {
|
||||||
return true
|
viewModel.sendAid(AID_CLEAR)
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_TAB) {
|
return true
|
||||||
if (event.isShiftPressed) {
|
}
|
||||||
viewModel.backTab()
|
KeyEvent.KEYCODE_Z -> {
|
||||||
} else {
|
viewModel.sendAid(AID_PA1)
|
||||||
viewModel.tab()
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_HOME -> {
|
||||||
|
viewModel.cursorHome()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_MOVE_END -> {
|
||||||
|
viewModel.eraseEof()
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
}
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_DEL) {
|
|
||||||
viewModel.backspace()
|
// 3. Alt Shortcuts (PA keys, Clear, Reset)
|
||||||
return true
|
if (alt) {
|
||||||
} else if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
|
when (keyCode) {
|
||||||
viewModel.resetKeyboard()
|
KeyEvent.KEYCODE_1 -> {
|
||||||
|
viewModel.sendAid(AID_PA1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_2 -> {
|
||||||
|
viewModel.sendAid(AID_PA2)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_3 -> {
|
||||||
|
viewModel.sendAid(AID_PA3)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_C -> {
|
||||||
|
viewModel.sendAid(AID_CLEAR)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_R -> {
|
||||||
|
viewModel.resetKeyboard()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Shift Shortcuts
|
||||||
|
if (shift && keyCode == KeyEvent.KEYCODE_INSERT) {
|
||||||
|
pasteFromClipboard()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. Standard Navigation & Special Keys
|
||||||
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||||
|
viewModel.cursorLeft()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||||
|
viewModel.cursorUp()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN -> {
|
||||||
|
viewModel.cursorDown()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||||
|
viewModel.cursorRight()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_MOVE_HOME -> {
|
||||||
|
viewModel.cursorHome()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_MOVE_END -> {
|
||||||
|
viewModel.eraseEof()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_PAGE_UP -> {
|
||||||
|
viewModel.sendAid(AID_PF7)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_PAGE_DOWN -> {
|
||||||
|
viewModel.sendAid(AID_PF8)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
|
||||||
|
viewModel.sendAid(AID_ENTER)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_TAB -> {
|
||||||
|
if (shift) {
|
||||||
|
viewModel.backTab()
|
||||||
|
isShiftPressedState.value = false
|
||||||
|
} else {
|
||||||
|
viewModel.tab()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DEL -> {
|
||||||
|
viewModel.backspace()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_FORWARD_DEL -> {
|
||||||
|
viewModel.deleteChar()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_ESCAPE -> {
|
||||||
|
if (shift) {
|
||||||
|
viewModel.sendAid(AID_CLEAR)
|
||||||
|
isShiftPressedState.value = false
|
||||||
|
} else {
|
||||||
|
viewModel.resetKeyboard()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_BREAK -> {
|
||||||
|
viewModel.sendAid(AID_PA1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Alphanumeric & Symbol Character Input
|
||||||
|
val unicode = getEffectiveUnicodeChar(event)
|
||||||
|
log.info("dispatchKeyEvent DOWN: keyCode=$keyCode, metaState=${event.metaState}, unicode=$unicode, char='${if (unicode > 0) unicode.toChar() else ' '}'")
|
||||||
|
if (unicode > 0) {
|
||||||
|
if ((unicode and KeyCharacterMap.COMBINING_ACCENT) != 0) {
|
||||||
|
deadKey = unicode and KeyCharacterMap.COMBINING_ACCENT_MASK
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val charToType = if (deadKey != 0) {
|
||||||
|
val combined = KeyCharacterMap.getDeadChar(deadKey, unicode)
|
||||||
|
deadKey = 0
|
||||||
|
if (combined != 0) combined.toChar() else unicode.toChar()
|
||||||
|
} else {
|
||||||
|
unicode.toChar()
|
||||||
|
}
|
||||||
|
if (charToType >= ' ') {
|
||||||
|
log.info("dispatchKeyEvent typing char: '$charToType'")
|
||||||
|
viewModel.typeString(charToType.toString())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("dispatchKeyEvent unhandled DOWN keyCode=$keyCode, metaState=${event.metaState}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.action == KeyEvent.ACTION_UP) {
|
||||||
|
when (event.keyCode) {
|
||||||
|
KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT -> {
|
||||||
|
// Let super handle modifier tracking
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return super.dispatchKeyEvent(event)
|
return super.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pasteFromClipboard() {
|
||||||
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? android.content.ClipboardManager
|
||||||
|
val clip = clipboard?.primaryClip
|
||||||
|
if (clip != null && clip.itemCount > 0) {
|
||||||
|
val text = clip.getItemAt(0)?.coerceToText(this)?.toString()
|
||||||
|
if (!text.isNullOrEmpty()) {
|
||||||
|
viewModel.pasteString(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -32,12 +32,17 @@ sealed interface TerminalInputAction {
|
|||||||
data class TypeText(val text: String) : TerminalInputAction
|
data class TypeText(val text: String) : TerminalInputAction
|
||||||
data class SendAid(val aidCode: Int) : TerminalInputAction
|
data class SendAid(val aidCode: Int) : TerminalInputAction
|
||||||
data object Backspace : TerminalInputAction
|
data object Backspace : TerminalInputAction
|
||||||
|
data object DeleteChar : TerminalInputAction
|
||||||
data object Tab : TerminalInputAction
|
data object Tab : TerminalInputAction
|
||||||
data object BackTab : TerminalInputAction
|
data object BackTab : TerminalInputAction
|
||||||
data object CursorLeft : TerminalInputAction
|
data object CursorLeft : TerminalInputAction
|
||||||
data object CursorRight : TerminalInputAction
|
data object CursorRight : TerminalInputAction
|
||||||
data object CursorUp : TerminalInputAction
|
data object CursorUp : TerminalInputAction
|
||||||
data object CursorDown : TerminalInputAction
|
data object CursorDown : TerminalInputAction
|
||||||
|
data object CursorHome : TerminalInputAction
|
||||||
|
data object EraseEof : TerminalInputAction
|
||||||
|
data object EraseInput : TerminalInputAction
|
||||||
|
data object Newline : TerminalInputAction
|
||||||
data object Reset : TerminalInputAction
|
data object Reset : TerminalInputAction
|
||||||
data class SetCursor(val baddr: Int) : TerminalInputAction
|
data class SetCursor(val baddr: Int) : TerminalInputAction
|
||||||
}
|
}
|
||||||
@@ -124,6 +129,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private val _ftState = MutableStateFlow(FTProgressState())
|
private val _ftState = MutableStateFlow(FTProgressState())
|
||||||
val ftState: StateFlow<FTProgressState> = _ftState.asStateFlow()
|
val ftState: StateFlow<FTProgressState> = _ftState.asStateFlow()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
private var client: Telnet3270Client? = null
|
private var client: Telnet3270Client? = null
|
||||||
var currentHost: String = ""
|
var currentHost: String = ""
|
||||||
private set
|
private set
|
||||||
@@ -162,6 +168,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
try {
|
try {
|
||||||
when (action) {
|
when (action) {
|
||||||
is TerminalInputAction.TypeText -> {
|
is TerminalInputAction.TypeText -> {
|
||||||
|
log.info("TypeText: text='${action.text}', curAddr=${buf.cursorAddress}, formatted=${buf.isFormatted}")
|
||||||
ip.isKeyboardLocked = false
|
ip.isKeyboardLocked = false
|
||||||
for (ch in action.text) {
|
for (ch in action.text) {
|
||||||
if (ch == '\n' || ch == '\r') {
|
if (ch == '\n' || ch == '\r') {
|
||||||
@@ -177,6 +184,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ip.typeCharacter(ch)
|
ip.typeCharacter(ch)
|
||||||
|
log.info("After typeCharacter('$ch'): newAddr=${buf.cursorAddress}, cellChar='${buf.getCell(curAddr).ucs4}'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,6 +196,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
ip.isKeyboardLocked = false
|
ip.isKeyboardLocked = false
|
||||||
ip.backspace()
|
ip.backspace()
|
||||||
}
|
}
|
||||||
|
is TerminalInputAction.DeleteChar -> {
|
||||||
|
ip.isKeyboardLocked = false
|
||||||
|
ip.deleteChar()
|
||||||
|
}
|
||||||
is TerminalInputAction.Tab -> {
|
is TerminalInputAction.Tab -> {
|
||||||
ip.tab()
|
ip.tab()
|
||||||
}
|
}
|
||||||
@@ -206,6 +218,20 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
is TerminalInputAction.CursorDown -> {
|
is TerminalInputAction.CursorDown -> {
|
||||||
ip.cursorDown()
|
ip.cursorDown()
|
||||||
}
|
}
|
||||||
|
is TerminalInputAction.CursorHome -> {
|
||||||
|
ip.cursorHome()
|
||||||
|
}
|
||||||
|
is TerminalInputAction.EraseEof -> {
|
||||||
|
ip.isKeyboardLocked = false
|
||||||
|
ip.eraseEof()
|
||||||
|
}
|
||||||
|
is TerminalInputAction.EraseInput -> {
|
||||||
|
ip.isKeyboardLocked = false
|
||||||
|
ip.eraseInput()
|
||||||
|
}
|
||||||
|
is TerminalInputAction.Newline -> {
|
||||||
|
ip.newline()
|
||||||
|
}
|
||||||
is TerminalInputAction.Reset -> {
|
is TerminalInputAction.Reset -> {
|
||||||
ip.reset()
|
ip.reset()
|
||||||
}
|
}
|
||||||
@@ -545,6 +571,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
inputChannel.trySend(TerminalInputAction.Backspace)
|
inputChannel.trySend(TerminalInputAction.Backspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteChar() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.DeleteChar)
|
||||||
|
}
|
||||||
|
|
||||||
fun tab() {
|
fun tab() {
|
||||||
inputChannel.trySend(TerminalInputAction.Tab)
|
inputChannel.trySend(TerminalInputAction.Tab)
|
||||||
}
|
}
|
||||||
@@ -557,6 +587,18 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
inputChannel.trySend(TerminalInputAction.Reset)
|
inputChannel.trySend(TerminalInputAction.Reset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun eraseEof() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.EraseEof)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun eraseInput() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.EraseInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newline() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.Newline)
|
||||||
|
}
|
||||||
|
|
||||||
fun sendAid(aidCode: Int) {
|
fun sendAid(aidCode: Int) {
|
||||||
inputChannel.trySend(TerminalInputAction.SendAid(aidCode))
|
inputChannel.trySend(TerminalInputAction.SendAid(aidCode))
|
||||||
}
|
}
|
||||||
@@ -581,6 +623,10 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
inputChannel.trySend(TerminalInputAction.CursorRight)
|
inputChannel.trySend(TerminalInputAction.CursorRight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cursorHome() {
|
||||||
|
inputChannel.trySend(TerminalInputAction.CursorHome)
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractScreenSnippet(buf: ScreenBuffer?): String {
|
private fun extractScreenSnippet(buf: ScreenBuffer?): String {
|
||||||
if (buf == null) return "Mainframe screen update received"
|
if (buf == null) return "Mainframe screen update received"
|
||||||
val rows = buf.rows
|
val rows = buf.rows
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ fun TerminalView(
|
|||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val currentScreenVersion by rememberUpdatedState(screenVersion)
|
||||||
|
val currentCursorAddr by rememberUpdatedState(cursorAddr)
|
||||||
|
|
||||||
var selectionStart by remember { mutableStateOf<Offset?>(null) }
|
var selectionStart by remember { mutableStateOf<Offset?>(null) }
|
||||||
var selectionEnd by remember { mutableStateOf<Offset?>(null) }
|
var selectionEnd by remember { mutableStateOf<Offset?>(null) }
|
||||||
var showContextMenu by remember { mutableStateOf(false) }
|
var showContextMenu by remember { mutableStateOf(false) }
|
||||||
@@ -160,6 +163,10 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
val version = currentScreenVersion
|
||||||
|
val activeCursorAddr = currentCursorAddr
|
||||||
|
if (version == -1L) return@Canvas
|
||||||
|
|
||||||
val width = size.width
|
val width = size.width
|
||||||
val height = size.height
|
val height = size.height
|
||||||
val cellWidth = width / cols
|
val cellWidth = width / cols
|
||||||
@@ -290,7 +297,7 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cursor indicator (respects blinking toggle & timer)
|
// Cursor indicator (respects blinking toggle & timer)
|
||||||
if (addr == cursorAddr && !isSelected && cursorVisible) {
|
if (addr == activeCursorAddr && !isSelected && cursorVisible) {
|
||||||
drawRect(
|
drawRect(
|
||||||
color = HOST_COLORS[HOST_COLOR_TURQUOISE].copy(alpha = 0.5f),
|
color = HOST_COLORS[HOST_COLOR_TURQUOISE].copy(alpha = 0.5f),
|
||||||
topLeft = Offset(left, top),
|
topLeft = Offset(left, top),
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package org.pubvm.a3270
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.lib3270j.TerminalModel
|
||||||
|
import org.lib3270j.charset.EbcdicTranslator
|
||||||
|
import org.lib3270j.input.InputProcessor
|
||||||
|
import org.lib3270j.protocol.DS3270Constants.*
|
||||||
|
import org.lib3270j.screen.ScreenBuffer
|
||||||
|
|
||||||
|
class HardwareKeyboardInputTest {
|
||||||
|
|
||||||
|
private lateinit var screenBuffer: ScreenBuffer
|
||||||
|
private lateinit var translator: EbcdicTranslator
|
||||||
|
private lateinit var inputProcessor: InputProcessor
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
val model = TerminalModel.IBM_3279_2 // 24x80
|
||||||
|
translator = EbcdicTranslator()
|
||||||
|
screenBuffer = ScreenBuffer(model, translator)
|
||||||
|
inputProcessor = InputProcessor(screenBuffer, translator, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnformattedTypingAndNavigation() {
|
||||||
|
assertEquals(0, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
// Type alphanumeric string
|
||||||
|
val text = "LOGON APPLID(TSO)"
|
||||||
|
for (ch in text) {
|
||||||
|
inputProcessor.typeCharacter(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(text.length, screenBuffer.cursorAddress)
|
||||||
|
for (i in text.indices) {
|
||||||
|
assertEquals(text[i], screenBuffer.getCell(i).ucs4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
inputProcessor.cursorLeft()
|
||||||
|
assertEquals(text.length - 1, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
inputProcessor.cursorRight()
|
||||||
|
assertEquals(text.length, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
inputProcessor.cursorDown()
|
||||||
|
assertEquals(80 + text.length, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
inputProcessor.cursorUp()
|
||||||
|
assertEquals(text.length, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
inputProcessor.cursorHome()
|
||||||
|
assertEquals(0, screenBuffer.cursorAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBackspaceAndForwardDelete() {
|
||||||
|
val text = "HELLO"
|
||||||
|
for (ch in text) {
|
||||||
|
inputProcessor.typeCharacter(ch)
|
||||||
|
}
|
||||||
|
assertEquals(5, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
// Backspace moves left and deletes char
|
||||||
|
inputProcessor.backspace()
|
||||||
|
assertEquals(4, screenBuffer.cursorAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormattedFieldInputAndTab() {
|
||||||
|
// Create formatted screen with protected label and unprotected input field
|
||||||
|
// Position 0: Protected FA
|
||||||
|
screenBuffer.setCellFA(0, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||||
|
// Set label "USERID:" at positions 1..7
|
||||||
|
val label = "USERID:"
|
||||||
|
for (i in label.indices) {
|
||||||
|
screenBuffer.getCell(i + 1).ucs4 = label[i]
|
||||||
|
screenBuffer.getCell(i + 1).ec = translator.unicodeToEbcdic(label[i]).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position 10: Unprotected FA
|
||||||
|
screenBuffer.setCellFA(10, FA_PRINTABLE.toByte())
|
||||||
|
|
||||||
|
// Position 20: Protected FA
|
||||||
|
screenBuffer.setCellFA(20, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||||
|
|
||||||
|
// Position 30: Unprotected FA
|
||||||
|
screenBuffer.setCellFA(30, FA_PRINTABLE.toByte())
|
||||||
|
|
||||||
|
screenBuffer.cursorAddress = 11
|
||||||
|
|
||||||
|
// Type into first field
|
||||||
|
val userid = "IBMUSER"
|
||||||
|
for (ch in userid) {
|
||||||
|
inputProcessor.typeCharacter(ch)
|
||||||
|
}
|
||||||
|
assertEquals(18, screenBuffer.cursorAddress)
|
||||||
|
for (i in userid.indices) {
|
||||||
|
assertEquals(userid[i], screenBuffer.getCell(11 + i).ucs4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab should jump to position 31 (next unprotected field)
|
||||||
|
inputProcessor.tab()
|
||||||
|
assertEquals(31, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
// BackTab should jump back to position 11
|
||||||
|
inputProcessor.backTab()
|
||||||
|
assertEquals(11, screenBuffer.cursorAddress)
|
||||||
|
|
||||||
|
// Home should jump to first unprotected field (position 11)
|
||||||
|
screenBuffer.cursorAddress = 50
|
||||||
|
inputProcessor.cursorHome()
|
||||||
|
assertEquals(11, screenBuffer.cursorAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFormattedDeleteCharAndEraseEof() {
|
||||||
|
// Position 0: Unprotected FA
|
||||||
|
screenBuffer.setCellFA(0, FA_PRINTABLE.toByte())
|
||||||
|
// Position 10: Protected FA
|
||||||
|
screenBuffer.setCellFA(10, (FA_PRINTABLE or FA_PROTECT).toByte())
|
||||||
|
|
||||||
|
screenBuffer.cursorAddress = 1
|
||||||
|
val text = "ABCDE"
|
||||||
|
for (ch in text) {
|
||||||
|
inputProcessor.typeCharacter(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move cursor to 'C' at pos 3
|
||||||
|
screenBuffer.cursorAddress = 3
|
||||||
|
inputProcessor.deleteChar()
|
||||||
|
|
||||||
|
// "ABDE "
|
||||||
|
assertEquals('A', screenBuffer.getCell(1).ucs4)
|
||||||
|
assertEquals('B', screenBuffer.getCell(2).ucs4)
|
||||||
|
assertEquals('D', screenBuffer.getCell(3).ucs4)
|
||||||
|
assertEquals('E', screenBuffer.getCell(4).ucs4)
|
||||||
|
assertEquals(0, screenBuffer.getCell(5).ec.toInt())
|
||||||
|
|
||||||
|
// Erase EOF from pos 3
|
||||||
|
inputProcessor.eraseEof()
|
||||||
|
assertEquals('A', screenBuffer.getCell(1).ucs4)
|
||||||
|
assertEquals('B', screenBuffer.getCell(2).ucs4)
|
||||||
|
assertEquals(0, screenBuffer.getCell(3).ec.toInt())
|
||||||
|
assertEquals(0, screenBuffer.getCell(4).ec.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPfKeyAids() {
|
||||||
|
val pfAids = intArrayOf(
|
||||||
|
AID_PF1, AID_PF2, AID_PF3, AID_PF4, AID_PF5, AID_PF6,
|
||||||
|
AID_PF7, AID_PF8, AID_PF9, AID_PF10, AID_PF11, AID_PF12,
|
||||||
|
AID_PF13, AID_PF14, AID_PF15, AID_PF16, AID_PF17, AID_PF18,
|
||||||
|
AID_PF19, AID_PF20, AID_PF21, AID_PF22, AID_PF23, AID_PF24
|
||||||
|
)
|
||||||
|
|
||||||
|
for (i in 1..24) {
|
||||||
|
val aid = pfAids[i - 1]
|
||||||
|
inputProcessor.sendAid(aid)
|
||||||
|
assertEquals(aid, inputProcessor.lastAid)
|
||||||
|
assertTrue(inputProcessor.isKeyboardLocked)
|
||||||
|
inputProcessor.reset()
|
||||||
|
assertFalse(inputProcessor.isKeyboardLocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user