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
|
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(
|
private static final Pattern MENU_NUMBER_PATTERN = Pattern.compile(
|
||||||
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
|
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
|
||||||
);
|
);
|
||||||
@@ -138,6 +138,10 @@ public class HotspotEngine {
|
|||||||
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
|
"(?:===>|=>|-->)\\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
|
// Caching state to avoid rescanning unchanged screens
|
||||||
private String cachedScreenText = "";
|
private String cachedScreenText = "";
|
||||||
private int cachedRows = 0;
|
private int cachedRows = 0;
|
||||||
@@ -369,6 +373,28 @@ public class HotspotEngine {
|
|||||||
hotspots.add(h);
|
hotspots.add(h);
|
||||||
markOccupied(occupied, startCol, endCol);
|
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;
|
gbcH.gridy = 2;
|
||||||
hotGroup.add(hotspotsPfCheck, gbcH);
|
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);
|
ThemeManager.styleCheckBox(hotspotsMenuCheck);
|
||||||
gbcH.gridy = 3;
|
gbcH.gridy = 3;
|
||||||
hotGroup.add(hotspotsMenuCheck, gbcH);
|
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) {
|
if (h.getType() == HotspotEngine.HotspotType.URL) {
|
||||||
openUrl(h.getAction());
|
openUrl(h.getAction());
|
||||||
return;
|
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 {
|
} else {
|
||||||
client.getPS().SendKeys(h.getAction());
|
client.getPS().SendKeys(h.getAction());
|
||||||
refreshScreen();
|
refreshScreen();
|
||||||
|
|||||||
@@ -189,4 +189,106 @@ public class HotspotEngineTest {
|
|||||||
List<HotspotEngine.Hotspot> empty = engine.scanText(screen, 1, 80);
|
List<HotspotEngine.Hotspot> empty = engine.scanText(screen, 1, 80);
|
||||||
assertEquals(0, empty.size());
|
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) {
|
} 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