Hotspot detection debugging
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m29s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m46s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 2m8s
Release j3270 / Build & Publish Release (push) Successful in 1m39s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m29s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m46s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 2m8s
Release j3270 / Build & Publish Release (push) Successful in 1m39s
This commit is contained in:
@@ -129,7 +129,7 @@ public class HotspotEngine {
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
// 3. Menu choices: "1.", "2.", "===> 3", "=> 4", "--> 5"
|
||||
// 3. Menu choices: "1.", "2.", "===> 3", "=> 4", "--> 5", ISPF (" 0 Settings", " 1 View", "10 SCLM", " X Exit")
|
||||
private static final Pattern MENU_NUMBER_PATTERN = Pattern.compile(
|
||||
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
|
||||
);
|
||||
@@ -138,6 +138,10 @@ public class HotspotEngine {
|
||||
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
|
||||
);
|
||||
|
||||
private static final Pattern 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/_-]*)"
|
||||
);
|
||||
|
||||
// Caching state to avoid rescanning unchanged screens
|
||||
private String cachedScreenText = "";
|
||||
private int cachedRows = 0;
|
||||
@@ -369,6 +373,28 @@ public class HotspotEngine {
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
|
||||
// Pattern 3c: ISPF menu choices (e.g. " 1 View", " 2 Edit", "10 SCLM", " X Exit", " M More", " 1 - View", " 3.1 Library")
|
||||
Matcher mIspf = ISPF_MENU_PATTERN.matcher(line);
|
||||
while (mIspf.find()) {
|
||||
String choiceStr = mIspf.group(1) != null ? mIspf.group(1) : mIspf.group(2).toUpperCase();
|
||||
String labelStr = mIspf.group(3);
|
||||
int startCol = mIspf.group(1) != null ? mIspf.start(1) : mIspf.start(2);
|
||||
int endCol = mIspf.end(3);
|
||||
if (isRangeOccupied(occupied, startCol, endCol)) continue;
|
||||
|
||||
String action = choiceStr + "[enter]";
|
||||
String tooltip = "Select option " + choiceStr + " (" + labelStr + ")";
|
||||
String display = line.substring(startCol, endCol);
|
||||
|
||||
Hotspot h = new Hotspot(
|
||||
HotspotType.MENU_CHOICE,
|
||||
r, startCol, r, endCol - 1,
|
||||
display, action, tooltip
|
||||
);
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -533,7 +533,7 @@ public class SettingsDialog extends JDialog {
|
||||
gbcH.gridy = 2;
|
||||
hotGroup.add(hotspotsPfCheck, gbcH);
|
||||
|
||||
hotspotsMenuCheck = new JCheckBox("Auto-detect Menu choices (\"1.\", \"2.\", \"===> 3\")", Settings.getHotspotsMenuChoicesEnabled());
|
||||
hotspotsMenuCheck = new JCheckBox("Auto-detect Menu choices (ISPF, \"1.\", \"===> 3\")", Settings.getHotspotsMenuChoicesEnabled());
|
||||
ThemeManager.styleCheckBox(hotspotsMenuCheck);
|
||||
gbcH.gridy = 3;
|
||||
hotGroup.add(hotspotsMenuCheck, gbcH);
|
||||
|
||||
@@ -533,6 +533,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I
|
||||
if (h.getType() == HotspotEngine.HotspotType.URL) {
|
||||
openUrl(h.getAction());
|
||||
return;
|
||||
} else if (h.getType() == HotspotEngine.HotspotType.MENU_CHOICE) {
|
||||
// In 3270 / ISPF, clicking on 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]).
|
||||
String action = h.getAction();
|
||||
if (!action.startsWith("[home]")) {
|
||||
action = "[home][eraseeof]" + action;
|
||||
}
|
||||
client.getPS().SendKeys(action);
|
||||
refreshScreen();
|
||||
return;
|
||||
} else {
|
||||
client.getPS().SendKeys(h.getAction());
|
||||
refreshScreen();
|
||||
|
||||
@@ -189,4 +189,106 @@ public class HotspotEngineTest {
|
||||
List<HotspotEngine.Hotspot> empty = engine.scanText(screen, 1, 80);
|
||||
assertEquals(0, empty.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Detects ISPF primary option menu choices (numbers, two-digit, and letters)")
|
||||
public void testIspfPrimaryOptionMenuDetection() {
|
||||
String 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";
|
||||
|
||||
List<HotspotEngine.Hotspot> 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());
|
||||
List<HotspotEngine.Hotspot> menuChoices = hotspots.stream()
|
||||
.filter(h -> h.getType() == HotspotEngine.HotspotType.MENU_CHOICE)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
assertEquals(13, menuChoices.size());
|
||||
|
||||
// Option 0
|
||||
HotspotEngine.Hotspot h0 = menuChoices.get(0);
|
||||
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, h0.getType());
|
||||
assertEquals("0 Settings", h0.getDisplayText());
|
||||
assertEquals("0[enter]", h0.getAction());
|
||||
assertEquals("Select option 0 (Settings)", h0.getTooltip());
|
||||
assertEquals(5, h0.getStartRow());
|
||||
|
||||
// Option 1
|
||||
HotspotEngine.Hotspot h1 = menuChoices.get(1);
|
||||
assertEquals("1 View", h1.getDisplayText());
|
||||
assertEquals("1[enter]", h1.getAction());
|
||||
assertEquals("Select option 1 (View)", h1.getTooltip());
|
||||
assertEquals(6, h1.getStartRow());
|
||||
|
||||
// Option 2
|
||||
HotspotEngine.Hotspot h2 = menuChoices.get(2);
|
||||
assertEquals("2 Edit", h2.getDisplayText());
|
||||
assertEquals("2[enter]", h2.getAction());
|
||||
assertEquals("Select option 2 (Edit)", h2.getTooltip());
|
||||
|
||||
// Option 10 (two-digit, index 9 after 0,1,2,3,4,5,6,7,9)
|
||||
HotspotEngine.Hotspot h10 = menuChoices.get(9);
|
||||
assertEquals("10 SCLM", h10.getDisplayText());
|
||||
assertEquals("10[enter]", h10.getAction());
|
||||
assertEquals("Select option 10 (SCLM)", h10.getTooltip());
|
||||
|
||||
// Option M (letter, index 11)
|
||||
HotspotEngine.Hotspot hM = menuChoices.get(11);
|
||||
assertEquals("M More", hM.getDisplayText());
|
||||
assertEquals("M[enter]", hM.getAction());
|
||||
assertEquals("Select option M (More)", hM.getTooltip());
|
||||
|
||||
// Option X (exit, index 12)
|
||||
HotspotEngine.Hotspot hX = menuChoices.get(12);
|
||||
assertEquals("X Exit", hX.getDisplayText());
|
||||
assertEquals("X[enter]", hX.getAction());
|
||||
assertEquals("Select option X (Exit)", hX.getTooltip());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Detects ISPF dotted sub-options, dashes, and multi-column menus")
|
||||
public void testIspfSubOptionsAndMultiColumn() {
|
||||
String screen =
|
||||
" 3.1 Library - Compress or print data set\n" +
|
||||
" 3.2 DataSet - Allocate, rename, or delete\n" +
|
||||
" 1 - View 2 - Edit";
|
||||
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 3, 80);
|
||||
assertEquals(4, hotspots.size());
|
||||
|
||||
HotspotEngine.Hotspot h31 = hotspots.get(0);
|
||||
assertEquals("3.1 Library", h31.getDisplayText());
|
||||
assertEquals("3.1[enter]", h31.getAction());
|
||||
assertEquals("Select option 3.1 (Library)", h31.getTooltip());
|
||||
|
||||
HotspotEngine.Hotspot h32 = hotspots.get(1);
|
||||
assertEquals("3.2 DataSet", h32.getDisplayText());
|
||||
assertEquals("3.2[enter]", h32.getAction());
|
||||
|
||||
HotspotEngine.Hotspot h1 = hotspots.get(2);
|
||||
assertEquals("1 - View", h1.getDisplayText());
|
||||
assertEquals("1[enter]", h1.getAction());
|
||||
|
||||
HotspotEngine.Hotspot h2 = hotspots.get(3);
|
||||
assertEquals("2 - Edit", h2.getDisplayText());
|
||||
assertEquals("2[enter]", h2.getAction());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -258,4 +258,78 @@ public class TerminalPanelSelectionTest {
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Clicking an ISPF menu choice navigates to home/option line, types choice, and sends AID_ENTER")
|
||||
public void testClickOnIspfMenuChoiceSubmitsViaHomeAndEnter() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Row 3 (col 0): Protected label "Option ===> "
|
||||
int row3Start = 3 * 80;
|
||||
sb.setCellFA(row3Start, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
String promptText = "Option ===> ";
|
||||
for (int i = 0; i < promptText.length(); i++) {
|
||||
sb.getCell(row3Start + 1 + i).ucs4 = promptText.charAt(i);
|
||||
sb.getCell(row3Start + 1 + i).ec = (byte) (0xC1 + i);
|
||||
}
|
||||
|
||||
// Unprotected command input field after "Option ===> " at address 253 (row 3 col 13)
|
||||
int inputFieldAddr = row3Start + 13;
|
||||
sb.setCellFA(inputFieldAddr, (byte) FA_PRINTABLE); // Unprotected!
|
||||
for (int i = 0; i < 20; i++) {
|
||||
sb.getCell(inputFieldAddr + 1 + i).ucs4 = ' ';
|
||||
sb.getCell(inputFieldAddr + 1 + i).ec = (byte) 0x40;
|
||||
}
|
||||
|
||||
// Row 6 (address 480): Protected field with ISPF menu option " 1 View"
|
||||
int row6Start = 6 * 80;
|
||||
sb.setCellFA(row6Start, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
String menuText = " 1 View Display source data or listings";
|
||||
for (int i = 0; i < menuText.length(); i++) {
|
||||
sb.getCell(row6Start + 1 + i).ucs4 = menuText.charAt(i);
|
||||
sb.getCell(row6Start + 1 + i).ec = (byte) (menuText.charAt(i) == ' ' ? 0x40 : 0xC1 + i);
|
||||
}
|
||||
|
||||
sb.updateFormatted();
|
||||
sb.updateDisplaySnapshot();
|
||||
|
||||
panel.setClient(client);
|
||||
panel.setHotspotsDirty(true);
|
||||
|
||||
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
|
||||
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int oy = panel.getRenderOffsetY();
|
||||
|
||||
MouseListener ml = panel.getMouseListeners()[0];
|
||||
|
||||
// Click at row 6, col 2 (on the "1" in " 1 View")
|
||||
int clickX = ox + 2 * cellW + cellW / 2;
|
||||
int clickY = oy + 6 * cellH + cellH / 2;
|
||||
|
||||
ml.mousePressed(new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
|
||||
ml.mouseReleased(new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1));
|
||||
|
||||
// AID_ENTER must have been transmitted
|
||||
assertEquals(AID_ENTER, client.getInputProcessor().getLastAid(), "Clicking menu choice must transmit AID_ENTER");
|
||||
|
||||
// Keyboard lock should be standard system lock awaiting host response, NOT protected field error
|
||||
assertNotEquals(com.ibm.eNetwork.ECL.ECLConstants.INHIBIT_PROTECTED_FIELD, client.getOIA().getInputInhibited(),
|
||||
"Keyboard must not be locked with protected field violation");
|
||||
|
||||
// Option "1" must have been typed into the unprotected command input field (address inputFieldAddr + 1)
|
||||
assertEquals('1', (char) sb.getCell(inputFieldAddr + 1).ucs4, "Option '1' must be placed on the command/option line");
|
||||
} catch (HeadlessException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user