Hotspot detection debugging
This commit is contained in:
@@ -93,6 +93,10 @@ class HotspotEngine {
|
|||||||
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
|
"(?:===>|=>|-->)\\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) {
|
private fun markOccupied(arr: BooleanArray, start: Int, end: Int) {
|
||||||
val s = start.coerceAtLeast(0)
|
val s = start.coerceAtLeast(0)
|
||||||
val e = end.coerceAtMost(arr.size)
|
val e = end.coerceAtMost(arr.size)
|
||||||
@@ -308,6 +312,33 @@ class HotspotEngine {
|
|||||||
hotspots.add(h)
|
hotspots.add(h)
|
||||||
markOccupied(occupied, startCol, endCol)
|
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)) {
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||||
Text("Auto-Detect Menu Choices", fontSize = 13.sp, fontWeight = FontWeight.Medium, color = Color.White)
|
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(
|
Switch(
|
||||||
checked = hotspotsMenuChoicesEnabled,
|
checked = hotspotsMenuChoicesEnabled,
|
||||||
|
|||||||
@@ -205,6 +205,17 @@ fun TerminalView(
|
|||||||
if (h.type == HotspotEngine.HotspotType.URL) {
|
if (h.type == HotspotEngine.HotspotType.URL) {
|
||||||
onOpenUrl?.invoke(h.action)
|
onOpenUrl?.invoke(h.action)
|
||||||
handledHotspot = true
|
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 {
|
} else {
|
||||||
onSendHotspotAction?.invoke(h.action)
|
onSendHotspotAction?.invoke(h.action)
|
||||||
handledHotspot = true
|
handledHotspot = true
|
||||||
|
|||||||
@@ -54,6 +54,105 @@ class HotspotAndHistoryFeaturesTest {
|
|||||||
assertTrue(enterHotspots.any { it.action == "[enter]" })
|
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
|
@Test
|
||||||
fun testHotspotHitTesting() {
|
fun testHotspotHitTesting() {
|
||||||
val engine = HotspotEngine()
|
val engine = HotspotEngine()
|
||||||
@@ -192,4 +291,22 @@ class HotspotAndHistoryFeaturesTest {
|
|||||||
assertEquals(true, hsPf)
|
assertEquals(true, hsPf)
|
||||||
assertEquals(false, hsChoices)
|
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