From c941fd3a2ae82a452cf39579cb2bad98858305b4 Mon Sep 17 00:00:00 2001 From: Rudi Date: Tue, 15 Sep 2026 23:40:22 +0000 Subject: [PATCH] Add new settings from j3270 --- .../java/haus/nightmare/a3270/MainActivity.kt | 8 ++- .../haus/nightmare/a3270/TerminalViewModel.kt | 38 ++++++++++- .../nightmare/a3270/storage/AppSettings.kt | 17 +++++ .../haus/nightmare/a3270/ui/SettingsDialog.kt | 24 +++++++ .../haus/nightmare/a3270/ui/TerminalView.kt | 23 +++++++ .../a3270/EntryAssistAndModesTest.kt | 63 +++++++++++++++++++ 6 files changed, 171 insertions(+), 2 deletions(-) diff --git a/src/main/java/haus/nightmare/a3270/MainActivity.kt b/src/main/java/haus/nightmare/a3270/MainActivity.kt index 8638b20..ba9235b 100644 --- a/src/main/java/haus/nightmare/a3270/MainActivity.kt +++ b/src/main/java/haus/nightmare/a3270/MainActivity.kt @@ -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) diff --git a/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt b/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt index 2028843..29b53f5 100644 --- a/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt +++ b/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt @@ -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 = _blockSelectMode.asStateFlow() + private val _blockPaste = MutableStateFlow(AppSettings.isBlockPasteEnabled(application)) + val blockPaste: StateFlow = _blockPaste.asStateFlow() + private val _uiTheme = MutableStateFlow(AppSettings.getUiTheme(application)) val uiTheme: StateFlow = _uiTheme.asStateFlow() @@ -206,6 +210,17 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application _blockSelectMode.value = enabled } + fun setBlockPaste(enabled: Boolean) { + val app = getApplication() + 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() + 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)) } } diff --git a/src/main/java/haus/nightmare/a3270/storage/AppSettings.kt b/src/main/java/haus/nightmare/a3270/storage/AppSettings.kt index 28da9a4..53a7ae9 100644 --- a/src/main/java/haus/nightmare/a3270/storage/AppSettings.kt +++ b/src/main/java/haus/nightmare/a3270/storage/AppSettings.kt @@ -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" -> { diff --git a/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt b/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt index 7e9bec2..adf4bc3 100644 --- a/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt +++ b/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt @@ -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() diff --git a/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt b/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt index 2ecde7a..2b7af88 100644 --- a/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt +++ b/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt @@ -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) { diff --git a/src/test/java/haus/nightmare/a3270/EntryAssistAndModesTest.kt b/src/test/java/haus/nightmare/a3270/EntryAssistAndModesTest.kt index ab171eb..51a951a 100644 --- a/src/test/java/haus/nightmare/a3270/EntryAssistAndModesTest.kt +++ b/src/test/java/haus/nightmare/a3270/EntryAssistAndModesTest.kt @@ -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)