Add new settings from j3270
Build and Test a3270 / Build Android APK (push) Failing after 26s

This commit is contained in:
2026-09-15 23:40:22 +00:00
parent b71f0f5ba4
commit c941fd3a2a
6 changed files with 171 additions and 2 deletions
@@ -528,6 +528,7 @@ fun MainScreen(
val isCrosshairRuler by viewModel.isCrosshairRuler.collectAsState()
val cursorStyle by viewModel.cursorStyle.collectAsState()
val blockSelectMode by viewModel.blockSelectMode.collectAsState()
val blockPaste by viewModel.blockPaste.collectAsState()
val uiTheme by viewModel.uiTheme.collectAsState()
val isInsertMode by viewModel.isInsertMode.collectAsState()
val isDocMode by viewModel.isDocMode.collectAsState()
@@ -660,6 +661,9 @@ fun MainScreen(
cursorStyle = cursorStyle,
isInsertMode = isInsertMode,
blockSelectMode = blockSelectMode,
blockPaste = blockPaste,
onToggleBlockSelectMode = { viewModel.setBlockSelectMode(!blockSelectMode) },
onToggleBlockPaste = { viewModel.setBlockPaste(!blockPaste) },
fourColorOverride = fourColorOverride,
onPasteLineWrap = { text, wrapCol, wordWrap ->
viewModel.pasteLineWrap(text, wrapCol, wordWrap)
@@ -897,6 +901,7 @@ fun MainScreen(
initialCrosshairRuler = isCrosshairRuler,
initialCursorStyle = cursorStyle,
initialBlockSelectMode = blockSelectMode,
initialBlockPaste = blockPaste,
initialStartupBehavior = AppSettings.getStartupBehavior(context).name,
initialDynamicRows = AppSettings.getDynamicRows(context),
initialDynamicCols = AppSettings.getDynamicCols(context),
@@ -915,7 +920,7 @@ fun MainScreen(
showSettingsDialog = false
terminalInputViewRef?.showSoftKeyboard()
},
onSave = { mask, blink, haptic, verify, gfx, defaultCp, theme, crosshair, style, blockSel, startup, dynRows, dynCols,
onSave = { mask, blink, haptic, verify, gfx, defaultCp, theme, crosshair, style, blockSel, blockPst, startup, dynRows, dynCols,
doc, wrap, startCol, endCol, bell, bellCol, tabs, resetIns, fourCol, numLk, autoSk ->
showSettingsDialog = false
viewModel.updateSettings(mask, blink, haptic, verify, gfx, defaultCp)
@@ -923,6 +928,7 @@ fun MainScreen(
viewModel.setCrosshairRuler(crosshair)
viewModel.setCursorStyle(style)
viewModel.setBlockSelectMode(blockSel)
viewModel.setBlockPaste(blockPst)
AppSettings.setStartupBehavior(context, AppSettings.StartupBehavior.valueOf(startup))
AppSettings.setDynamicRows(context, dynRows)
AppSettings.setDynamicCols(context, dynCols)
@@ -61,6 +61,7 @@ sealed interface TerminalInputAction {
data class SetAplMode(val enabled: Boolean) : TerminalInputAction
data class SetCursor(val baddr: Int) : TerminalInputAction
data class LightPenSelect(val baddr: Int) : TerminalInputAction
data class PasteText(val text: String) : TerminalInputAction
}
data class UntrustedCertPromptState(
@@ -161,6 +162,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
private val _blockSelectMode = MutableStateFlow(AppSettings.isBlockSelectModeEnabled(application))
val blockSelectMode: StateFlow<Boolean> = _blockSelectMode.asStateFlow()
private val _blockPaste = MutableStateFlow(AppSettings.isBlockPasteEnabled(application))
val blockPaste: StateFlow<Boolean> = _blockPaste.asStateFlow()
private val _uiTheme = MutableStateFlow(AppSettings.getUiTheme(application))
val uiTheme: StateFlow<String> = _uiTheme.asStateFlow()
@@ -206,6 +210,17 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
_blockSelectMode.value = enabled
}
fun setBlockPaste(enabled: Boolean) {
val app = getApplication<Application>()
AppSettings.setBlockPasteEnabled(app, enabled)
_blockPaste.value = enabled
applyModeSettings()
}
fun toggleBlockPaste() {
setBlockPaste(!_blockPaste.value)
}
fun setUiTheme(theme: String) {
AppSettings.setUiTheme(getApplication(), theme)
_uiTheme.value = theme
@@ -270,7 +285,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
ip.isInsertOffOnAid = AppSettings.getInsertOffOnAid(app)
ip.isNumericFieldLock = AppSettings.getNumericFieldLock(app)
ip.isAutoSkipEnabled = AppSettings.getAutoSkipEnabled(app)
ip.isBlockPaste = AppSettings.isBlockPasteEnabled(app)
}
c.ps?.isBlockPaste = AppSettings.isBlockPasteEnabled(app)
_fourColorOverride.value = AppSettings.getFourColorOverride(app)
_isDocMode.value = c.isDocMode
_isWordWrap.value = c.isWordWrap
@@ -290,6 +307,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
_isCrosshairRuler.value = AppSettings.isCrosshairRulerEnabled(app)
_cursorStyle.value = AppSettings.getCursorStyle(app)
_blockSelectMode.value = AppSettings.isBlockSelectModeEnabled(app)
_blockPaste.value = AppSettings.isBlockPasteEnabled(app)
_uiTheme.value = AppSettings.getUiTheme(app)
_fourColorOverride.value = AppSettings.getFourColorOverride(app)
applyModeSettings()
@@ -391,6 +409,24 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
}
}
}
is TerminalInputAction.PasteText -> {
log.info("PasteText: len=${action.text.length}, curAddr=${buf.cursorAddress}, formatted=${buf.isFormatted}, isNvt=$isNvt")
ip.isKeyboardLocked = false
if (isNvt) {
for (ch in action.text) {
if (ch == '\n' || ch == '\r') {
c.sendNVTString("\r\n")
} else {
c.sendNVTChar(ch)
}
}
} else {
val app = getApplication<Application>()
val blockPaste = AppSettings.isBlockPasteEnabled(app)
ip.pasteText(action.text, true, false, blockPaste)
_screenVersion.value = System.currentTimeMillis()
}
}
is TerminalInputAction.SendAid -> {
if (isNvt) {
if (action.aidCode == AID_ENTER) {
@@ -1103,7 +1139,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
fun pasteString(text: String) {
if (text.isNotEmpty()) {
inputChannel.trySend(TerminalInputAction.TypeText(text))
inputChannel.trySend(TerminalInputAction.PasteText(text))
}
}
@@ -25,6 +25,7 @@ object AppSettings {
private const val KEY_CROSSHAIR_RULER = "crosshair_ruler"
private const val KEY_CURSOR_STYLE = "cursor_style"
private const val KEY_BLOCK_SELECT_MODE = "block_select_mode"
private const val KEY_BLOCK_PASTE = "block_paste"
private const val KEY_STARTUP_BEHAVIOR = "startup_behavior"
private const val KEY_DYNAMIC_ROWS = "dynamic_rows"
private const val KEY_DYNAMIC_COLS = "dynamic_cols"
@@ -137,6 +138,15 @@ object AppSettings {
getPrefs(context).edit().putBoolean(KEY_BLOCK_SELECT_MODE, enabled).apply()
}
// ========== Block Paste Mode ==========
fun isBlockPasteEnabled(context: Context): Boolean {
return getPrefs(context).getBoolean(KEY_BLOCK_PASTE, true)
}
fun setBlockPasteEnabled(context: Context, enabled: Boolean) {
getPrefs(context).edit().putBoolean(KEY_BLOCK_PASTE, enabled).apply()
}
// ========== Startup Behavior ==========
fun getStartupBehavior(context: Context): StartupBehavior {
val s = getPrefs(context).getString(KEY_STARTUP_BEHAVIOR, StartupBehavior.SHOW_CONNECT.name)
@@ -302,6 +312,7 @@ object AppSettings {
p.println("dynamicRows = ${getDynamicRows(context)}")
p.println("dynamicCols = ${getDynamicCols(context)}")
p.println("blockSelectMode = ${isBlockSelectModeEnabled(context)}")
p.println("blockPaste = ${isBlockPasteEnabled(context)}")
p.println()
p.println("[entryassist]")
@@ -369,6 +380,12 @@ object AppSettings {
"dynamicrows", "dynamic_rows" -> value.toIntOrNull()?.let { setDynamicRows(context, it) }
"dynamiccols", "dynamic_cols" -> value.toIntOrNull()?.let { setDynamicCols(context, it) }
"blockselectmode" -> setBlockSelectModeEnabled(context, value.toBoolean())
"blockpaste", "blockpastemode" -> setBlockPasteEnabled(context, value.toBoolean())
}
}
"clipboard" -> {
when (key.lowercase(Locale.US)) {
"blockpaste", "blockpastemode" -> setBlockPasteEnabled(context, value.toBoolean())
}
}
"entryassist" -> {
@@ -30,6 +30,7 @@ fun SettingsDialog(
initialCrosshairRuler: Boolean = false,
initialCursorStyle: String = "BLOCK",
initialBlockSelectMode: Boolean = false,
initialBlockPaste: Boolean = true,
initialStartupBehavior: String = "SHOW_CONNECT",
initialDynamicRows: Int = 62,
initialDynamicCols: Int = 160,
@@ -56,6 +57,7 @@ fun SettingsDialog(
crosshairRuler: Boolean,
cursorStyle: String,
blockSelectMode: Boolean,
blockPaste: Boolean,
startupBehavior: String,
dynamicRows: Int,
dynamicCols: Int,
@@ -86,6 +88,7 @@ fun SettingsDialog(
var crosshairRuler by remember { mutableStateOf(initialCrosshairRuler) }
var cursorStyle by remember { mutableStateOf(initialCursorStyle) }
var blockSelectMode by remember { mutableStateOf(initialBlockSelectMode) }
var blockPaste by remember { mutableStateOf(initialBlockPaste) }
var startupBehavior by remember { mutableStateOf(initialStartupBehavior) }
var dynamicRowsStr by remember { mutableStateOf(initialDynamicRows.toString()) }
var dynamicColsStr by remember { mutableStateOf(initialDynamicCols.toString()) }
@@ -262,6 +265,25 @@ fun SettingsDialog(
Spacer(modifier = Modifier.height(10.dp))
// Block Paste Mode
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
Text("Block Paste Mode", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Align multi-line clipboard pastes with cursor start column.", fontSize = 12.sp, color = Color.LightGray)
}
Switch(
checked = blockPaste,
onCheckedChange = { blockPaste = it },
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
)
}
Spacer(modifier = Modifier.height(10.dp))
// Haptic Feedback
Row(
modifier = Modifier.fillMaxWidth(),
@@ -707,6 +729,7 @@ fun SettingsDialog(
crosshairRuler,
cursorStyle,
blockSelectMode,
blockPaste,
startupBehavior,
dRows,
dCols,
@@ -760,6 +783,7 @@ fun SettingsDialog(
crosshairRuler = haus.nightmare.a3270.storage.AppSettings.isCrosshairRulerEnabled(context)
cursorStyle = haus.nightmare.a3270.storage.AppSettings.getCursorStyle(context)
blockSelectMode = haus.nightmare.a3270.storage.AppSettings.isBlockSelectModeEnabled(context)
blockPaste = haus.nightmare.a3270.storage.AppSettings.isBlockPasteEnabled(context)
startupBehavior = haus.nightmare.a3270.storage.AppSettings.getStartupBehavior(context).name
dynamicRowsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
@@ -70,6 +70,7 @@ fun TerminalView(
cursorStyle: String = "BLOCK",
isInsertMode: Boolean = false,
blockSelectMode: Boolean = false,
blockPaste: Boolean = true,
fourColorOverride: Boolean = false,
searchHighlightAddr: Int = -1,
searchHighlightLen: Int = 0,
@@ -78,6 +79,8 @@ fun TerminalView(
onGraphicTouch: ((Int, Int, Int) -> Unit)? = null,
onPasteText: (String) -> Unit = {},
onPasteLineWrap: ((String, Int, Boolean) -> Unit)? = null,
onToggleBlockSelectMode: (() -> Unit)? = null,
onToggleBlockPaste: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
@@ -569,6 +572,26 @@ fun TerminalView(
showPasteWrapDialog = true
}
)
if (onToggleBlockSelectMode != null) {
DropdownMenuItem(
text = { Text(if (blockSelectMode) "✓ Block Select Mode" else " Block Select Mode") },
onClick = {
showContextMenu = false
onToggleBlockSelectMode()
}
)
}
if (onToggleBlockPaste != null) {
DropdownMenuItem(
text = { Text(if (blockPaste) "✓ Block Paste" else " Block Paste") },
onClick = {
showContextMenu = false
onToggleBlockPaste()
}
)
}
}
if (showPasteWrapDialog) {
@@ -4,6 +4,7 @@ import haus.nightmare.lib3270j.TerminalModel
import haus.nightmare.lib3270j.charset.EbcdicTranslator
import haus.nightmare.lib3270j.ecl.ECLConstants
import haus.nightmare.lib3270j.ecl.ECLOIA
import haus.nightmare.lib3270j.ecl.ECLPS
import haus.nightmare.lib3270j.input.InputProcessor
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
import haus.nightmare.lib3270j.screen.ScreenBuffer
@@ -191,6 +192,13 @@ class EntryAssistAndModesTest {
@Test
fun testIniSectionSyntaxMatchingJ3270() {
val sampleIni = """
[behavior]
blockSelectMode = false
blockPaste = true
[clipboard]
blockPasteMode = true
[entryassist]
docMode = true
wordWrap = true
@@ -223,6 +231,10 @@ class EntryAssistAndModesTest {
}
}
assertEquals("false", parsed["behavior.blockselectmode"])
assertEquals("true", parsed["behavior.blockpaste"])
assertEquals("true", parsed["clipboard.blockpastemode"])
assertEquals("true", parsed["entryassist.docmode"])
assertEquals("true", parsed["entryassist.wordwrap"])
assertEquals("5", parsed["entryassist.startcol"])
@@ -237,6 +249,57 @@ class EntryAssistAndModesTest {
assertEquals("true", parsed["modes.autoskip"])
}
@Test
fun testBlockPasteMultiLineAlignment() {
val translator = EbcdicTranslator()
val screen = ScreenBuffer(TerminalModel.IBM_3278_2, translator)
val input = InputProcessor(screen, translator, null)
val ps = ECLPS(screen, input, translator)
// Verify default blockPaste is true
assertTrue(input.isBlockPaste)
assertTrue(ps.isBlockPaste)
// 1. Unformatted screen: verify start column 15 is preserved across newlines
screen.erase(false)
val startPos = screen.rowColToAddress(0, 15)
screen.cursorAddress = startPos
val multilineText = "FIRST\nSECOND\nTHIRD"
val pasted = input.pasteText(multilineText, true, false, true)
assertEquals(16, pasted)
assertEquals("FIRST", ps.getString(startPos, 5))
val row1Col15 = screen.rowColToAddress(1, 15)
assertEquals("SECOND", ps.getString(row1Col15, 6))
val row2Col15 = screen.rowColToAddress(2, 15)
assertEquals("THIRD", ps.getString(row2Col15, 5))
// 2. Formatted screen with blockPaste = true
screen.erase(false)
screen.setFieldAttribute(0, FA_PRINTABLE.toByte())
screen.setFieldAttribute(80, FA_PRINTABLE.toByte())
screen.setFieldAttribute(160, FA_PRINTABLE.toByte())
screen.cursorAddress = screen.rowColToAddress(0, 25)
val pastedFormatted = input.pasteText("ALPHA\r\nBETA\r\nGAMMA", true, false, true)
assertEquals(14, pastedFormatted)
assertEquals("ALPHA", ps.getString(screen.rowColToAddress(0, 25), 5))
assertEquals("BETA", ps.getString(screen.rowColToAddress(1, 25), 4))
assertEquals("GAMMA", ps.getString(screen.rowColToAddress(2, 25), 5))
// 3. Formatted screen with blockPaste = false: reverts to first unprotected field on next row (pos 81)
screen.erase(false)
screen.setFieldAttribute(0, FA_PRINTABLE.toByte())
screen.setFieldAttribute(80, FA_PRINTABLE.toByte())
screen.cursorAddress = screen.rowColToAddress(0, 25)
input.pasteText("LINE1\nLINE2", true, false, false)
assertEquals("LINE1", ps.getString(screen.rowColToAddress(0, 25), 5))
assertEquals("LINE2", ps.getString(81, 5))
}
@Test
fun testCmsFilenameFormatting() {
// Upload (local -> host)