From cf58b5c72c824908466d5b30464844f2d547de98 Mon Sep 17 00:00:00 2001 From: Rudi Date: Fri, 21 Aug 2026 01:03:14 -0400 Subject: [PATCH] Debug input 2 --- README.md | 4 +- build.gradle | 4 +- src/main/java/org/pubvm/a3270/MainActivity.kt | 14 +- .../java/org/pubvm/a3270/TerminalViewModel.kt | 148 +++++++++++------- .../org/pubvm/a3270/storage/AppSettings.kt | 9 ++ .../java/org/pubvm/a3270/ui/FunctionKeyBar.kt | 39 +++-- .../java/org/pubvm/a3270/ui/SettingsDialog.kt | 42 ++++- 7 files changed, 179 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 3f26741..2fcf7e6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # a3270 [![Build & Test](https://git.hugfreevikings.wtf/rudi/a3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/a3270/actions) -[![Latest Release](https://git.hugfreevikings.wtf/rudi/a3270/badges/release.svg)](https://git.hugfreevikings.wtf/rudi/a3270/releases) [![Android](https://img.shields.io/badge/Android-API%2024%2B%20%7C%20Compose-green.svg)](https://developer.android.com) [![License](https://img.shields.io/badge/License-MIT%20%2F%20BSD-green.svg)](LICENSE) @@ -110,3 +109,6 @@ I have contributed no code to this project, it was entirely written by LLMs with - Claude Opus 4.6 - Gemini 3.1 Pro - Gemini 3.7 Flash + - Gemma4 12B and 26B + - GPT-OSS 120B + - Qwen3 4B and 32B diff --git a/build.gradle b/build.gradle index 7062f66..c098801 100644 --- a/build.gradle +++ b/build.gradle @@ -11,8 +11,8 @@ android { applicationId "org.pubvm.a3270" minSdk 24 targetSdk 34 - versionCode 3 - versionName "0.1.2" + versionCode 4 + versionName "0.1.3" } buildTypes { diff --git a/src/main/java/org/pubvm/a3270/MainActivity.kt b/src/main/java/org/pubvm/a3270/MainActivity.kt index 0f482e2..5277c60 100644 --- a/src/main/java/org/pubvm/a3270/MainActivity.kt +++ b/src/main/java/org/pubvm/a3270/MainActivity.kt @@ -147,6 +147,7 @@ fun MainScreen( val screenVersion by viewModel.screenVersion.collectAsState() val maskHiddenInput by viewModel.maskHiddenInput.collectAsState() val cursorBlink by viewModel.cursorBlink.collectAsState() + val hapticFeedback by viewModel.hapticFeedback.collectAsState() val ftState by viewModel.ftState.collectAsState() var showConnectDialog by remember { mutableStateOf(false) } @@ -249,6 +250,7 @@ fun MainScreen( TwoRowKeyBar( connectionState = connectionState, isShiftPressed = isShiftPressed, + hapticFeedbackEnabled = hapticFeedback, onClearShift = onClearShift, onConnectClick = { showConnectDialog = true }, onDisconnectClick = { viewModel.disconnect() }, @@ -318,9 +320,10 @@ fun MainScreen( ftProgressState = ftState, onDismiss = { showFtDialog = false }, onStartTransfer = { config -> - val error = viewModel.startFileTransfer(config) - if (error != null) { - Toast.makeText(context, error, Toast.LENGTH_LONG).show() + viewModel.startFileTransfer(config) { error -> + if (error != null) { + Toast.makeText(context, error, Toast.LENGTH_LONG).show() + } } }, onCancelTransfer = { @@ -333,10 +336,11 @@ fun MainScreen( SettingsDialog( initialMaskHiddenInput = maskHiddenInput, initialCursorBlink = cursorBlink, + initialHapticFeedback = hapticFeedback, onDismiss = { showSettingsDialog = false }, - onSave = { mask, blink -> + onSave = { mask, blink, haptic -> showSettingsDialog = false - viewModel.updateSettings(mask, blink) + viewModel.updateSettings(mask, blink, haptic) terminalInputViewRef?.showSoftKeyboard() } ) diff --git a/src/main/java/org/pubvm/a3270/TerminalViewModel.kt b/src/main/java/org/pubvm/a3270/TerminalViewModel.kt index c171ec9..26e49bf 100644 --- a/src/main/java/org/pubvm/a3270/TerminalViewModel.kt +++ b/src/main/java/org/pubvm/a3270/TerminalViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.lib3270j.ConnectionConfig import org.lib3270j.ConnectionState import org.lib3270j.Telnet3270Client @@ -92,6 +93,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application private val _cursorBlink = MutableStateFlow(AppSettings.isCursorBlinkEnabled(application)) val cursorBlink: StateFlow = _cursorBlink.asStateFlow() + private val _hapticFeedback = MutableStateFlow(AppSettings.isHapticFeedbackEnabled(application)) + val hapticFeedback: StateFlow = _hapticFeedback.asStateFlow() + // File Transfer State private var fileTransferCoordinator: FileTransfer? = null private val _ftState = MutableStateFlow(FTProgressState()) @@ -191,11 +195,13 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application } } - fun updateSettings(maskHidden: Boolean, blink: Boolean) { + fun updateSettings(maskHidden: Boolean, blink: Boolean, haptic: Boolean) { AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden) AppSettings.setCursorBlinkEnabled(getApplication(), blink) + AppSettings.setHapticFeedbackEnabled(getApplication(), haptic) _maskHiddenInput.value = maskHidden _cursorBlink.value = blink + _hapticFeedback.value = haptic } fun connect(host: String, port: Int = 23, modelNum: Int = 2, luName: String = "", hostType: String = "TSO") { @@ -316,77 +322,105 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application } } - fun startFileTransfer(config: FTConfig): String? { - val c = client ?: return "Terminal is not connected." - - // Resolve local path if relative (default to app files or download directory) - val rawPath = config.localFilename - if (!rawPath.startsWith("/")) { - val appFilesDir = getApplication().getExternalFilesDir(null) ?: getApplication().filesDir - val resolvedFile = File(appFilesDir, rawPath) - config.localFilename = resolvedFile.absolutePath + fun startFileTransfer(config: FTConfig, onResult: (String?) -> Unit = {}) { + val c = client + if (c == null || !_connectionState.value.isConnected()) { + val err = "Terminal is not connected." + _ftState.value = FTProgressState(isActive = false, statusMessage = err, isError = true) + onResult(err) + return } - if (fileTransferCoordinator == null) { - fileTransferCoordinator = FileTransfer(c, object : FileTransfer.FileTransferCallback { - override fun onTransferStarted() { - _ftState.value = FTProgressState( - isActive = true, - isRunning = false, - bytesTransferred = 0L, - statusMessage = "Starting IND\$FILE transfer..." - ) + viewModelScope.launch(Dispatchers.IO) { + try { + // Resolve local path if relative (default to app files or download directory) + val rawPath = config.localFilename + if (!rawPath.startsWith("/")) { + val appFilesDir = getApplication().getExternalFilesDir(null) ?: getApplication().filesDir + val resolvedFile = File(appFilesDir, rawPath) + config.localFilename = resolvedFile.absolutePath } - override fun onTransferRunning() { - _ftState.value = _ftState.value.copy( - isRunning = true, - statusMessage = "Transfer in progress..." - ) + if (fileTransferCoordinator == null) { + fileTransferCoordinator = FileTransfer(c, object : FileTransfer.FileTransferCallback { + override fun onTransferStarted() { + _ftState.value = FTProgressState( + isActive = true, + isRunning = false, + bytesTransferred = 0L, + statusMessage = "Starting IND\$FILE transfer..." + ) + } + + override fun onTransferRunning() { + _ftState.value = _ftState.value.copy( + isRunning = true, + statusMessage = "Transfer in progress..." + ) + } + + override fun onBytesTransferred(bytes: Long) { + _ftState.value = _ftState.value.copy( + bytesTransferred = bytes, + statusMessage = "Transferring: $bytes bytes" + ) + } + + override fun onTransferComplete(message: String) { + _ftState.value = FTProgressState( + isActive = false, + isRunning = false, + bytesTransferred = _ftState.value.bytesTransferred, + statusMessage = message, + isError = false + ) + } + + override fun onTransferAborted(error: String) { + _ftState.value = FTProgressState( + isActive = false, + isRunning = false, + bytesTransferred = _ftState.value.bytesTransferred, + statusMessage = "Transfer failed: $error", + isError = true + ) + } + }) } - override fun onBytesTransferred(bytes: Long) { - _ftState.value = _ftState.value.copy( - bytesTransferred = bytes, - statusMessage = "Transferring: $bytes bytes" - ) - } - - override fun onTransferComplete(message: String) { + val err = fileTransferCoordinator?.startTransfer(config) + if (err != null) { _ftState.value = FTProgressState( isActive = false, - isRunning = false, - bytesTransferred = _ftState.value.bytesTransferred, - statusMessage = message, - isError = false - ) - } - - override fun onTransferAborted(error: String) { - _ftState.value = FTProgressState( - isActive = false, - isRunning = false, - bytesTransferred = _ftState.value.bytesTransferred, - statusMessage = "Transfer failed: $error", + statusMessage = "Error: $err", isError = true ) } - }) + withContext(Dispatchers.Main) { + onResult(err) + } + } catch (e: Exception) { + log.warning("Error in startFileTransfer: ${e.message}") + _ftState.value = FTProgressState( + isActive = false, + statusMessage = "Transfer error: ${e.message}", + isError = true + ) + withContext(Dispatchers.Main) { + onResult(e.message) + } + } } - - val err = fileTransferCoordinator?.startTransfer(config) - if (err != null) { - _ftState.value = FTProgressState( - isActive = false, - statusMessage = "Error: $err", - isError = true - ) - } - return err } fun cancelFileTransfer() { - fileTransferCoordinator?.cancel() + viewModelScope.launch(Dispatchers.IO) { + try { + fileTransferCoordinator?.cancel() + } catch (e: Exception) { + log.warning("Error cancelling transfer: ${e.message}") + } + } } fun typeChar(ch: Char) { diff --git a/src/main/java/org/pubvm/a3270/storage/AppSettings.kt b/src/main/java/org/pubvm/a3270/storage/AppSettings.kt index 2ba02f2..e618470 100644 --- a/src/main/java/org/pubvm/a3270/storage/AppSettings.kt +++ b/src/main/java/org/pubvm/a3270/storage/AppSettings.kt @@ -7,6 +7,7 @@ object AppSettings { private const val PREFS_NAME = "a3270_settings" private const val KEY_MASK_HIDDEN_INPUT = "mask_hidden_input" private const val KEY_CURSOR_BLINK = "cursor_blink" + private const val KEY_HAPTIC_FEEDBACK = "haptic_feedback" private fun getPrefs(context: Context): SharedPreferences { return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -27,4 +28,12 @@ object AppSettings { fun setCursorBlinkEnabled(context: Context, enabled: Boolean) { getPrefs(context).edit().putBoolean(KEY_CURSOR_BLINK, enabled).apply() } + + fun isHapticFeedbackEnabled(context: Context): Boolean { + return getPrefs(context).getBoolean(KEY_HAPTIC_FEEDBACK, true) + } + + fun setHapticFeedbackEnabled(context: Context, enabled: Boolean) { + getPrefs(context).edit().putBoolean(KEY_HAPTIC_FEEDBACK, enabled).apply() + } } diff --git a/src/main/java/org/pubvm/a3270/ui/FunctionKeyBar.kt b/src/main/java/org/pubvm/a3270/ui/FunctionKeyBar.kt index 75efd97..bad2f66 100644 --- a/src/main/java/org/pubvm/a3270/ui/FunctionKeyBar.kt +++ b/src/main/java/org/pubvm/a3270/ui/FunctionKeyBar.kt @@ -1,20 +1,20 @@ package org.pubvm.a3270.ui +import android.view.HapticFeedbackConstants import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.focus.focusProperties import org.lib3270j.ConnectionState import org.lib3270j.protocol.DS3270Constants.* @@ -22,6 +22,7 @@ import org.lib3270j.protocol.DS3270Constants.* fun TwoRowKeyBar( connectionState: ConnectionState, isShiftPressed: Boolean = false, + hapticFeedbackEnabled: Boolean = true, onClearShift: () -> Unit = {}, onConnectClick: () -> Unit, onDisconnectClick: () -> Unit, @@ -58,6 +59,7 @@ fun TwoRowKeyBar( label = "⋮", color = Color(0xFF343A40), modifier = Modifier.fillMaxWidth(), + hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { menuExpanded = true } ) @@ -106,6 +108,7 @@ fun TwoRowKeyBar( label = tabLabel, color = Color(0xFF1C7ED6), modifier = Modifier.weight(1f), + hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { if (isShiftPressed) { @@ -118,34 +121,34 @@ fun TwoRowKeyBar( ) // 3. RESET - KeyButton("RESET", Color(0xFFE67700), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onReset) + KeyButton("RESET", Color(0xFFE67700), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onReset) // 4. ENTER - KeyButton("ENTER", Color(0xFF2B8A3E), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_ENTER) }) + KeyButton("ENTER", Color(0xFF2B8A3E), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_ENTER) }) // 5. CLEAR - KeyButton("CLEAR", Color(0xFFC92A2A), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_CLEAR) }) + KeyButton("CLEAR", Color(0xFFC92A2A), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_CLEAR) }) // 6. PA1 - KeyButton("PA1", Color(0xFF495057), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA1) }) + KeyButton("PA1", Color(0xFF495057), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA1) }) // 7. PA2 - KeyButton("PA2", Color(0xFF495057), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA2) }) + KeyButton("PA2", Color(0xFF495057), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA2) }) // 8. PA3 - KeyButton("PA3", Color(0xFF495057), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA3) }) + KeyButton("PA3", Color(0xFF495057), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA3) }) // 9. Left Navigation - KeyButton("←", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorLeft) + KeyButton("←", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorLeft) // 10. Up Navigation - KeyButton("↑", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorUp) + KeyButton("↑", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorUp) // 11. Down Navigation - KeyButton("↓", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorDown) + KeyButton("↓", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorDown) // 12. Right Navigation - KeyButton("→", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorRight) + KeyButton("→", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorRight) } Spacer(modifier = Modifier.height(1.dp).fillMaxWidth().background(Color(0xFF2C2D30))) @@ -174,6 +177,7 @@ fun TwoRowKeyBar( label = "F$i", color = Color(0xFF364FC7), modifier = Modifier.weight(1f), + hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(aid) @@ -193,10 +197,17 @@ private fun KeyButton( color: Color, onClick: () -> Unit, modifier: Modifier = Modifier, + hapticFeedbackEnabled: Boolean = true, innerPaddingHorizontal: androidx.compose.ui.unit.Dp = 0.dp ) { + val view = LocalView.current Surface( - onClick = onClick, + onClick = { + if (hapticFeedbackEnabled) { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + } + onClick() + }, shape = RoundedCornerShape(4.dp), color = color, shadowElevation = 1.dp, diff --git a/src/main/java/org/pubvm/a3270/ui/SettingsDialog.kt b/src/main/java/org/pubvm/a3270/ui/SettingsDialog.kt index 0c84919..15488b6 100644 --- a/src/main/java/org/pubvm/a3270/ui/SettingsDialog.kt +++ b/src/main/java/org/pubvm/a3270/ui/SettingsDialog.kt @@ -1,7 +1,9 @@ package org.pubvm.a3270.ui import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -16,11 +18,13 @@ import androidx.compose.ui.window.Dialog fun SettingsDialog( initialMaskHiddenInput: Boolean, initialCursorBlink: Boolean, + initialHapticFeedback: Boolean = true, onDismiss: () -> Unit, - onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean) -> Unit + onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean, hapticFeedback: Boolean) -> Unit ) { var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) } var cursorBlink by remember { mutableStateOf(initialCursorBlink) } + var hapticFeedback by remember { mutableStateOf(initialHapticFeedback) } Dialog(onDismissRequest = onDismiss) { Card( @@ -33,6 +37,7 @@ fun SettingsDialog( Column( modifier = Modifier .fillMaxWidth() + .verticalScroll(rememberScrollState()) .padding(20.dp) ) { Text( @@ -106,6 +111,39 @@ fun SettingsDialog( ) } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider(color = Color(0xFF2C2D30)) + Spacer(modifier = Modifier.height(16.dp)) + + // Setting 3: Button Haptic Feedback + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) { + Text( + text = "Button Haptic Feedback", + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White + ) + Text( + text = "Vibrate softly when tapping function and navigation buttons to match keyboard tactile feedback.", + fontSize = 12.sp, + color = Color.LightGray + ) + } + Switch( + checked = hapticFeedback, + onCheckedChange = { hapticFeedback = it }, + colors = SwitchDefaults.colors( + checkedThumbColor = Color.White, + checkedTrackColor = Color(0xFF2B8A3E) + ) + ) + } + Spacer(modifier = Modifier.height(24.dp)) // Action Buttons @@ -119,7 +157,7 @@ fun SettingsDialog( Spacer(modifier = Modifier.width(8.dp)) Button( onClick = { - onSave(maskHiddenInput, cursorBlink) + onSave(maskHiddenInput, cursorBlink, hapticFeedback) }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E)) ) {