This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user