j3270 updates applied for lightpen
Build and Test a3270 / Build Android APK (push) Successful in 4m34s
Build and Test a3270 / Build Android APK (push) Successful in 4m34s
This commit is contained in:
@@ -9,9 +9,11 @@ echo "=== Building a3270 (Android APK) ==="
|
||||
# Set up JAVA_HOME (prefer JDK 21 if available)
|
||||
if [ -d "/Users/rudi/.sdkman/candidates/java/21.0.6-sem" ]; then
|
||||
export JAVA_HOME="/Users/rudi/.sdkman/candidates/java/21.0.6-sem"
|
||||
elif [ -d "$HOME/.sdkman/candidates/java/21.0.2-open" ]; then
|
||||
export JAVA_HOME="$HOME/.sdkman/candidates/java/21.0.2-open"
|
||||
elif [ -z "$JAVA_HOME" ]; then
|
||||
if [ -d "/Users/rudi/.sdkman/candidates/java/current" ]; then
|
||||
export JAVA_HOME="/Users/rudi/.sdkman/candidates/java/current"
|
||||
if [ -d "$HOME/.sdkman/candidates/java/current" ]; then
|
||||
export JAVA_HOME="$HOME/.sdkman/candidates/java/current"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -19,6 +21,8 @@ fi
|
||||
if [ -z "$ANDROID_HOME" ]; then
|
||||
if [ -d "$HOME/Library/Android/sdk" ]; then
|
||||
export ANDROID_HOME="$HOME/Library/Android/sdk"
|
||||
elif [ -d "$HOME/Android/Sdk" ]; then
|
||||
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -243,6 +243,12 @@ class MainActivity : ComponentActivity() {
|
||||
viewModel.resetKeyboard()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_L -> {
|
||||
viewModel.toggleLightPen()
|
||||
val isLp = viewModel.isLightPenMode.value
|
||||
Toast.makeText(this, "Light Pen: " + (if (isLp) "ON" else "OFF"), Toast.LENGTH_SHORT).show()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +405,7 @@ fun MainScreen(
|
||||
val isTlsVerified by viewModel.isTlsVerified.collectAsState()
|
||||
val untrustedCertPrompt by viewModel.untrustedCertPrompt.collectAsState()
|
||||
val ftState by viewModel.ftState.collectAsState()
|
||||
val isLightPenMode by viewModel.isLightPenMode.collectAsState()
|
||||
|
||||
var showConnectDialog by remember { mutableStateOf(false) }
|
||||
var showFtDialog by remember { mutableStateOf(false) }
|
||||
@@ -466,12 +473,21 @@ fun MainScreen(
|
||||
screenVersion = screenVersion,
|
||||
programSymbolManager = viewModel.getClient()?.programSymbolManager,
|
||||
graphicsPlane = viewModel.getClient()?.graphicsPlane,
|
||||
gocaDecoder = viewModel.getGocaDecoder(),
|
||||
isLightPenMode = isLightPenMode,
|
||||
maskHiddenFields = maskHiddenInput,
|
||||
blinkCursor = cursorBlink,
|
||||
onTapAddress = { addr ->
|
||||
viewModel.setCursor(addr)
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
},
|
||||
onLightPenSelect = { addr ->
|
||||
viewModel.selectLightPen(addr)
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
},
|
||||
onGraphicTouch = { px, py ->
|
||||
viewModel.handleGraphicTouch(px, py)
|
||||
},
|
||||
onPasteText = { text ->
|
||||
viewModel.pasteString(text)
|
||||
},
|
||||
@@ -512,11 +528,13 @@ fun MainScreen(
|
||||
TwoRowKeyBar(
|
||||
connectionState = connectionState,
|
||||
isShiftPressed = isShiftPressed,
|
||||
isLightPenMode = isLightPenMode,
|
||||
hapticFeedbackEnabled = hapticFeedback,
|
||||
onClearShift = onClearShift,
|
||||
onConnectClick = { showConnectDialog = true },
|
||||
onDisconnectClick = { viewModel.disconnect() },
|
||||
onFtClick = { showFtDialog = true },
|
||||
onToggleLightPen = { viewModel.toggleLightPen() },
|
||||
onSettingsClick = { showSettingsDialog = true },
|
||||
onSendAid = { aid ->
|
||||
viewModel.sendAid(aid)
|
||||
@@ -563,7 +581,8 @@ fun MainScreen(
|
||||
cols = cols,
|
||||
isTls = isTlsActive,
|
||||
isTlsVerified = isTlsVerified,
|
||||
graphicsMode = defaultGraphicsMode
|
||||
graphicsMode = defaultGraphicsMode,
|
||||
isLightPenMode = isLightPenMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ sealed interface TerminalInputAction {
|
||||
data object Newline : TerminalInputAction
|
||||
data object Reset : TerminalInputAction
|
||||
data class SetCursor(val baddr: Int) : TerminalInputAction
|
||||
data class LightPenSelect(val baddr: Int) : TerminalInputAction
|
||||
}
|
||||
|
||||
data class UntrustedCertPromptState(
|
||||
@@ -124,6 +125,17 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
private val _untrustedCertPrompt = MutableStateFlow<UntrustedCertPromptState?>(null)
|
||||
val untrustedCertPrompt: StateFlow<UntrustedCertPromptState?> = _untrustedCertPrompt.asStateFlow()
|
||||
|
||||
private val _isLightPenMode = MutableStateFlow(false)
|
||||
val isLightPenMode: StateFlow<Boolean> = _isLightPenMode.asStateFlow()
|
||||
|
||||
fun toggleLightPen() {
|
||||
_isLightPenMode.value = !_isLightPenMode.value
|
||||
}
|
||||
|
||||
fun setLightPenMode(enabled: Boolean) {
|
||||
_isLightPenMode.value = enabled
|
||||
}
|
||||
|
||||
// File Transfer State
|
||||
private var fileTransferCoordinator: FileTransfer? = null
|
||||
private val _ftState = MutableStateFlow(FTProgressState())
|
||||
@@ -240,6 +252,13 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
buf.cursorAddress = action.baddr
|
||||
}
|
||||
}
|
||||
is TerminalInputAction.LightPenSelect -> {
|
||||
if (action.baddr in 0 until (buf.rows * buf.cols)) {
|
||||
buf.cursorAddress = action.baddr
|
||||
val res = ip.lightPenSelect(action.baddr)
|
||||
log.info("LightPenSelect at addr ${action.baddr}, result=$res")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.warning("Error processing input action: ${e.message}")
|
||||
@@ -627,6 +646,27 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
inputChannel.trySend(TerminalInputAction.CursorHome)
|
||||
}
|
||||
|
||||
fun selectLightPen(baddr: Int) {
|
||||
inputChannel.trySend(TerminalInputAction.LightPenSelect(baddr))
|
||||
}
|
||||
|
||||
fun getGocaDecoder(): org.lib3270j.graphics.GocaDecoder? = client?.gocaDecoder
|
||||
|
||||
fun handleGraphicTouch(px: Int, py: Int, button: Int = 1, shift: Boolean = false, ctrl: Boolean = false) {
|
||||
val c = client ?: return
|
||||
val goca = c.gocaDecoder ?: return
|
||||
if (goca.isGraphicsCursorActive) {
|
||||
goca.setGraphicCursorFromPixel(px, py)
|
||||
c.inputProcessor.sendGraphicMouseAid(
|
||||
AID_ENTER,
|
||||
button,
|
||||
shift,
|
||||
ctrl
|
||||
)
|
||||
_screenVersion.value = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractScreenSnippet(buf: ScreenBuffer?): String {
|
||||
if (buf == null) return "Mainframe screen update received"
|
||||
val rows = buf.rows
|
||||
|
||||
@@ -22,11 +22,13 @@ import org.lib3270j.protocol.DS3270Constants.*
|
||||
fun TwoRowKeyBar(
|
||||
connectionState: ConnectionState,
|
||||
isShiftPressed: Boolean = false,
|
||||
isLightPenMode: Boolean = false,
|
||||
hapticFeedbackEnabled: Boolean = true,
|
||||
onClearShift: () -> Unit = {},
|
||||
onConnectClick: () -> Unit,
|
||||
onDisconnectClick: () -> Unit,
|
||||
onFtClick: () -> Unit,
|
||||
onToggleLightPen: () -> Unit = {},
|
||||
onSettingsClick: () -> Unit = {},
|
||||
onSendAid: (Int) -> Unit,
|
||||
onReset: () -> Unit,
|
||||
@@ -53,7 +55,7 @@ fun TwoRowKeyBar(
|
||||
.padding(horizontal = 1.dp, vertical = 1.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 1. Menu Dropdown Button (Connect/Disconnect/FT/Settings)
|
||||
// 1. Menu Dropdown Button (Connect/Disconnect/FT/Light Pen/Settings)
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
KeyButton(
|
||||
label = "⋮",
|
||||
@@ -92,6 +94,19 @@ fun TwoRowKeyBar(
|
||||
}
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = if (isLightPenMode) "Light Pen: ON" else "Light Pen: OFF",
|
||||
color = if (isLightPenMode) Color(0xFFFFFF50) else Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
menuExpanded = false
|
||||
onToggleLightPen()
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Settings", color = Color.White, fontWeight = FontWeight.SemiBold) },
|
||||
onClick = {
|
||||
|
||||
@@ -27,6 +27,7 @@ fun OiaStatusBar(
|
||||
isTls: Boolean = false,
|
||||
isTlsVerified: Boolean = true,
|
||||
graphicsMode: String = "NONE",
|
||||
isLightPenMode: Boolean = false,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val row = if (cols > 0) (cursorAddr / cols) + 1 else 1
|
||||
@@ -119,6 +120,24 @@ fun OiaStatusBar(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Light Pen Badge
|
||||
if (isLightPenMode) {
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Surface(
|
||||
color = Color(0xFFFFFF50).copy(alpha = 0.25f),
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "LP",
|
||||
color = Color(0xFFFFFF50),
|
||||
fontSize = 9.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Terminal Keyboard Lock Status Badge (X SYSTEM vs READY)
|
||||
|
||||
@@ -60,9 +60,13 @@ fun TerminalView(
|
||||
screenVersion: Long,
|
||||
programSymbolManager: org.lib3270j.graphics.ProgramSymbolManager? = null,
|
||||
graphicsPlane: org.lib3270j.graphics.GraphicsPlane? = null,
|
||||
gocaDecoder: org.lib3270j.graphics.GocaDecoder? = null,
|
||||
isLightPenMode: Boolean = false,
|
||||
maskHiddenFields: Boolean = true,
|
||||
blinkCursor: Boolean = true,
|
||||
onTapAddress: (Int) -> Unit,
|
||||
onLightPenSelect: ((Int) -> Unit)? = null,
|
||||
onGraphicTouch: ((Int, Int) -> Unit)? = null,
|
||||
onPasteText: (String) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -97,7 +101,7 @@ fun TerminalView(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(COLOR_BLACK)
|
||||
.pointerInput(cols, rows) {
|
||||
.pointerInput(cols, rows, isLightPenMode, gocaDecoder) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
@@ -106,6 +110,7 @@ fun TerminalView(
|
||||
var currentPos = startPos
|
||||
var isDragStarted = false
|
||||
val slop = viewConfiguration.touchSlop
|
||||
val isGraphicCursor = gocaDecoder != null && gocaDecoder.isGraphicsCursorActive
|
||||
|
||||
while (true) {
|
||||
val event = awaitPointerEvent()
|
||||
@@ -116,7 +121,7 @@ fun TerminalView(
|
||||
currentPos = change.position
|
||||
val distance = (currentPos - startPos).getDistance()
|
||||
|
||||
if (distance > slop) {
|
||||
if (distance > slop && !isLightPenMode && !isGraphicCursor) {
|
||||
if (!isDragStarted) {
|
||||
isDragStarted = true
|
||||
selectionStart = startPos
|
||||
@@ -133,12 +138,12 @@ fun TerminalView(
|
||||
if (isDragStarted) {
|
||||
// Drag completed: Keep selection box highlighted, DO NOT open context menu
|
||||
showContextMenu = false
|
||||
} else if (duration >= 600L && totalDistance <= slop) {
|
||||
// Stationary Long Press (held >= 600ms without dragging)
|
||||
} else if (duration >= 600L && totalDistance <= slop && !isLightPenMode && !isGraphicCursor) {
|
||||
// Stationary Long Press (held >= 600ms without dragging) - only in normal alphanumeric mode
|
||||
contextMenuOffset = startPos
|
||||
showContextMenu = true
|
||||
} else if (totalDistance <= slop) {
|
||||
// Short Tap (Move Cursor)
|
||||
// Short Tap
|
||||
showContextMenu = false
|
||||
selectionStart = null
|
||||
selectionEnd = null
|
||||
@@ -146,8 +151,20 @@ fun TerminalView(
|
||||
if (metrics.cellWidth > 0 && metrics.cellHeight > 0) {
|
||||
val col = ((startPos.x - metrics.offsetX) / metrics.cellWidth).toInt().coerceIn(0, cols - 1)
|
||||
val row = ((startPos.y - metrics.offsetY) / metrics.cellHeight).toInt().coerceIn(0, rows - 1)
|
||||
var addr = row * cols + col
|
||||
val clickAddr = row * cols + col
|
||||
|
||||
if (isGraphicCursor) {
|
||||
val gridW = metrics.gridWidth
|
||||
val gridH = metrics.gridHeight
|
||||
val gWidth = graphicsPlane?.canvasWidth ?: gridW.toInt()
|
||||
val gHeight = graphicsPlane?.canvasHeight ?: gridH.toInt()
|
||||
val px = if (gridW > 0 && gWidth > 0) (((startPos.x - metrics.offsetX) * gWidth) / gridW).toInt().coerceIn(0, gWidth - 1) else (startPos.x - metrics.offsetX).toInt()
|
||||
val py = if (gridH > 0 && gHeight > 0) (((startPos.y - metrics.offsetY) * gHeight) / gridH).toInt().coerceIn(0, gHeight - 1) else (startPos.y - metrics.offsetY).toInt()
|
||||
onGraphicTouch?.invoke(px, py)
|
||||
} else if (isLightPenMode) {
|
||||
onLightPenSelect?.invoke(clickAddr)
|
||||
} else {
|
||||
var addr = clickAddr
|
||||
val buf = screenBuffer
|
||||
if (buf != null && buf.isFormatted) {
|
||||
val faVal = buf.getFieldAttributeAt(addr)
|
||||
@@ -161,6 +178,7 @@ fun TerminalView(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
val version = currentScreenVersion
|
||||
val activeCursorAddr = currentCursorAddr
|
||||
|
||||
@@ -165,4 +165,51 @@ class HardwareKeyboardInputTest {
|
||||
assertFalse(inputProcessor.isKeyboardLocked)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLightPenSelection() {
|
||||
// Field 1 at pos 0: Selectable Unprotected (FA_INT_HIGH)
|
||||
// Designator at pos 1: '?' (EBCDIC 0x6F)
|
||||
val fa = (FA_PRINTABLE or FA_INT_HIGH_SEL).toByte()
|
||||
assertTrue(org.lib3270j.protocol.DS3270Constants.faIsSelectable(fa.toInt() and 0xFF))
|
||||
|
||||
screenBuffer.setCellFA(0, fa)
|
||||
screenBuffer.getCell(1).ucs4 = '?'
|
||||
screenBuffer.getCell(1).ec = translator.unicodeToEbcdic('?').toByte()
|
||||
|
||||
// Field 2 at pos 20: Non-selectable (normal intensity)
|
||||
screenBuffer.setCellFA(20, FA_PRINTABLE.toByte())
|
||||
screenBuffer.getCell(21).ucs4 = '?'
|
||||
|
||||
// Selecting field 1 ('?') toggles designator to '>' and sets modified bit
|
||||
val result1 = inputProcessor.lightPenSelect(1)
|
||||
assertTrue(result1)
|
||||
assertEquals('>', screenBuffer.getCell(1).ucs4)
|
||||
assertEquals(0x6E.toByte(), screenBuffer.getCell(1).ec)
|
||||
val fa1 = screenBuffer.getCell(0).fa.toInt() and 0xFF
|
||||
assertTrue((fa1 and FA_MODIFY) != 0)
|
||||
|
||||
// Selecting field 1 again ('>') toggles back to '?' and clears modified bit
|
||||
val result2 = inputProcessor.lightPenSelect(1)
|
||||
assertTrue(result2)
|
||||
assertEquals('?', screenBuffer.getCell(1).ucs4)
|
||||
assertEquals(0x6F.toByte(), screenBuffer.getCell(1).ec)
|
||||
|
||||
// Selecting field 2 (non-selectable) returns false
|
||||
val resultNonSelectable = inputProcessor.lightPenSelect(21)
|
||||
assertFalse(resultNonSelectable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLightPenEnterSelection() {
|
||||
// Field with '&' designator immediately sends AID_ENTER
|
||||
val fa = (FA_PRINTABLE or FA_INT_HIGH_SEL).toByte()
|
||||
screenBuffer.setCellFA(0, fa)
|
||||
screenBuffer.getCell(1).ucs4 = '&'
|
||||
screenBuffer.getCell(1).ec = translator.unicodeToEbcdic('&').toByte()
|
||||
|
||||
val result = inputProcessor.lightPenSelect(1)
|
||||
assertTrue(result)
|
||||
assertEquals(AID_ENTER, inputProcessor.lastAid)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user