Hotspot detection debugging
This commit is contained in:
@@ -93,6 +93,10 @@ class HotspotEngine {
|
||||
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
|
||||
)
|
||||
|
||||
private val ISPF_MENU_PATTERN = Pattern.compile(
|
||||
"(?:^\\s*|(?<=\\s{3,}))(?:(\\d{1,2}(?:\\.\\d{1,2})?)(?:\\s*[-–—]\\s*|\\s+)|([A-Za-z])(?:\\s*[-–—]\\s*|\\s{2,}))([A-Za-z][A-Za-z0-9/_-]*)"
|
||||
)
|
||||
|
||||
private fun markOccupied(arr: BooleanArray, start: Int, end: Int) {
|
||||
val s = start.coerceAtLeast(0)
|
||||
val e = end.coerceAtMost(arr.size)
|
||||
@@ -308,6 +312,33 @@ class HotspotEngine {
|
||||
hotspots.add(h)
|
||||
markOccupied(occupied, startCol, endCol)
|
||||
}
|
||||
|
||||
// 3c. Scan ISPF Menu choices (e.g. " 1 View", " 2 Edit", "10 SCLM", " X Exit", " M More", " 1 - View", " 3.1 Library")
|
||||
val mIspf = ISPF_MENU_PATTERN.matcher(line)
|
||||
while (mIspf.find()) {
|
||||
val choiceStr = mIspf.group(1) ?: mIspf.group(2)?.uppercase() ?: continue
|
||||
val labelStr = mIspf.group(3) ?: continue
|
||||
val startCol = if (mIspf.group(1) != null) mIspf.start(1) else mIspf.start(2)
|
||||
val endCol = mIspf.end(3)
|
||||
if (isRangeOccupied(occupied, startCol, endCol)) continue
|
||||
|
||||
val action = "$choiceStr[enter]"
|
||||
val tooltip = "Select option $choiceStr ($labelStr)"
|
||||
val display = line.substring(startCol, endCol)
|
||||
|
||||
val h = Hotspot(
|
||||
type = HotspotType.MENU_CHOICE,
|
||||
startRow = r,
|
||||
startCol = startCol,
|
||||
endRow = r,
|
||||
endCol = endCol - 1,
|
||||
displayText = display,
|
||||
action = action,
|
||||
tooltip = tooltip
|
||||
)
|
||||
hotspots.add(h)
|
||||
markOccupied(occupied, startCol, endCol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -441,7 +441,7 @@ fun SettingsDialog(
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text("Auto-Detect Menu Choices", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = Color.White)
|
||||
Text("\"1.\", \"2.\", \"===> 3\", \"=> 4\"", fontSize = 11.sp, color = Color.LightGray)
|
||||
Text("ISPF, \"1.\", \"2.\", \"===> 3\", \"=> 4\"", fontSize = 11.sp, color = Color.LightGray)
|
||||
}
|
||||
Switch(
|
||||
checked = hotspotsMenuChoicesEnabled,
|
||||
|
||||
@@ -205,6 +205,17 @@ fun TerminalView(
|
||||
if (h.type == HotspotEngine.HotspotType.URL) {
|
||||
onOpenUrl?.invoke(h.action)
|
||||
handledHotspot = true
|
||||
} else if (h.type == HotspotEngine.HotspotType.MENU_CHOICE) {
|
||||
// In 3270 / ISPF, clicking/tapping a menu choice hotspot positions the cursor
|
||||
// on a protected screen label. Typing directly into a protected cell triggers
|
||||
// INHIBIT_PROTECTED_FIELD keyboard lock. Menu choices must be submitted via
|
||||
// the command/option line (first unprotected field reachable via [home]).
|
||||
var action = h.action
|
||||
if (!action.startsWith("[home]")) {
|
||||
action = "[home][eraseeof]$action"
|
||||
}
|
||||
onSendHotspotAction?.invoke(action)
|
||||
handledHotspot = true
|
||||
} else {
|
||||
onSendHotspotAction?.invoke(h.action)
|
||||
handledHotspot = true
|
||||
|
||||
@@ -54,6 +54,105 @@ class HotspotAndHistoryFeaturesTest {
|
||||
assertTrue(enterHotspots.any { it.action == "[enter]" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIspfPrimaryOptionMenuDetection() {
|
||||
val engine = HotspotEngine()
|
||||
val screen =
|
||||
" Menu Utilities Compilers Options Status Help\n" +
|
||||
" ──────────────────────────────────────────────────────────────────────────────\n" +
|
||||
" ISPF Primary Option Menu\n" +
|
||||
" Option ===>\n" +
|
||||
" \n" +
|
||||
" 0 Settings Terminal and user parameters User ID . : RUDI\n" +
|
||||
" 1 View Display source data or listings Time. . . : 12:31\n" +
|
||||
" 2 Edit Create or change source data Terminal. : 3278\n" +
|
||||
" 3 Utilities Perform utility functions Screen. . : 1\n" +
|
||||
" 4 Foreground Interactive language processing Language. : ENGLISH\n" +
|
||||
" 5 Batch Submit job for language processing Appl ID . : ISR\n" +
|
||||
" 6 Command Enter TSO or Workstation commands TSO logon : DEVPROC\n" +
|
||||
" 7 Dialog Test Perform dialog testing TSO prefix: RUDI\n" +
|
||||
" 9 IBM Products IBM program development products System ID : VS01\n" +
|
||||
" 10 SCLM SW Configuration Library Manager Release . : ISPF 7.5\n" +
|
||||
" 11 Workplace ISPF Object/Action Workplace\n" +
|
||||
" M More Additional workplace options\n" +
|
||||
" X Exit Terminate ISPF using log and list defaults"
|
||||
|
||||
val hotspots = engine.scanText(screen, 18, 80)
|
||||
|
||||
// 14 total hotspots: 13 ISPF menu choices (0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, M, X) + 1 [ENTER] key on row 11
|
||||
assertEquals(14, hotspots.size)
|
||||
val menuChoices = hotspots.filter { it.type == HotspotType.MENU_CHOICE }
|
||||
assertEquals(13, menuChoices.size)
|
||||
|
||||
// Option 0
|
||||
val h0 = menuChoices[0]
|
||||
assertEquals(HotspotType.MENU_CHOICE, h0.type)
|
||||
assertEquals("0 Settings", h0.displayText)
|
||||
assertEquals("0[enter]", h0.action)
|
||||
assertEquals("Select option 0 (Settings)", h0.tooltip)
|
||||
assertEquals(5, h0.startRow)
|
||||
|
||||
// Option 1
|
||||
val h1 = menuChoices[1]
|
||||
assertEquals("1 View", h1.displayText)
|
||||
assertEquals("1[enter]", h1.action)
|
||||
assertEquals("Select option 1 (View)", h1.tooltip)
|
||||
assertEquals(6, h1.startRow)
|
||||
|
||||
// Option 2
|
||||
val h2 = menuChoices[2]
|
||||
assertEquals("2 Edit", h2.displayText)
|
||||
assertEquals("2[enter]", h2.action)
|
||||
assertEquals("Select option 2 (Edit)", h2.tooltip)
|
||||
|
||||
// Option 10 (two-digit, index 9 after 0,1,2,3,4,5,6,7,9)
|
||||
val h10 = menuChoices[9]
|
||||
assertEquals("10 SCLM", h10.displayText)
|
||||
assertEquals("10[enter]", h10.action)
|
||||
assertEquals("Select option 10 (SCLM)", h10.tooltip)
|
||||
|
||||
// Option M (letter, index 11)
|
||||
val hM = menuChoices[11]
|
||||
assertEquals("M More", hM.displayText)
|
||||
assertEquals("M[enter]", hM.action)
|
||||
assertEquals("Select option M (More)", hM.tooltip)
|
||||
|
||||
// Option X (exit, index 12)
|
||||
val hX = menuChoices[12]
|
||||
assertEquals("X Exit", hX.displayText)
|
||||
assertEquals("X[enter]", hX.action)
|
||||
assertEquals("Select option X (Exit)", hX.tooltip)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIspfSubOptionsAndMultiColumn() {
|
||||
val engine = HotspotEngine()
|
||||
val screen =
|
||||
" 3.1 Library - Compress or print data set\n" +
|
||||
" 3.2 DataSet - Allocate, rename, or delete\n" +
|
||||
" 1 - View 2 - Edit"
|
||||
|
||||
val hotspots = engine.scanText(screen, 3, 80)
|
||||
assertEquals(4, hotspots.size)
|
||||
|
||||
val h31 = hotspots[0]
|
||||
assertEquals("3.1 Library", h31.displayText)
|
||||
assertEquals("3.1[enter]", h31.action)
|
||||
assertEquals("Select option 3.1 (Library)", h31.tooltip)
|
||||
|
||||
val h32 = hotspots[1]
|
||||
assertEquals("3.2 DataSet", h32.displayText)
|
||||
assertEquals("3.2[enter]", h32.action)
|
||||
|
||||
val h1 = hotspots[2]
|
||||
assertEquals("1 - View", h1.displayText)
|
||||
assertEquals("1[enter]", h1.action)
|
||||
|
||||
val h2 = hotspots[3]
|
||||
assertEquals("2 - Edit", h2.displayText)
|
||||
assertEquals("2[enter]", h2.action)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHotspotHitTesting() {
|
||||
val engine = HotspotEngine()
|
||||
@@ -192,4 +291,22 @@ class HotspotAndHistoryFeaturesTest {
|
||||
assertEquals(true, hsPf)
|
||||
assertEquals(false, hsChoices)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMenuChoiceActionPrefixingForSafeSubmission() {
|
||||
val engine = HotspotEngine()
|
||||
val screen = " 1 View Display source data or listings"
|
||||
val hotspots = engine.scanText(screen, 1, 80)
|
||||
assertEquals(1, hotspots.size)
|
||||
val h = hotspots[0]
|
||||
assertEquals(HotspotType.MENU_CHOICE, h.type)
|
||||
assertEquals("1[enter]", h.action)
|
||||
|
||||
// Menu choices must be submitted via [home][eraseeof] to prevent protected field keyboard lock
|
||||
var safeAction = h.action
|
||||
if (!safeAction.startsWith("[home]")) {
|
||||
safeAction = "[home][eraseeof]$safeAction"
|
||||
}
|
||||
assertEquals("[home][eraseeof]1[enter]", safeAction)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user