Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
2a22a636b6
|
|||
|
d3c149fe4a
|
|||
|
8c8e1f17db
|
@@ -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
|
||||||
@@ -218,6 +432,7 @@ fun MainScreen(
|
|||||||
autoHost.hostType,
|
autoHost.hostType,
|
||||||
autoHost.useTls,
|
autoHost.useTls,
|
||||||
autoHost.tlsVerifyCert,
|
autoHost.tlsVerifyCert,
|
||||||
|
autoHost.tn3270e,
|
||||||
autoHost.graphicsMode
|
autoHost.graphicsMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -356,9 +571,9 @@ fun MainScreen(
|
|||||||
if (showConnectDialog) {
|
if (showConnectDialog) {
|
||||||
ConnectDialog(
|
ConnectDialog(
|
||||||
onDismiss = { showConnectDialog = false },
|
onDismiss = { showConnectDialog = false },
|
||||||
onConnect = { host, port, model, luName, hostType, useTls, tlsVerifyCert, graphicsMode ->
|
onConnect = { host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode ->
|
||||||
showConnectDialog = false
|
showConnectDialog = false
|
||||||
viewModel.connect(host, port, model, luName, hostType, useTls, tlsVerifyCert, graphicsMode)
|
viewModel.connect(host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode)
|
||||||
terminalInputViewRef?.showSoftKeyboard()
|
terminalInputViewRef?.showSoftKeyboard()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -253,6 +279,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
hostType: String = "TSO",
|
hostType: String = "TSO",
|
||||||
useTls: Boolean = false,
|
useTls: Boolean = false,
|
||||||
tlsVerifyCert: Boolean = true,
|
tlsVerifyCert: Boolean = true,
|
||||||
|
tn3270e: Boolean = true,
|
||||||
graphicsModeStr: String = "BOTH"
|
graphicsModeStr: String = "BOTH"
|
||||||
) {
|
) {
|
||||||
currentHost = host
|
currentHost = host
|
||||||
@@ -284,6 +311,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
val effectiveHost = parsedConfig.host
|
val effectiveHost = parsedConfig.host
|
||||||
val effectivePort = parsedConfig.port
|
val effectivePort = parsedConfig.port
|
||||||
val effectiveTls = useTls || parsedConfig.isUseTls
|
val effectiveTls = useTls || parsedConfig.isUseTls
|
||||||
|
val effectiveTn3270e = tn3270e && parsedConfig.isTn3270eEnabled
|
||||||
val globalVerify = AppSettings.isVerifyCertsEnabled(getApplication())
|
val globalVerify = AppSettings.isVerifyCertsEnabled(getApplication())
|
||||||
val effectiveVerify = tlsVerifyCert && globalVerify
|
val effectiveVerify = tlsVerifyCert && globalVerify
|
||||||
val gMode = org.lib3270j.graphics.GraphicsMode.fromString(graphicsModeStr)
|
val gMode = org.lib3270j.graphics.GraphicsMode.fromString(graphicsModeStr)
|
||||||
@@ -298,6 +326,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
isUseTls = effectiveTls
|
isUseTls = effectiveTls
|
||||||
isTlsVerifyCert = effectiveVerify
|
isTlsVerifyCert = effectiveVerify
|
||||||
|
isTn3270eEnabled = effectiveTn3270e
|
||||||
graphicsMode = gMode
|
graphicsMode = gMode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,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)
|
||||||
}
|
}
|
||||||
@@ -554,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))
|
||||||
}
|
}
|
||||||
@@ -578,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
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ data class SavedHost(
|
|||||||
val hostType: String = "TSO",
|
val hostType: String = "TSO",
|
||||||
val useTls: Boolean = false,
|
val useTls: Boolean = false,
|
||||||
val tlsVerifyCert: Boolean = true,
|
val tlsVerifyCert: Boolean = true,
|
||||||
|
val tn3270e: Boolean = true,
|
||||||
val graphicsMode: String = "BOTH"
|
val graphicsMode: String = "BOTH"
|
||||||
) {
|
) {
|
||||||
fun toJson(): JSONObject {
|
fun toJson(): JSONObject {
|
||||||
@@ -31,6 +32,7 @@ data class SavedHost(
|
|||||||
put("hostType", hostType)
|
put("hostType", hostType)
|
||||||
put("useTls", useTls)
|
put("useTls", useTls)
|
||||||
put("tlsVerifyCert", tlsVerifyCert)
|
put("tlsVerifyCert", tlsVerifyCert)
|
||||||
|
put("tn3270e", tn3270e)
|
||||||
put("graphicsMode", graphicsMode)
|
put("graphicsMode", graphicsMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,6 +50,7 @@ data class SavedHost(
|
|||||||
hostType = json.optString("hostType", "TSO"),
|
hostType = json.optString("hostType", "TSO"),
|
||||||
useTls = json.optBoolean("useTls", false),
|
useTls = json.optBoolean("useTls", false),
|
||||||
tlsVerifyCert = json.optBoolean("tlsVerifyCert", true),
|
tlsVerifyCert = json.optBoolean("tlsVerifyCert", true),
|
||||||
|
tn3270e = json.optBoolean("tn3270e", true),
|
||||||
graphicsMode = json.optString("graphicsMode", "BOTH")
|
graphicsMode = json.optString("graphicsMode", "BOTH")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.pubvm.a3270.ui
|
package org.pubvm.a3270.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -10,22 +11,34 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import org.pubvm.a3270.storage.HostStorage
|
import org.pubvm.a3270.storage.HostStorage
|
||||||
import org.pubvm.a3270.storage.SavedHost
|
import org.pubvm.a3270.storage.SavedHost
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectDialog(
|
fun ConnectDialog(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onConnect: (host: String, port: Int, model: Int, luName: String, hostType: String, useTls: Boolean, tlsVerifyCert: Boolean, graphicsMode: String) -> Unit
|
onConnect: (
|
||||||
|
host: String,
|
||||||
|
port: Int,
|
||||||
|
model: Int,
|
||||||
|
luName: String,
|
||||||
|
hostType: String,
|
||||||
|
useTls: Boolean,
|
||||||
|
tlsVerifyCert: Boolean,
|
||||||
|
tn3270e: Boolean,
|
||||||
|
graphicsMode: String
|
||||||
|
) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var savedHosts by remember { mutableStateOf(HostStorage.getSavedHosts(context)) }
|
var savedHosts by remember { mutableStateOf(HostStorage.getSavedHosts(context)) }
|
||||||
@@ -40,6 +53,7 @@ fun ConnectDialog(
|
|||||||
var hostType by remember { mutableStateOf("TSO") }
|
var hostType by remember { mutableStateOf("TSO") }
|
||||||
var useTls by remember { mutableStateOf(false) }
|
var useTls by remember { mutableStateOf(false) }
|
||||||
var tlsVerifyCert by remember { mutableStateOf(true) }
|
var tlsVerifyCert by remember { mutableStateOf(true) }
|
||||||
|
var tn3270e by remember { mutableStateOf(true) }
|
||||||
var graphicsMode by remember { mutableStateOf("BOTH") }
|
var graphicsMode by remember { mutableStateOf("BOTH") }
|
||||||
|
|
||||||
fun loadProfile(saved: SavedHost) {
|
fun loadProfile(saved: SavedHost) {
|
||||||
@@ -53,6 +67,7 @@ fun ConnectDialog(
|
|||||||
hostType = saved.hostType
|
hostType = saved.hostType
|
||||||
useTls = saved.useTls
|
useTls = saved.useTls
|
||||||
tlsVerifyCert = saved.tlsVerifyCert
|
tlsVerifyCert = saved.tlsVerifyCert
|
||||||
|
tn3270e = saved.tn3270e
|
||||||
graphicsMode = saved.graphicsMode
|
graphicsMode = saved.graphicsMode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +82,7 @@ fun ConnectDialog(
|
|||||||
hostType = "TSO"
|
hostType = "TSO"
|
||||||
useTls = false
|
useTls = false
|
||||||
tlsVerifyCert = true
|
tlsVerifyCert = true
|
||||||
|
tn3270e = true
|
||||||
graphicsMode = "BOTH"
|
graphicsMode = "BOTH"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +101,7 @@ fun ConnectDialog(
|
|||||||
hostType = hostType,
|
hostType = hostType,
|
||||||
useTls = useTls,
|
useTls = useTls,
|
||||||
tlsVerifyCert = tlsVerifyCert,
|
tlsVerifyCert = tlsVerifyCert,
|
||||||
|
tn3270e = tn3270e,
|
||||||
graphicsMode = graphicsMode
|
graphicsMode = graphicsMode
|
||||||
)
|
)
|
||||||
HostStorage.saveHost(context, hostToSave)
|
HostStorage.saveHost(context, hostToSave)
|
||||||
@@ -102,20 +119,24 @@ fun ConnectDialog(
|
|||||||
keyboardController?.show()
|
keyboardController?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = Color(0xFF1E1E1E),
|
color = Color(0xFF1E1E1E),
|
||||||
tonalElevation = 6.dp,
|
tonalElevation = 6.dp,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(0.96f)
|
.fillMaxWidth(0.95f)
|
||||||
.wrapContentHeight()
|
.heightIn(max = 680.dp)
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.padding(14.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
// Title Row
|
// Title Row
|
||||||
Row(
|
Row(
|
||||||
@@ -123,12 +144,17 @@ fun ConnectDialog(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text("Connect / Manage Hosts", fontWeight = FontWeight.Bold, fontSize = 15.sp, color = Color.White)
|
Text(
|
||||||
|
"Connect / Manage Hosts",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { clearFields() },
|
onClick = { clearFields() },
|
||||||
contentPadding = PaddingValues(horizontal = 6.dp, vertical = 2.dp)
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp)
|
||||||
) {
|
) {
|
||||||
Text("+ New Profile", fontSize = 12.sp)
|
Text("+ New Profile", fontSize = 12.sp, color = Color(0xFF339AF0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,18 +162,23 @@ fun ConnectDialog(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(max = 460.dp)
|
.weight(1f, fill = false)
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.verticalScroll(scrollState),
|
.verticalScroll(scrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
// Saved Profiles List Section
|
// Saved Profiles List Section
|
||||||
if (savedHosts.isNotEmpty()) {
|
if (savedHosts.isNotEmpty()) {
|
||||||
Text("Saved Host Profiles:", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.Gray)
|
Text(
|
||||||
|
"Saved Host Profiles:",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
@@ -155,12 +186,13 @@ fun ConnectDialog(
|
|||||||
savedHosts.forEach { saved ->
|
savedHosts.forEach { saved ->
|
||||||
val isSelected = (saved.id == selectedHostId)
|
val isSelected = (saved.id == selectedHostId)
|
||||||
val tlsTag = if (saved.useTls) {
|
val tlsTag = if (saved.useTls) {
|
||||||
if (saved.tlsVerifyCert) " [🔒 TLS]" else " [🔓 TLS/Unverified]"
|
if (saved.tlsVerifyCert) " [🔒 TLS]" else " [🔓 TLS/NoVerify]"
|
||||||
} else ""
|
} else ""
|
||||||
|
val eTag = if (!saved.tn3270e) " [Non-E]" else ""
|
||||||
val gfxTag = if (saved.graphicsMode != "NONE") " [GFX: ${saved.graphicsMode}]" else ""
|
val gfxTag = if (saved.graphicsMode != "NONE") " [GFX: ${saved.graphicsMode}]" else ""
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(6.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
color = if (isSelected) Color(0xFF2C3E50) else Color(0xFF25262B),
|
color = if (isSelected) Color(0xFF2C3E50) else Color(0xFF25262B),
|
||||||
border = if (isSelected) androidx.compose.foundation.BorderStroke(1.dp, Color(0xFF339AF0)) else null,
|
border = if (isSelected) androidx.compose.foundation.BorderStroke(1.dp, Color(0xFF339AF0)) else null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -170,7 +202,7 @@ fun ConnectDialog(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -178,11 +210,11 @@ fun ConnectDialog(
|
|||||||
Text(
|
Text(
|
||||||
text = saved.name,
|
text = saved.name,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
fontSize = 12.sp,
|
fontSize = 13.sp,
|
||||||
color = Color.White
|
color = Color.White
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "${saved.host}:${saved.port} (M${saved.model} - ${saved.hostType})$tlsTag$gfxTag" +
|
text = "${saved.host}:${saved.port} (M${saved.model} - ${saved.hostType})$tlsTag$eTag$gfxTag" +
|
||||||
if (saved.autoConnect) " [Auto]" else "",
|
if (saved.autoConnect) " [Auto]" else "",
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
color = Color.LightGray
|
color = Color.LightGray
|
||||||
@@ -201,12 +233,13 @@ fun ConnectDialog(
|
|||||||
hostToConn.hostType,
|
hostToConn.hostType,
|
||||||
hostToConn.useTls,
|
hostToConn.useTls,
|
||||||
hostToConn.tlsVerifyCert,
|
hostToConn.tlsVerifyCert,
|
||||||
|
hostToConn.tn3270e,
|
||||||
hostToConn.graphicsMode
|
hostToConn.graphicsMode
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(28.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
) {
|
) {
|
||||||
Text("▶", fontSize = 13.sp, color = Color(0xFF51CF66))
|
Text("▶", fontSize = 14.sp, color = Color(0xFF51CF66))
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -216,20 +249,20 @@ fun ConnectDialog(
|
|||||||
clearFields()
|
clearFields()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.size(28.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
) {
|
) {
|
||||||
Text("✕", fontSize = 12.sp, color = Color(0xFFFF6B6B))
|
Text("✕", fontSize = 13.sp, color = Color(0xFFFF6B6B))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(color = Color(0xFF373A40), modifier = Modifier.padding(vertical = 2.dp))
|
HorizontalDivider(color = Color(0xFF373A40), modifier = Modifier.padding(vertical = 4.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = if (selectedHostId != null) "Editing Profile:" else "New Profile Details:",
|
text = if (selectedHostId != null) "Editing Profile:" else "Host Connection Details:",
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.Gray
|
color = Color.Gray
|
||||||
@@ -243,142 +276,236 @@ fun ConnectDialog(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = host,
|
|
||||||
onValueChange = { host = it },
|
|
||||||
label = { Text("Host / IP Address") },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
)
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = portStr,
|
|
||||||
onValueChange = { portStr = it },
|
|
||||||
label = { Text("Port (default 23 or 992)") },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Terminal Model
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Text("Model:", fontSize = 11.sp, color = Color.Gray)
|
OutlinedTextField(
|
||||||
listOf(2, 3, 4, 5).forEach { m ->
|
value = host,
|
||||||
FilterChip(
|
onValueChange = { host = it },
|
||||||
selected = (modelNum == m),
|
label = { Text("Host / IP Address") },
|
||||||
onClick = { modelNum = m },
|
singleLine = true,
|
||||||
label = { Text("M$m", fontSize = 11.sp) }
|
modifier = Modifier
|
||||||
)
|
.weight(0.68f)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = portStr,
|
||||||
|
onValueChange = { portStr = it },
|
||||||
|
label = { Text("Port") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(0.32f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminal Model Selector
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Terminal Model:", fontSize = 11.sp, color = Color.Gray)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
listOf(
|
||||||
|
2 to "M2",
|
||||||
|
3 to "M3",
|
||||||
|
4 to "M4",
|
||||||
|
5 to "M5"
|
||||||
|
).forEach { (m, label) ->
|
||||||
|
val isSelected = (modelNum == m)
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
color = if (isSelected) Color(0xFF1C7ED6) else Color(0xFF2C2D30),
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(34.dp)
|
||||||
|
.clickable { modelNum = m }
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(horizontal = 2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host System Type (TSO, CMS, CICS)
|
// Host System Type (TSO, CMS, CICS)
|
||||||
Row(
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
Text("Host System Type:", fontSize = 11.sp, color = Color.Gray)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
) {
|
|
||||||
Text("Host Type:", fontSize = 11.sp, color = Color.Gray)
|
|
||||||
listOf("TSO", "CMS", "CICS").forEach { ht ->
|
|
||||||
FilterChip(
|
|
||||||
selected = (hostType == ht),
|
|
||||||
onClick = { hostType = ht },
|
|
||||||
label = { Text(ht, fontSize = 11.sp) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Graphics Mode
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text("Graphics:", fontSize = 11.sp, color = Color.Gray)
|
|
||||||
listOf(
|
|
||||||
"PROGRAMMED_SYMBOLS" to "Symbols",
|
|
||||||
"VECTOR_GRAPHICS" to "Vector",
|
|
||||||
"BOTH" to "Both",
|
|
||||||
"NONE" to "None"
|
|
||||||
).forEach { (modeKey, modeLabel) ->
|
|
||||||
FilterChip(
|
|
||||||
selected = (graphicsMode.equals(modeKey, ignoreCase = true)),
|
|
||||||
onClick = { graphicsMode = modeKey },
|
|
||||||
label = { Text(modeLabel, fontSize = 11.sp) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLS / SSL Checkbox
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
val newTls = !useTls
|
|
||||||
useTls = newTls
|
|
||||||
if (newTls && portStr.trim() == "23") {
|
|
||||||
portStr = "992"
|
|
||||||
} else if (!newTls && portStr.trim() == "992") {
|
|
||||||
portStr = "23"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Checkbox(
|
|
||||||
checked = useTls,
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
useTls = checked
|
|
||||||
if (checked && portStr.trim() == "23") {
|
|
||||||
portStr = "992"
|
|
||||||
} else if (!checked && portStr.trim() == "992") {
|
|
||||||
portStr = "23"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text("Enable TLS / SSL Connection", fontSize = 12.sp, color = Color.White)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify Certificate Checkbox
|
|
||||||
if (useTls) {
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp)
|
|
||||||
.clickable { tlsVerifyCert = !tlsVerifyCert }
|
|
||||||
) {
|
) {
|
||||||
Checkbox(
|
listOf("TSO", "CMS", "CICS").forEach { ht ->
|
||||||
checked = tlsVerifyCert,
|
val isSelected = (hostType == ht)
|
||||||
onCheckedChange = { tlsVerifyCert = it }
|
Surface(
|
||||||
)
|
shape = RoundedCornerShape(6.dp),
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
color = if (isSelected) Color(0xFF1C7ED6) else Color(0xFF2C2D30),
|
||||||
Text("Verify Server Certificate", fontSize = 12.sp, color = if (tlsVerifyCert) Color.LightGray else Color(0xFFFFB450))
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(34.dp)
|
||||||
|
.clickable { hostType = ht }
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = ht,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graphics Mode (Both, Vector, Symbols, Off)
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Graphics Mode:", fontSize = 11.sp, color = Color.Gray)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
listOf(
|
||||||
|
"BOTH" to "Both",
|
||||||
|
"VECTOR_GRAPHICS" to "Vector",
|
||||||
|
"PROGRAMMED_SYMBOLS" to "Symbols",
|
||||||
|
"NONE" to "Off"
|
||||||
|
).forEach { (modeKey, modeLabel) ->
|
||||||
|
val isSelected = graphicsMode.equals(modeKey, ignoreCase = true)
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(6.dp),
|
||||||
|
color = if (isSelected) Color(0xFF2B8A3E) else Color(0xFF2C2D30),
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(34.dp)
|
||||||
|
.clickable { graphicsMode = modeKey }
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(horizontal = 2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = modeLabel,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = luName,
|
value = luName,
|
||||||
onValueChange = { luName = it },
|
onValueChange = { luName = it },
|
||||||
label = { Text("LU Name (Optional)") },
|
label = { Text("LU Name / Device Pool (Optional)") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
// Protocol & Security Options
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
.clickable { autoConnect = !autoConnect }
|
|
||||||
) {
|
) {
|
||||||
Checkbox(
|
// TLS / SSL Checkbox
|
||||||
checked = autoConnect,
|
Row(
|
||||||
onCheckedChange = { autoConnect = it }
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
)
|
modifier = Modifier
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
.fillMaxWidth()
|
||||||
Text("Auto-connect on app startup", fontSize = 12.sp)
|
.clickable {
|
||||||
|
val newTls = !useTls
|
||||||
|
useTls = newTls
|
||||||
|
if (newTls && portStr.trim() == "23") {
|
||||||
|
portStr = "992"
|
||||||
|
} else if (!newTls && portStr.trim() == "992") {
|
||||||
|
portStr = "23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = useTls,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
useTls = checked
|
||||||
|
if (checked && portStr.trim() == "23") {
|
||||||
|
portStr = "992"
|
||||||
|
} else if (!checked && portStr.trim() == "992") {
|
||||||
|
portStr = "23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Enable TLS / SSL Connection", fontSize = 12.sp, color = Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Certificate Checkbox (only when TLS is active)
|
||||||
|
if (useTls) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 24.dp)
|
||||||
|
.clickable { tlsVerifyCert = !tlsVerifyCert }
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = tlsVerifyCert,
|
||||||
|
onCheckedChange = { tlsVerifyCert = it }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
"Verify Server Certificate",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = if (tlsVerifyCert) Color.LightGray else Color(0xFFFFB450)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TN3270E Checkbox
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { tn3270e = !tn3270e }
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = tn3270e,
|
||||||
|
onCheckedChange = { tn3270e = it }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Enable TN3270E (Extended 3270)", fontSize = 12.sp, color = Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-connect Checkbox
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { autoConnect = !autoConnect }
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = autoConnect,
|
||||||
|
onCheckedChange = { autoConnect = it }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Auto-connect on app startup", fontSize = 12.sp, color = Color.White)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,13 +519,13 @@ fun ConnectDialog(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text("Cancel")
|
Text("Cancel", color = Color.LightGray)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
OutlinedButton(onClick = { saveCurrentProfile() }) {
|
OutlinedButton(onClick = { saveCurrentProfile() }) {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
val saved = saveCurrentProfile()
|
val saved = saveCurrentProfile()
|
||||||
@@ -411,12 +538,14 @@ fun ConnectDialog(
|
|||||||
saved.hostType,
|
saved.hostType,
|
||||||
saved.useTls,
|
saved.useTls,
|
||||||
saved.tlsVerifyCert,
|
saved.tlsVerifyCert,
|
||||||
|
saved.tn3270e,
|
||||||
saved.graphicsMode
|
saved.graphicsMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||||
) {
|
) {
|
||||||
Text("Connect")
|
Text("Connect", color = Color.White, fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.pubvm.a3270.ui
|
package org.pubvm.a3270.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -14,6 +15,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsDialog(
|
fun SettingsDialog(
|
||||||
initialMaskHiddenInput: Boolean,
|
initialMaskHiddenInput: Boolean,
|
||||||
@@ -30,13 +33,17 @@ fun SettingsDialog(
|
|||||||
var verifyCerts by remember { mutableStateOf(initialVerifyCerts) }
|
var verifyCerts by remember { mutableStateOf(initialVerifyCerts) }
|
||||||
var defaultGraphicsMode by remember { mutableStateOf(initialDefaultGraphicsMode) }
|
var defaultGraphicsMode by remember { mutableStateOf(initialDefaultGraphicsMode) }
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
Card(
|
Card(
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth(0.95f)
|
||||||
.padding(16.dp)
|
.heightIn(max = 680.dp)
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -200,20 +207,37 @@ fun SettingsDialog(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
listOf(
|
listOf(
|
||||||
"BOTH" to "Both",
|
"BOTH" to "Both",
|
||||||
"VECTOR_GRAPHICS" to "Vector",
|
"VECTOR_GRAPHICS" to "Vector",
|
||||||
"PROGRAMMED_SYMBOLS" to "Symbols",
|
"PROGRAMMED_SYMBOLS" to "Symbols",
|
||||||
"NONE" to "None"
|
"NONE" to "Off"
|
||||||
).forEach { (modeKey, modeLabel) ->
|
).forEach { (modeKey, modeLabel) ->
|
||||||
FilterChip(
|
val isSelected = defaultGraphicsMode.equals(modeKey, ignoreCase = true)
|
||||||
selected = defaultGraphicsMode.equals(modeKey, ignoreCase = true),
|
Surface(
|
||||||
onClick = { defaultGraphicsMode = modeKey },
|
shape = RoundedCornerShape(6.dp),
|
||||||
label = { Text(modeLabel, fontSize = 11.sp) }
|
color = if (isSelected) Color(0xFF2B8A3E) else Color(0xFF2C2D30),
|
||||||
)
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(34.dp)
|
||||||
|
.clickable { defaultGraphicsMode = modeKey }
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize().padding(horizontal = 2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = modeLabel,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
@@ -139,11 +142,10 @@ fun TerminalView(
|
|||||||
showContextMenu = false
|
showContextMenu = false
|
||||||
selectionStart = null
|
selectionStart = null
|
||||||
selectionEnd = null
|
selectionEnd = null
|
||||||
val cellWidth = size.width / cols
|
val metrics = calculateGridMetrics(size.width.toFloat(), size.height.toFloat(), cols, rows)
|
||||||
val cellHeight = size.height / rows
|
if (metrics.cellWidth > 0 && metrics.cellHeight > 0) {
|
||||||
if (cellWidth > 0 && cellHeight > 0) {
|
val col = ((startPos.x - metrics.offsetX) / metrics.cellWidth).toInt().coerceIn(0, cols - 1)
|
||||||
val col = (startPos.x / cellWidth).toInt().coerceIn(0, cols - 1)
|
val row = ((startPos.y - metrics.offsetY) / metrics.cellHeight).toInt().coerceIn(0, rows - 1)
|
||||||
val row = (startPos.y / cellHeight).toInt().coerceIn(0, rows - 1)
|
|
||||||
var addr = row * cols + col
|
var addr = row * cols + col
|
||||||
|
|
||||||
val buf = screenBuffer
|
val buf = screenBuffer
|
||||||
@@ -160,17 +162,30 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val width = size.width
|
val version = currentScreenVersion
|
||||||
val height = size.height
|
val activeCursorAddr = currentCursorAddr
|
||||||
val cellWidth = width / cols
|
if (version == -1L) return@Canvas
|
||||||
val cellHeight = height / rows
|
|
||||||
|
|
||||||
if (cellWidth <= 0 || cellHeight <= 0) return@Canvas
|
val metrics = calculateGridMetrics(size.width, size.height, cols, rows)
|
||||||
|
if (metrics.cellWidth <= 0f || metrics.cellHeight <= 0f) return@Canvas
|
||||||
|
|
||||||
|
val cellWidth = metrics.cellWidth
|
||||||
|
val cellHeight = metrics.cellHeight
|
||||||
|
val offsetX = metrics.offsetX
|
||||||
|
val offsetY = metrics.offsetY
|
||||||
|
|
||||||
val paint = Paint().apply {
|
val paint = Paint().apply {
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
typeface = Typeface.MONOSPACE
|
typeface = Typeface.MONOSPACE
|
||||||
textSize = cellHeight * 0.85f
|
textAlign = Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate optimal font size ensuring characters fit cleanly in cell without crowding
|
||||||
|
val baseTextSize = cellHeight * 0.78f
|
||||||
|
paint.textSize = baseTextSize
|
||||||
|
val measuredW = paint.measureText("W")
|
||||||
|
if (measuredW > cellWidth * 0.85f && measuredW > 0f) {
|
||||||
|
paint.textSize = baseTextSize * (cellWidth * 0.85f / measuredW)
|
||||||
}
|
}
|
||||||
|
|
||||||
val buf = screenBuffer
|
val buf = screenBuffer
|
||||||
@@ -187,11 +202,11 @@ fun TerminalView(
|
|||||||
|
|
||||||
val start = selectionStart
|
val start = selectionStart
|
||||||
val end = selectionEnd
|
val end = selectionEnd
|
||||||
if (start != null && end != null) {
|
if (start != null && end != null && cellWidth > 0f && cellHeight > 0f) {
|
||||||
val startCol = (start.x / cellWidth).toInt().coerceIn(0, cols - 1)
|
val startCol = ((start.x - offsetX) / cellWidth).toInt().coerceIn(0, cols - 1)
|
||||||
val startRow = (start.y / cellHeight).toInt().coerceIn(0, rows - 1)
|
val startRow = ((start.y - offsetY) / cellHeight).toInt().coerceIn(0, rows - 1)
|
||||||
val endCol = (end.x / cellWidth).toInt().coerceIn(0, cols - 1)
|
val endCol = ((end.x - offsetX) / cellWidth).toInt().coerceIn(0, cols - 1)
|
||||||
val endRow = (end.y / cellHeight).toInt().coerceIn(0, rows - 1)
|
val endRow = ((end.y - offsetY) / cellHeight).toInt().coerceIn(0, rows - 1)
|
||||||
|
|
||||||
selMinRow = minOf(startRow, endRow)
|
selMinRow = minOf(startRow, endRow)
|
||||||
selMaxRow = maxOf(startRow, endRow)
|
selMaxRow = maxOf(startRow, endRow)
|
||||||
@@ -204,8 +219,8 @@ fun TerminalView(
|
|||||||
val addr = r * cols + c
|
val addr = r * cols + c
|
||||||
if (addr >= totalCells) break
|
if (addr >= totalCells) break
|
||||||
|
|
||||||
val left = c * cellWidth
|
val left = offsetX + c * cellWidth
|
||||||
val top = r * cellHeight
|
val top = offsetY + r * cellHeight
|
||||||
|
|
||||||
var charVal = ' '
|
var charVal = ' '
|
||||||
var fgColor: Color
|
var fgColor: Color
|
||||||
@@ -279,7 +294,7 @@ fun TerminalView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight selected block range
|
// Draw selection box highlight if selected
|
||||||
val isSelected = r in selMinRow..selMaxRow && c in selMinCol..selMaxCol
|
val isSelected = r in selMinRow..selMaxRow && c in selMinCol..selMaxCol
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
drawRect(
|
drawRect(
|
||||||
@@ -290,7 +305,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),
|
||||||
@@ -307,7 +322,7 @@ fun TerminalView(
|
|||||||
val symHeight = slot.height
|
val symHeight = slot.height
|
||||||
val rgbArray = slot.getRgbPixels(fgColor.toArgb(), bgColor.toArgb())
|
val rgbArray = slot.getRgbPixels(fgColor.toArgb(), bgColor.toArgb())
|
||||||
if (rgbArray != null && symWidth > 0 && symHeight > 0) {
|
if (rgbArray != null && symWidth > 0 && symHeight > 0) {
|
||||||
val bmp = android.graphics.Bitmap.createBitmap(rgbArray, symWidth, symHeight, android.graphics.Bitmap.Config.ARGB_8888)
|
val bmp = android.graphics.Bitmap.createBitmap(rgbArray as IntArray, symWidth, symHeight, android.graphics.Bitmap.Config.ARGB_8888)
|
||||||
drawContext.canvas.nativeCanvas.drawBitmap(
|
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||||
bmp,
|
bmp,
|
||||||
null,
|
null,
|
||||||
@@ -323,11 +338,12 @@ fun TerminalView(
|
|||||||
paint.color = fgColor.toArgb()
|
paint.color = fgColor.toArgb()
|
||||||
paint.isFakeBoldText = isBold
|
paint.isFakeBoldText = isBold
|
||||||
val fontMetrics = paint.fontMetrics
|
val fontMetrics = paint.fontMetrics
|
||||||
val textY = top + (cellHeight - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top
|
val textX = left + cellWidth / 2f
|
||||||
|
val textY = top + (cellHeight - fontMetrics.bottom + fontMetrics.top) / 2f - fontMetrics.top
|
||||||
|
|
||||||
drawContext.canvas.nativeCanvas.drawText(
|
drawContext.canvas.nativeCanvas.drawText(
|
||||||
charVal.toString(),
|
charVal.toString(),
|
||||||
left + (cellWidth / 4),
|
textX,
|
||||||
textY,
|
textY,
|
||||||
paint
|
paint
|
||||||
)
|
)
|
||||||
@@ -336,8 +352,8 @@ fun TerminalView(
|
|||||||
if (isUnderline) {
|
if (isUnderline) {
|
||||||
drawLine(
|
drawLine(
|
||||||
color = fgColor,
|
color = fgColor,
|
||||||
start = Offset(left, top + cellHeight - 2),
|
start = Offset(left, top + cellHeight - 2f),
|
||||||
end = Offset(left + cellWidth, top + cellHeight - 2),
|
end = Offset(left + cellWidth, top + cellHeight - 2f),
|
||||||
strokeWidth = 2f
|
strokeWidth = 2f
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -346,8 +362,8 @@ fun TerminalView(
|
|||||||
|
|
||||||
// Draw Vector Graphics Plane overlay if present
|
// Draw Vector Graphics Plane overlay if present
|
||||||
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
if (graphicsPlane != null && graphicsPlane.hasContent()) {
|
||||||
val gridW = (cols * cellWidth).toInt()
|
val gridW = metrics.gridWidth.toInt()
|
||||||
val gridH = (rows * cellHeight).toInt()
|
val gridH = metrics.gridHeight.toInt()
|
||||||
if (gridW > 0 && gridH > 0) {
|
if (gridW > 0 && gridH > 0) {
|
||||||
graphicsPlane.resize(gridW, gridH)
|
graphicsPlane.resize(gridW, gridH)
|
||||||
val rgb = graphicsPlane.rgbBuffer
|
val rgb = graphicsPlane.rgbBuffer
|
||||||
@@ -356,7 +372,7 @@ fun TerminalView(
|
|||||||
drawContext.canvas.nativeCanvas.drawBitmap(
|
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||||
bmp,
|
bmp,
|
||||||
null,
|
null,
|
||||||
android.graphics.RectF(0f, 0f, gridW.toFloat(), gridH.toFloat()),
|
android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -418,6 +434,57 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class TerminalGridMetrics(
|
||||||
|
val cellWidth: Float,
|
||||||
|
val cellHeight: Float,
|
||||||
|
val gridWidth: Float,
|
||||||
|
val gridHeight: Float,
|
||||||
|
val offsetX: Float,
|
||||||
|
val offsetY: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
fun calculateGridMetrics(
|
||||||
|
viewWidth: Float,
|
||||||
|
viewHeight: Float,
|
||||||
|
cols: Int,
|
||||||
|
rows: Int
|
||||||
|
): TerminalGridMetrics {
|
||||||
|
if (viewWidth <= 0f || viewHeight <= 0f || cols <= 0 || rows <= 0) {
|
||||||
|
return TerminalGridMetrics(0f, 0f, 0f, 0f, 0f, 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
val rawCellW = viewWidth / cols
|
||||||
|
val rawCellH = viewHeight / rows
|
||||||
|
val currentRatio = rawCellW / rawCellH
|
||||||
|
|
||||||
|
// Target terminal cell aspect ratio is roughly 0.50 to 0.60 (width / height)
|
||||||
|
val targetMinRatio = 0.48f
|
||||||
|
val targetMaxRatio = 0.60f
|
||||||
|
|
||||||
|
val cellW: Float
|
||||||
|
val cellH: Float
|
||||||
|
|
||||||
|
if (currentRatio > targetMaxRatio) {
|
||||||
|
// Screen is wider than ideal aspect ratio -> clamp width, pillarbox (equal blank bars left/right)
|
||||||
|
cellH = rawCellH
|
||||||
|
cellW = rawCellH * targetMaxRatio
|
||||||
|
} else if (currentRatio < targetMinRatio) {
|
||||||
|
// Screen is taller than ideal aspect ratio -> clamp height, letterbox (equal blank bars top/bottom)
|
||||||
|
cellW = rawCellW
|
||||||
|
cellH = rawCellW / targetMinRatio
|
||||||
|
} else {
|
||||||
|
cellW = rawCellW
|
||||||
|
cellH = rawCellH
|
||||||
|
}
|
||||||
|
|
||||||
|
val gridW = cols * cellW
|
||||||
|
val gridH = rows * cellH
|
||||||
|
val offX = (viewWidth - gridW) / 2f
|
||||||
|
val offY = (viewHeight - gridH) / 2f
|
||||||
|
|
||||||
|
return TerminalGridMetrics(cellW, cellH, gridW, gridH, offX, offY)
|
||||||
|
}
|
||||||
|
|
||||||
private fun copySelection(
|
private fun copySelection(
|
||||||
start: Offset?,
|
start: Offset?,
|
||||||
end: Offset?,
|
end: Offset?,
|
||||||
@@ -429,13 +496,15 @@ private fun copySelection(
|
|||||||
) {
|
) {
|
||||||
if (start != null && end != null && screenBuffer != null && cols > 0 && rows > 0) {
|
if (start != null && end != null && screenBuffer != null && cols > 0 && rows > 0) {
|
||||||
val displayMetrics = context.resources.displayMetrics
|
val displayMetrics = context.resources.displayMetrics
|
||||||
val cellWidth = displayMetrics.widthPixels.toFloat() / cols
|
val viewW = displayMetrics.widthPixels.toFloat()
|
||||||
val cellHeight = displayMetrics.heightPixels.toFloat() / rows
|
val viewH = displayMetrics.heightPixels.toFloat()
|
||||||
|
val metrics = calculateGridMetrics(viewW, viewH, cols, rows)
|
||||||
|
if (metrics.cellWidth <= 0f || metrics.cellHeight <= 0f) return
|
||||||
|
|
||||||
val startCol = (start.x / cellWidth).toInt().coerceIn(0, cols - 1)
|
val startCol = ((start.x - metrics.offsetX) / metrics.cellWidth).toInt().coerceIn(0, cols - 1)
|
||||||
val startRow = (start.y / cellHeight).toInt().coerceIn(0, rows - 1)
|
val startRow = ((start.y - metrics.offsetY) / metrics.cellHeight).toInt().coerceIn(0, rows - 1)
|
||||||
val endCol = (end.x / cellWidth).toInt().coerceIn(0, cols - 1)
|
val endCol = ((end.x - metrics.offsetX) / metrics.cellWidth).toInt().coerceIn(0, cols - 1)
|
||||||
val endRow = (end.y / cellHeight).toInt().coerceIn(0, rows - 1)
|
val endRow = ((end.y - metrics.offsetY) / metrics.cellHeight).toInt().coerceIn(0, rows - 1)
|
||||||
|
|
||||||
val minRow = minOf(startRow, endRow)
|
val minRow = minOf(startRow, endRow)
|
||||||
val maxRow = maxOf(startRow, endRow)
|
val maxRow = maxOf(startRow, endRow)
|
||||||
|
|||||||
@@ -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