Clear cruft
This commit is contained in:
@@ -70,13 +70,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
setLocationRelativeTo(null);
|
||||
setMinimumSize(new Dimension(640, 400));
|
||||
|
||||
// Status refresh timer — also ensures focus stays on terminal
|
||||
// Status refresh timer — also ensures focus stays on terminal (guarding against focus stealing)
|
||||
refreshTimer = new Timer(100, e -> {
|
||||
if (client != null) {
|
||||
statusBar.updateStatus();
|
||||
syncModeMenuItems();
|
||||
if (isActive() && !terminalPanel.hasFocus()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
if (!isAnyChildDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) {
|
||||
if (!terminalPanel.hasFocus()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -96,11 +98,30 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
@Override
|
||||
public void windowActivated(WindowEvent e) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
if (!isAnyChildDialogActive()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isAnyChildDialogActive() {
|
||||
Window focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
|
||||
if (focusedWindow != null && focusedWindow != this) {
|
||||
return true;
|
||||
}
|
||||
if (findDialog != null && findDialog.isVisible()) return true;
|
||||
if (scriptDialog != null && scriptDialog.isVisible()) return true;
|
||||
if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
|
||||
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
|
||||
for (Window owned : getOwnedWindows()) {
|
||||
if (owned != null && owned.isVisible()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
terminalPanel = new TerminalPanel();
|
||||
statusBar = new StatusBar();
|
||||
@@ -743,13 +764,26 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
fileTransfer.cancel();
|
||||
fileTransfer = null;
|
||||
}
|
||||
client.disconnect();
|
||||
Telnet3270Client c = client;
|
||||
client = null;
|
||||
terminalPanel.setClient(null);
|
||||
statusBar.setClient(null, terminalPanel);
|
||||
syncModeMenuItems();
|
||||
terminalPanel.repaint();
|
||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||
new Thread(() -> {
|
||||
try {
|
||||
c.disconnect();
|
||||
} catch (Exception ignored) {}
|
||||
}, "Disconnect-Thread").start();
|
||||
|
||||
Runnable uiReset = () -> {
|
||||
terminalPanel.setClient(null);
|
||||
statusBar.setClient(null, terminalPanel);
|
||||
syncModeMenuItems();
|
||||
terminalPanel.repaint();
|
||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||
};
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
uiReset.run();
|
||||
} else {
|
||||
SwingUtilities.invokeLater(uiReset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,7 +824,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
statusBar.updateStatus();
|
||||
terminalPanel.repaint();
|
||||
|
||||
if (newState.isFullSession() && !oldState.isFullSession()) {
|
||||
if (newState == ConnectionState.RECONNECTING) {
|
||||
setTitle("j3270 — " + lastHost + ":" + lastPort + " [Reconnecting...]");
|
||||
} else if (newState == ConnectionState.NOT_CONNECTED) {
|
||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
||||
} else if (newState.isFullSession() && !oldState.isFullSession()) {
|
||||
String tlsIndicator = (client != null && client.getConfig().isUseTls()) ?
|
||||
(client.getConfig().isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
|
||||
String dev = (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) ?
|
||||
" [" + client.getTelnetFSM().getConnectedLu() + "]" : "";
|
||||
setTitle("j3270 — " + lastHost + ":" + lastPort + tlsIndicator + dev);
|
||||
terminalPanel.guardedPack();
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
@@ -847,6 +890,19 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorMoved(int oldAddress, int newAddress) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
terminalPanel.repaint();
|
||||
statusBar.updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyboardUnlocked() {
|
||||
SwingUtilities.invokeLater(statusBar::updateStatus);
|
||||
}
|
||||
|
||||
// ========== Help dialogs ==========
|
||||
|
||||
private void showKeyMappings() {
|
||||
@@ -949,6 +1005,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
boolean cliTls = false;
|
||||
boolean cliNoVerifyCert = false;
|
||||
Boolean cliTn3270e = null;
|
||||
Boolean cliAutoSysUnlock = null;
|
||||
GraphicsMode cliGraphicsMode = null;
|
||||
String configFile = null;
|
||||
|
||||
@@ -965,6 +1022,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
cliTn3270e = false;
|
||||
} else if ("--tn3270e".equals(arg)) {
|
||||
cliTn3270e = true;
|
||||
} else if ("--auto-sys-unlock".equals(arg)) {
|
||||
cliAutoSysUnlock = true;
|
||||
} else if ("--no-auto-sys-unlock".equals(arg)) {
|
||||
cliAutoSysUnlock = false;
|
||||
} else if (arg.startsWith("--graphics=")) {
|
||||
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
|
||||
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
||||
@@ -1041,6 +1102,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
final boolean finalTls = cliTls;
|
||||
final boolean finalNoVerify = cliNoVerifyCert;
|
||||
final Boolean finalTn3270e = cliTn3270e;
|
||||
final Boolean finalAutoSysUnlock = cliAutoSysUnlock;
|
||||
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
@@ -1077,6 +1139,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
if (finalTn3270e != null) {
|
||||
config.setTn3270eEnabled(finalTn3270e);
|
||||
}
|
||||
if (finalAutoSysUnlock != null) {
|
||||
config.setAutoSysUnlock(finalAutoSysUnlock);
|
||||
} else {
|
||||
config.setAutoSysUnlock(haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
|
||||
}
|
||||
if (finalGraphicsMode != null) {
|
||||
config.setGraphicsMode(finalGraphicsMode);
|
||||
} else {
|
||||
@@ -1100,6 +1167,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
||||
config.setTlsVerifyCert(verify);
|
||||
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
|
||||
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
|
||||
if (finalGraphicsMode != null) {
|
||||
config.setGraphicsMode(finalGraphicsMode);
|
||||
} else {
|
||||
|
||||
@@ -105,6 +105,51 @@ public class Settings {
|
||||
prefs.putBoolean("autoConnectTn3270e", tn3270e);
|
||||
}
|
||||
|
||||
public static boolean getAutoSysUnlock() {
|
||||
return prefs.getBoolean("autoSysUnlock", true);
|
||||
}
|
||||
|
||||
public static void setAutoSysUnlock(boolean autoSysUnlock) {
|
||||
prefs.putBoolean("autoSysUnlock", autoSysUnlock);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getAutoConnectKeepAlive() {
|
||||
return prefs.getBoolean("autoConnectKeepAlive", true);
|
||||
}
|
||||
|
||||
public static void setAutoConnectKeepAlive(boolean keepAlive) {
|
||||
prefs.putBoolean("autoConnectKeepAlive", keepAlive);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static int getAutoConnectKeepAliveInterval() {
|
||||
return prefs.getInt("autoConnectKeepAliveInterval", 120);
|
||||
}
|
||||
|
||||
public static void setAutoConnectKeepAliveInterval(int interval) {
|
||||
prefs.putInt("autoConnectKeepAliveInterval", interval);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getAutoConnectAutoReconnect() {
|
||||
return prefs.getBoolean("autoConnectAutoReconnect", false);
|
||||
}
|
||||
|
||||
public static void setAutoConnectAutoReconnect(boolean autoReconnect) {
|
||||
prefs.putBoolean("autoConnectAutoReconnect", autoReconnect);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static int getAutoConnectReconnectMaxRetries() {
|
||||
return prefs.getInt("autoConnectReconnectMaxRetries", 5);
|
||||
}
|
||||
|
||||
public static void setAutoConnectReconnectMaxRetries(int retries) {
|
||||
prefs.putInt("autoConnectReconnectMaxRetries", retries);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
|
||||
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
|
||||
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
||||
@@ -311,6 +356,26 @@ public class Settings {
|
||||
prefs.putBoolean("blockSelectMode", block);
|
||||
}
|
||||
|
||||
// ========== Clipboard & Tabular Paste ==========
|
||||
|
||||
public static boolean getEnablePasteFromExcel() {
|
||||
return prefs.getBoolean("enablePasteFromExcel", true);
|
||||
}
|
||||
|
||||
public static void setEnablePasteFromExcel(boolean val) {
|
||||
prefs.putBoolean("enablePasteFromExcel", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getPasteStopAtProtectedLine() {
|
||||
return prefs.getBoolean("pasteStopAtProtectedLine", false);
|
||||
}
|
||||
|
||||
public static void setPasteStopAtProtectedLine(boolean val) {
|
||||
prefs.putBoolean("pasteStopAtProtectedLine", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
// ========== Crosshair Ruler ==========
|
||||
|
||||
public static boolean getCrosshairRuler() {
|
||||
@@ -509,11 +574,42 @@ public class Settings {
|
||||
setDynamicCols(Integer.parseInt(value));
|
||||
break;
|
||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
|
||||
case "autoSysUnlock":
|
||||
case "auto_sys_unlock":
|
||||
setAutoSysUnlock(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "enablePasteFromExcel":
|
||||
case "pasteFromExcel":
|
||||
case "excelPaste":
|
||||
setEnablePasteFromExcel(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteStopAtProtectedLine":
|
||||
case "stopAtProtected":
|
||||
case "pasteStopAtProtected":
|
||||
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown behavior/connection key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
case "clipboard":
|
||||
switch (key) {
|
||||
case "enablePasteFromExcel":
|
||||
case "pasteFromExcel":
|
||||
case "excelPaste":
|
||||
setEnablePasteFromExcel(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteStopAtProtectedLine":
|
||||
case "stopAtProtected":
|
||||
case "pasteStopAtProtected":
|
||||
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown clipboard key: " + key);
|
||||
}
|
||||
break;
|
||||
|
||||
case "colors":
|
||||
if (key.startsWith("color_")) {
|
||||
int index = Integer.parseInt(key.substring(6));
|
||||
@@ -633,6 +729,9 @@ public class Settings {
|
||||
w.println("dynamicRows = " + getDynamicRows());
|
||||
w.println("dynamicCols = " + getDynamicCols());
|
||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||
w.println("autoSysUnlock = " + getAutoSysUnlock());
|
||||
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
|
||||
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
|
||||
w.println();
|
||||
|
||||
// [entryassist]
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.graphics.DefaultPixelBuffer;
|
||||
import haus.nightmare.lib3270j.graphics.PixelBuffer;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBuffer;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.awt.image.DirectColorModel;
|
||||
import java.awt.image.Raster;
|
||||
import java.awt.image.SinglePixelPackedSampleModel;
|
||||
import java.awt.image.WritableRaster;
|
||||
|
||||
/**
|
||||
* Desktop Swing/AWT bridge for converting between lib3270j PixelBuffer and java.awt BufferedImage.
|
||||
*/
|
||||
public final class AwtPixelBufferBridge {
|
||||
|
||||
private AwtPixelBufferBridge() {}
|
||||
|
||||
/**
|
||||
* Converts a PixelBuffer to a BufferedImage.
|
||||
* Uses TYPE_INT_ARGB with backed raster for optimal Java2D hardware acceleration.
|
||||
*/
|
||||
public static BufferedImage toBufferedImage(PixelBuffer buffer) {
|
||||
if (buffer == null) return null;
|
||||
int w = buffer.getWidth();
|
||||
int h = buffer.getHeight();
|
||||
if (w <= 0 || h <= 0) {
|
||||
return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
|
||||
}
|
||||
|
||||
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
copyIntoBufferedImage(buffer, img);
|
||||
return img;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies pixels from a PixelBuffer directly into an existing BufferedImage.
|
||||
*/
|
||||
public static void copyIntoBufferedImage(PixelBuffer buffer, BufferedImage target) {
|
||||
if (buffer == null || target == null) return;
|
||||
int w = Math.min(buffer.getWidth(), target.getWidth());
|
||||
int h = Math.min(buffer.getHeight(), target.getHeight());
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
int[] srcPixels = buffer.getPixels();
|
||||
if (srcPixels == null) return;
|
||||
|
||||
if (target.getType() == BufferedImage.TYPE_INT_ARGB || target.getType() == BufferedImage.TYPE_INT_RGB) {
|
||||
if (target.getRaster().getDataBuffer() instanceof DataBufferInt) {
|
||||
int[] dstPixels = ((DataBufferInt) target.getRaster().getDataBuffer()).getData();
|
||||
int srcW = buffer.getWidth();
|
||||
int dstW = target.getWidth();
|
||||
if (srcW == dstW && srcW == w && srcPixels.length >= w * h && dstPixels.length >= w * h) {
|
||||
System.arraycopy(srcPixels, 0, dstPixels, 0, w * h);
|
||||
return;
|
||||
}
|
||||
for (int y = 0; y < h; y++) {
|
||||
System.arraycopy(srcPixels, y * srcW, dstPixels, y * dstW, w);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for custom or incompatible image formats
|
||||
target.setRGB(0, 0, w, h, srcPixels, 0, buffer.getWidth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps or converts a java.awt BufferedImage into a PixelBuffer.
|
||||
*/
|
||||
public static PixelBuffer toPixelBuffer(BufferedImage img) {
|
||||
if (img == null) return null;
|
||||
int w = img.getWidth();
|
||||
int h = img.getHeight();
|
||||
int[] pixels;
|
||||
if ((img.getType() == BufferedImage.TYPE_INT_ARGB || img.getType() == BufferedImage.TYPE_INT_RGB)
|
||||
&& img.getRaster().getDataBuffer() instanceof DataBufferInt) {
|
||||
pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
|
||||
} else {
|
||||
pixels = new int[w * h];
|
||||
img.getRGB(0, 0, w, h, pixels, 0, w);
|
||||
}
|
||||
return new DefaultPixelBuffer(w, h, pixels);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ public class ConnectDialog extends JDialog {
|
||||
private JCheckBox tlsCheckBox;
|
||||
private JCheckBox verifyCertCheckBox;
|
||||
private JCheckBox tn3270eCheckBox;
|
||||
private JCheckBox keepAliveCheckBox;
|
||||
private JCheckBox autoReconnectCheckBox;
|
||||
private boolean confirmed;
|
||||
private ConnectionConfig result;
|
||||
|
||||
@@ -246,6 +248,24 @@ public class ConnectDialog extends JDialog {
|
||||
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
|
||||
mainPanel.add(tn3270eCheckBox, gbc);
|
||||
|
||||
// Keep-Alive Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 10;
|
||||
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
|
||||
ThemeManager.styleCheckBox(keepAliveCheckBox);
|
||||
keepAliveCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive());
|
||||
mainPanel.add(keepAliveCheckBox, gbc);
|
||||
|
||||
// Auto-Reconnect Checkbox
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 11;
|
||||
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
|
||||
ThemeManager.styleCheckBox(autoReconnectCheckBox);
|
||||
autoReconnectCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
|
||||
mainPanel.add(autoReconnectCheckBox, gbc);
|
||||
|
||||
// Buttons
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
|
||||
|
||||
@@ -266,7 +286,7 @@ public class ConnectDialog extends JDialog {
|
||||
buttonPanel.add(connectBtn);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 10;
|
||||
gbc.gridy = 12;
|
||||
gbc.gridwidth = 2;
|
||||
mainPanel.add(buttonPanel, gbc);
|
||||
|
||||
@@ -328,6 +348,18 @@ public class ConnectDialog extends JDialog {
|
||||
result.setUseTls(tlsCheckBox.isSelected());
|
||||
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
||||
result.setTn3270eEnabled(tn3270eCheckBox.isSelected());
|
||||
result.setAutoSysUnlock(haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
|
||||
|
||||
boolean ka = keepAliveCheckBox.isSelected();
|
||||
result.setKeepAliveEnabled(ka);
|
||||
result.setKeepAliveIntervalSeconds(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAliveInterval());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectKeepAlive(ka);
|
||||
|
||||
boolean ar = autoReconnectCheckBox.isSelected();
|
||||
result.setAutoReconnect(ar);
|
||||
result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar);
|
||||
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Keyboard mapping profile (.kmp) file importer and keybinding utility.
|
||||
* Parses keyboard mapping profile files, translates key scan codes and mnemonics
|
||||
* to j3270 action handlers.
|
||||
*/
|
||||
public class KeyBindings {
|
||||
|
||||
private static final Logger log = Logger.getLogger(KeyBindings.class.getName());
|
||||
|
||||
// IBM scan code to standard Java KeyStroke key name mapping
|
||||
private static final Map<Integer, String> SCAN_CODE_MAP = new HashMap<>();
|
||||
|
||||
// Mnemonic to j3270 action mapping
|
||||
private static final Map<String, String> MNEMONIC_MAP = new HashMap<>();
|
||||
|
||||
static {
|
||||
// Alphanumeric and symbol keys
|
||||
SCAN_CODE_MAP.put(1, "BACK_QUOTE");
|
||||
SCAN_CODE_MAP.put(2, "1");
|
||||
SCAN_CODE_MAP.put(3, "2");
|
||||
SCAN_CODE_MAP.put(4, "3");
|
||||
SCAN_CODE_MAP.put(5, "4");
|
||||
SCAN_CODE_MAP.put(6, "5");
|
||||
SCAN_CODE_MAP.put(7, "6");
|
||||
SCAN_CODE_MAP.put(8, "7");
|
||||
SCAN_CODE_MAP.put(9, "8");
|
||||
SCAN_CODE_MAP.put(10, "9");
|
||||
SCAN_CODE_MAP.put(11, "0");
|
||||
SCAN_CODE_MAP.put(12, "MINUS");
|
||||
SCAN_CODE_MAP.put(13, "EQUALS");
|
||||
SCAN_CODE_MAP.put(14, "BACK_SPACE");
|
||||
SCAN_CODE_MAP.put(15, "TAB");
|
||||
SCAN_CODE_MAP.put(16, "Q");
|
||||
SCAN_CODE_MAP.put(17, "W");
|
||||
SCAN_CODE_MAP.put(18, "E");
|
||||
SCAN_CODE_MAP.put(19, "R");
|
||||
SCAN_CODE_MAP.put(20, "T");
|
||||
SCAN_CODE_MAP.put(21, "Y");
|
||||
SCAN_CODE_MAP.put(22, "U");
|
||||
SCAN_CODE_MAP.put(23, "I");
|
||||
SCAN_CODE_MAP.put(24, "O");
|
||||
SCAN_CODE_MAP.put(25, "P");
|
||||
SCAN_CODE_MAP.put(26, "OPEN_BRACKET");
|
||||
SCAN_CODE_MAP.put(27, "CLOSE_BRACKET");
|
||||
SCAN_CODE_MAP.put(28, "BACK_SLASH");
|
||||
SCAN_CODE_MAP.put(29, "CAPS_LOCK");
|
||||
SCAN_CODE_MAP.put(30, "A");
|
||||
SCAN_CODE_MAP.put(31, "S");
|
||||
SCAN_CODE_MAP.put(32, "D");
|
||||
SCAN_CODE_MAP.put(33, "F");
|
||||
SCAN_CODE_MAP.put(34, "G");
|
||||
SCAN_CODE_MAP.put(35, "H");
|
||||
SCAN_CODE_MAP.put(36, "J");
|
||||
SCAN_CODE_MAP.put(37, "K");
|
||||
SCAN_CODE_MAP.put(38, "L");
|
||||
SCAN_CODE_MAP.put(39, "SEMICOLON");
|
||||
SCAN_CODE_MAP.put(40, "QUOTE");
|
||||
SCAN_CODE_MAP.put(41, "BACK_QUOTE");
|
||||
SCAN_CODE_MAP.put(42, "ENTER");
|
||||
SCAN_CODE_MAP.put(43, "ENTER");
|
||||
SCAN_CODE_MAP.put(45, "Z");
|
||||
SCAN_CODE_MAP.put(46, "X");
|
||||
SCAN_CODE_MAP.put(47, "C");
|
||||
SCAN_CODE_MAP.put(48, "V");
|
||||
SCAN_CODE_MAP.put(49, "B");
|
||||
SCAN_CODE_MAP.put(50, "N");
|
||||
SCAN_CODE_MAP.put(51, "M");
|
||||
SCAN_CODE_MAP.put(52, "COMMA");
|
||||
SCAN_CODE_MAP.put(53, "PERIOD");
|
||||
SCAN_CODE_MAP.put(54, "SLASH");
|
||||
SCAN_CODE_MAP.put(57, "SPACE");
|
||||
|
||||
// Editing & Navigation keys
|
||||
SCAN_CODE_MAP.put(75, "INSERT");
|
||||
SCAN_CODE_MAP.put(76, "DELETE");
|
||||
SCAN_CODE_MAP.put(79, "LEFT");
|
||||
SCAN_CODE_MAP.put(80, "HOME");
|
||||
SCAN_CODE_MAP.put(81, "END");
|
||||
SCAN_CODE_MAP.put(83, "UP");
|
||||
SCAN_CODE_MAP.put(84, "DOWN");
|
||||
SCAN_CODE_MAP.put(85, "PAGE_UP");
|
||||
SCAN_CODE_MAP.put(86, "PAGE_DOWN");
|
||||
SCAN_CODE_MAP.put(89, "RIGHT");
|
||||
|
||||
// Numeric Keypad
|
||||
SCAN_CODE_MAP.put(90, "NUM_LOCK");
|
||||
SCAN_CODE_MAP.put(91, "NUMPAD7");
|
||||
SCAN_CODE_MAP.put(92, "NUMPAD4");
|
||||
SCAN_CODE_MAP.put(93, "NUMPAD1");
|
||||
SCAN_CODE_MAP.put(95, "DIVIDE");
|
||||
SCAN_CODE_MAP.put(96, "NUMPAD8");
|
||||
SCAN_CODE_MAP.put(97, "NUMPAD5");
|
||||
SCAN_CODE_MAP.put(98, "NUMPAD2");
|
||||
SCAN_CODE_MAP.put(99, "NUMPAD0");
|
||||
SCAN_CODE_MAP.put(100, "MULTIPLY");
|
||||
SCAN_CODE_MAP.put(101, "NUMPAD9");
|
||||
SCAN_CODE_MAP.put(102, "NUMPAD6");
|
||||
SCAN_CODE_MAP.put(103, "NUMPAD3");
|
||||
SCAN_CODE_MAP.put(104, "DECIMAL");
|
||||
SCAN_CODE_MAP.put(105, "SUBTRACT");
|
||||
SCAN_CODE_MAP.put(106, "ADD");
|
||||
SCAN_CODE_MAP.put(108, "ENTER");
|
||||
|
||||
// Function keys & Escape
|
||||
SCAN_CODE_MAP.put(110, "ESCAPE");
|
||||
SCAN_CODE_MAP.put(112, "F1");
|
||||
SCAN_CODE_MAP.put(113, "F2");
|
||||
SCAN_CODE_MAP.put(114, "F3");
|
||||
SCAN_CODE_MAP.put(115, "F4");
|
||||
SCAN_CODE_MAP.put(116, "F5");
|
||||
SCAN_CODE_MAP.put(117, "F6");
|
||||
SCAN_CODE_MAP.put(118, "F7");
|
||||
SCAN_CODE_MAP.put(119, "F8");
|
||||
SCAN_CODE_MAP.put(120, "F9");
|
||||
SCAN_CODE_MAP.put(121, "F10");
|
||||
SCAN_CODE_MAP.put(122, "F11");
|
||||
SCAN_CODE_MAP.put(123, "F12");
|
||||
|
||||
// Mnemonics mapping
|
||||
MNEMONIC_MAP.put("enter", "ENTER");
|
||||
MNEMONIC_MAP.put("enterreset", "ENTER");
|
||||
MNEMONIC_MAP.put("newline", "NEWLINE");
|
||||
MNEMONIC_MAP.put("tab", "TAB");
|
||||
MNEMONIC_MAP.put("backtab", "shift TAB");
|
||||
MNEMONIC_MAP.put("reset", "ESCAPE");
|
||||
MNEMONIC_MAP.put("clear", "CLEAR");
|
||||
MNEMONIC_MAP.put("eraseeof", "END");
|
||||
MNEMONIC_MAP.put("eof", "END");
|
||||
MNEMONIC_MAP.put("erasefld", "ERASE_INPUT");
|
||||
MNEMONIC_MAP.put("erinp", "ERASE_INPUT");
|
||||
MNEMONIC_MAP.put("eraseinput", "ERASE_INPUT");
|
||||
MNEMONIC_MAP.put("dup", "DUP");
|
||||
MNEMONIC_MAP.put("fieldmark", "FIELD_MARK");
|
||||
MNEMONIC_MAP.put("fldmark", "FIELD_MARK");
|
||||
MNEMONIC_MAP.put("fldext", "NEWLINE");
|
||||
MNEMONIC_MAP.put("field-exit", "NEWLINE");
|
||||
MNEMONIC_MAP.put("field+", "TAB");
|
||||
MNEMONIC_MAP.put("field-", "TAB");
|
||||
MNEMONIC_MAP.put("attn", "ATTN");
|
||||
MNEMONIC_MAP.put("sysreq", "SYSREQ");
|
||||
MNEMONIC_MAP.put("cursel", "CURSEL");
|
||||
MNEMONIC_MAP.put("copy", "COPY");
|
||||
MNEMONIC_MAP.put("paste", "PASTE");
|
||||
MNEMONIC_MAP.put("selectall", "SELECTALL");
|
||||
MNEMONIC_MAP.put("insert", "INSERT");
|
||||
MNEMONIC_MAP.put("delete", "DELETE");
|
||||
MNEMONIC_MAP.put("backspace", "BACK_SPACE");
|
||||
MNEMONIC_MAP.put("home", "HOME");
|
||||
MNEMONIC_MAP.put("end", "END");
|
||||
MNEMONIC_MAP.put("up", "UP");
|
||||
MNEMONIC_MAP.put("down", "DOWN");
|
||||
MNEMONIC_MAP.put("left", "LEFT");
|
||||
MNEMONIC_MAP.put("right", "RIGHT");
|
||||
MNEMONIC_MAP.put("pageup", "PAGE_UP");
|
||||
MNEMONIC_MAP.put("pagedn", "PAGE_DOWN");
|
||||
MNEMONIC_MAP.put("pagedown", "PAGE_DOWN");
|
||||
MNEMONIC_MAP.put("docmode", "DOCMODE");
|
||||
MNEMONIC_MAP.put("wordwrap", "WORDWRAP");
|
||||
MNEMONIC_MAP.put("apl", "APL");
|
||||
MNEMONIC_MAP.put("rule", "CROSSHAIR_RULER");
|
||||
MNEMONIC_MAP.put("statusbar", "STATUS_BAR");
|
||||
|
||||
for (int i = 1; i <= 24; i++) {
|
||||
MNEMONIC_MAP.put("pf" + i, "PF" + i);
|
||||
}
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
MNEMONIC_MAP.put("pa" + i, "PA" + i);
|
||||
}
|
||||
// Shifted PF keys 1-12 = PF13-PF24
|
||||
for (int i = 1; i <= 12; i++) {
|
||||
MNEMONIC_MAP.put("spf" + i, "PF" + (i + 12));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .kmp file from a File object.
|
||||
*
|
||||
* @param file the .kmp file to parse
|
||||
* @return Map of j3270 action names to key stroke bindings
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public static Map<String, String> parseKmp(File file) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
return parseKmp(reader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .kmp file from an InputStream.
|
||||
*
|
||||
* @param in the InputStream to parse
|
||||
* @return Map of j3270 action names to key stroke bindings
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public static Map<String, String> parseKmp(InputStream in) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
|
||||
return parseKmp(reader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .kmp file from a Reader.
|
||||
* Supports both INI-style KEY<n>=action formats and bind statements.
|
||||
*
|
||||
* @param reader the reader to parse from
|
||||
* @return Map of j3270 action names to key stroke bindings
|
||||
* @throws IOException on I/O error
|
||||
*/
|
||||
public static Map<String, String> parseKmp(Reader reader) throws IOException {
|
||||
BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);
|
||||
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
|
||||
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
line = line.trim();
|
||||
|
||||
// Strip comments
|
||||
int commentIdx = line.indexOf(';');
|
||||
if (commentIdx >= 0) {
|
||||
line = line.substring(0, commentIdx).trim();
|
||||
}
|
||||
commentIdx = line.indexOf('#');
|
||||
if (commentIdx >= 0) {
|
||||
line = line.substring(0, commentIdx).trim();
|
||||
}
|
||||
|
||||
if (line.isEmpty() || line.startsWith("[")) {
|
||||
continue; // Skip section headers and blank lines
|
||||
}
|
||||
|
||||
// Syntax 1: bind [mnemonic] KeyCombination OR bind action KeyCombination
|
||||
if (line.toLowerCase().startsWith("bind ")) {
|
||||
parseBindStatement(line.substring(5).trim(), actionToBindings);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Syntax 2: KEY<n>=action OR Modifier-KEY<n>=action OR KeyStroke=[mnemonic]
|
||||
int eq = line.indexOf('=');
|
||||
if (eq > 0) {
|
||||
String left = line.substring(0, eq).trim();
|
||||
String right = line.substring(eq + 1).trim();
|
||||
parseKeyAssignment(left, right, actionToBindings);
|
||||
}
|
||||
}
|
||||
|
||||
// Format into combined comma-separated bindings map
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : actionToBindings.entrySet()) {
|
||||
String action = entry.getKey();
|
||||
List<String> bindings = entry.getValue();
|
||||
if (!bindings.isEmpty()) {
|
||||
result.put(action, String.join(", ", bindings));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
|
||||
String[] parts = stmt.split("\\s+", 2);
|
||||
if (parts.length < 2) return;
|
||||
|
||||
String target = parts[0].trim();
|
||||
String keyStrokeStr = parts[1].trim();
|
||||
|
||||
String action = resolveMnemonic(target);
|
||||
if (action == null) {
|
||||
// Check if parts[1] was the mnemonic instead (e.g. "bind KeyCombination [mnemonic]")
|
||||
String altAction = resolveMnemonic(keyStrokeStr);
|
||||
if (altAction != null) {
|
||||
action = altAction;
|
||||
keyStrokeStr = target;
|
||||
}
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
String normKeyStroke = normalizeKeyStroke(keyStrokeStr);
|
||||
if (normKeyStroke != null && !normKeyStroke.isEmpty()) {
|
||||
addBinding(actionToBindings, action, normKeyStroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseKeyAssignment(String left, String right, Map<String, List<String>> actionToBindings) {
|
||||
// Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
|
||||
String action = resolveMnemonic(right);
|
||||
String keyStroke = null;
|
||||
|
||||
if (action != null) {
|
||||
keyStroke = parseScanCodeEntry(left);
|
||||
} else {
|
||||
// Case B: left is mnemonic, right is KeyStroke
|
||||
action = resolveMnemonic(left);
|
||||
if (action != null) {
|
||||
keyStroke = normalizeKeyStroke(right);
|
||||
} else {
|
||||
// Case C: left is KeyStroke, right is mnemonic
|
||||
action = resolveMnemonic(right);
|
||||
if (action != null) {
|
||||
keyStroke = normalizeKeyStroke(left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action != null && keyStroke != null && !keyStroke.isEmpty()) {
|
||||
addBinding(actionToBindings, action, keyStroke);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse scan code entry like "KEY43", "S-KEY112", "C-KEY43", "CS-KEY85", etc.
|
||||
*/
|
||||
public static String parseScanCodeEntry(String entry) {
|
||||
if (entry == null || entry.isEmpty()) return null;
|
||||
|
||||
String upper = entry.toUpperCase().trim();
|
||||
int keyPos = upper.indexOf("KEY");
|
||||
if (keyPos < 0) {
|
||||
return normalizeKeyStroke(entry);
|
||||
}
|
||||
|
||||
String prefix = upper.substring(0, keyPos);
|
||||
if (prefix.endsWith("-")) {
|
||||
prefix = prefix.substring(0, prefix.length() - 1);
|
||||
}
|
||||
|
||||
String numPart = upper.substring(keyPos + 3);
|
||||
int scanCode;
|
||||
try {
|
||||
scanCode = Integer.parseInt(numPart.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String baseKey = SCAN_CODE_MAP.get(scanCode);
|
||||
if (baseKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (prefix.contains("C")) sb.append("ctrl ");
|
||||
if (prefix.contains("A") || prefix.contains("2")) sb.append("alt ");
|
||||
if (prefix.contains("S")) sb.append("shift ");
|
||||
if (prefix.contains("M")) sb.append("meta ");
|
||||
|
||||
sb.append(baseKey);
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize key stroke expression (e.g. "Ctrl+Shift+C" -> "ctrl shift C").
|
||||
*/
|
||||
public static String normalizeKeyStroke(String stroke) {
|
||||
if (stroke == null || stroke.trim().isEmpty()) return null;
|
||||
String s = stroke.trim();
|
||||
|
||||
// Check if it is an IBM scan code
|
||||
if (s.toUpperCase().contains("KEY")) {
|
||||
String fromScan = parseScanCodeEntry(s);
|
||||
if (fromScan != null) return fromScan;
|
||||
}
|
||||
|
||||
// Replace '+' with space
|
||||
s = s.replace("+", " ");
|
||||
String[] parts = s.split("\\s+");
|
||||
|
||||
StringBuilder mods = new StringBuilder();
|
||||
String mainKey = null;
|
||||
|
||||
for (String p : parts) {
|
||||
String lp = p.toLowerCase();
|
||||
if ("ctrl".equals(lp) || "control".equals(lp)) {
|
||||
mods.append("ctrl ");
|
||||
} else if ("shift".equals(lp)) {
|
||||
mods.append("shift ");
|
||||
} else if ("alt".equals(lp)) {
|
||||
mods.append("alt ");
|
||||
} else if ("meta".equals(lp) || "cmd".equals(lp) || "command".equals(lp)) {
|
||||
mods.append("meta ");
|
||||
} else {
|
||||
mainKey = p.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
if (mainKey == null) return null;
|
||||
return (mods.toString() + mainKey).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a mnemonic token (e.g. "[enter]", "enter", "[pf12]", "eraseeof")
|
||||
* into a j3270 action name.
|
||||
*/
|
||||
public static String resolveMnemonic(String token) {
|
||||
if (token == null) return null;
|
||||
String clean = token.trim();
|
||||
if (clean.startsWith("[") && clean.endsWith("]")) {
|
||||
clean = clean.substring(1, clean.length() - 1);
|
||||
}
|
||||
clean = clean.toLowerCase();
|
||||
|
||||
String direct = MNEMONIC_MAP.get(clean);
|
||||
if (direct != null) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
// Case-insensitive match on existing action names
|
||||
String upper = clean.toUpperCase();
|
||||
if (MNEMONIC_MAP.containsValue(upper)) {
|
||||
return upper;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void addBinding(Map<String, List<String>> map, String action, String keyStroke) {
|
||||
List<String> list = map.computeIfAbsent(action, k -> new ArrayList<>());
|
||||
if (!list.contains(keyStroke)) {
|
||||
list.add(keyStroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import haus.nightmare.j3270.config.Settings;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
@@ -33,6 +34,8 @@ public class SettingsDialog extends JDialog {
|
||||
private JCheckBox blockSelectCheck;
|
||||
private JSpinner dynamicRowsSpinner;
|
||||
private JSpinner dynamicColsSpinner;
|
||||
private JCheckBox enablePasteFromExcelCheck;
|
||||
private JCheckBox pasteStopAtProtectedCheck;
|
||||
|
||||
// Entry Assist & Modes tab
|
||||
private JCheckBox docModeCheck;
|
||||
@@ -291,10 +294,18 @@ public class SettingsDialog extends JDialog {
|
||||
gbc.gridx = 1;
|
||||
panel.add(dynDimPanel, gbc);
|
||||
|
||||
// Placeholder for potentially more behavior options below
|
||||
// Clipboard & Tabular Paste options
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.gridwidth = 2;
|
||||
enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel());
|
||||
panel.add(enablePasteFromExcelCheck, gbc);
|
||||
|
||||
gbc.gridy = 5;
|
||||
pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine());
|
||||
panel.add(pasteStopAtProtectedCheck, gbc);
|
||||
|
||||
gbc.gridy = 6;
|
||||
gbc.weighty = 1.0;
|
||||
panel.add(Box.createGlue(), gbc);
|
||||
|
||||
@@ -653,6 +664,45 @@ public class SettingsDialog extends JDialog {
|
||||
});
|
||||
btnPanel.add(btnReset);
|
||||
|
||||
// Import Keymap — import from .kmp file
|
||||
JButton btnImportKmp = new JButton("Import Keymap...");
|
||||
ThemeManager.styleButton(btnImportKmp, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnImportKmp.addActionListener(e -> {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Import Keymap Profile");
|
||||
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("Keyboard Map Profile (*.kmp)", "kmp"));
|
||||
int ret = chooser.showOpenDialog(this);
|
||||
if (ret == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
try {
|
||||
Map<String, String> imported = KeyBindings.parseKmp(f);
|
||||
int updatedCount = 0;
|
||||
for (Map.Entry<String, String> entry : imported.entrySet()) {
|
||||
String action = entry.getKey();
|
||||
String binding = entry.getValue();
|
||||
tempKeyBindings.put(action, binding);
|
||||
for (int row = 0; row < keymapModel.getRowCount(); row++) {
|
||||
if (action.equalsIgnoreCase((String) keymapModel.getValueAt(row, 0))) {
|
||||
keymapModel.setValueAt(binding, row, 1);
|
||||
updatedCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Successfully imported " + updatedCount + " key bindings from " + f.getName(),
|
||||
"Keymap Imported",
|
||||
JOptionPane.INFORMATION_MESSAGE);
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Failed to import keymap: " + ex.getMessage(),
|
||||
"Import Error",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
btnPanel.add(btnImportKmp);
|
||||
|
||||
main.add(new JScrollPane(table), BorderLayout.CENTER);
|
||||
main.add(btnPanel, BorderLayout.SOUTH);
|
||||
|
||||
@@ -705,6 +755,14 @@ public class SettingsDialog extends JDialog {
|
||||
// Block select mode
|
||||
Settings.setBlockSelectMode(blockSelectCheck.isSelected());
|
||||
|
||||
// Clipboard & Tabular Paste options
|
||||
if (enablePasteFromExcelCheck != null) {
|
||||
Settings.setEnablePasteFromExcel(enablePasteFromExcelCheck.isSelected());
|
||||
}
|
||||
if (pasteStopAtProtectedCheck != null) {
|
||||
Settings.setPasteStopAtProtectedLine(pasteStopAtProtectedCheck.isSelected());
|
||||
}
|
||||
|
||||
// Default Dynamic screen dimensions
|
||||
if (dynamicRowsSpinner != null && dynamicColsSpinner != null) {
|
||||
Settings.setDynamicRows((Integer) dynamicRowsSpinner.getValue());
|
||||
|
||||
@@ -137,12 +137,20 @@ public class StatusBar extends JPanel {
|
||||
}
|
||||
|
||||
public void applyTheme(UITheme theme) {
|
||||
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(() -> applyTheme(theme));
|
||||
return;
|
||||
}
|
||||
setBackground(ThemeManager.getStatusBarBg(theme));
|
||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
public void updateStatus() {
|
||||
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(this::updateStatus);
|
||||
return;
|
||||
}
|
||||
UITheme theme = ThemeManager.getTheme();
|
||||
if (client == null) {
|
||||
connectionStatus.setText("Not Connected");
|
||||
@@ -170,6 +178,10 @@ public class StatusBar extends JPanel {
|
||||
connectionStatus.setText("Not Connected");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||
break;
|
||||
case RECONNECTING:
|
||||
connectionStatus.setText("Reconnecting...");
|
||||
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
|
||||
break;
|
||||
case TCP_PENDING:
|
||||
case TELNET_PENDING:
|
||||
connectionStatus.setText("Connecting...");
|
||||
|
||||
@@ -494,8 +494,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
int cols = sb != null ? sb.getDisplayCols() : 80;
|
||||
int curRow = cols > 0 ? curPos / cols : 0;
|
||||
int curCol = cols > 0 ? curPos % cols : 0;
|
||||
boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel();
|
||||
boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine();
|
||||
|
||||
if (client.getPS() != null && (text.contains("\n") || text.contains("\r"))) {
|
||||
if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || text.contains("\t") || text.contains("\n") || text.contains("\r"))) {
|
||||
client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected);
|
||||
} else if (client.getPS() != null) {
|
||||
client.getPS().pasteString(text, curRow, curCol);
|
||||
} else {
|
||||
for (char ch : text.toCharArray()) {
|
||||
@@ -603,6 +607,10 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
|
||||
|
||||
public void guardedPack() {
|
||||
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(this::guardedPack);
|
||||
return;
|
||||
}
|
||||
resizeGuard = true;
|
||||
revalidate();
|
||||
Container top = getTopLevelAncestor();
|
||||
@@ -1163,10 +1171,19 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
if (ip != null) {
|
||||
ip.setBellEnabled(haus.nightmare.j3270.config.Settings.getEntryAssistBell());
|
||||
ip.setBellColumn(Math.max(0, haus.nightmare.j3270.config.Settings.getEntryAssistBellCol() - 1));
|
||||
if (ip.getBellListener() == null) {
|
||||
ip.setBellListener(() -> {
|
||||
SwingUtilities.invokeLater(() -> Toolkit.getDefaultToolkit().beep());
|
||||
});
|
||||
}
|
||||
ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
|
||||
ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
|
||||
ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
|
||||
}
|
||||
if (client.getPS() != null) {
|
||||
client.getPS().setEnablePasteFromExcel(haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel());
|
||||
client.getPS().setPasteStopAtProtectedLine(haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine());
|
||||
}
|
||||
}
|
||||
if (statusBar != null) {
|
||||
statusBar.updateStatus();
|
||||
@@ -1432,8 +1449,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
if (rgb != null && gWidth > 0 && gHeight > 0) {
|
||||
long currentUpdateCount = client.getGraphicsPlane().getUpdateCount();
|
||||
if (cachedGraphicsImage == null || currentUpdateCount != lastGraphicsUpdateCount || cachedGraphicsImage.getWidth() != gWidth || cachedGraphicsImage.getHeight() != gHeight) {
|
||||
cachedGraphicsImage = new java.awt.image.BufferedImage(gWidth, gHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
cachedGraphicsImage.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth);
|
||||
cachedGraphicsImage = AwtPixelBufferBridge.toBufferedImage(client.getGraphicsPlane());
|
||||
lastGraphicsUpdateCount = currentUpdateCount;
|
||||
}
|
||||
if (gWidth == gridW && gHeight == gridH) {
|
||||
@@ -1548,7 +1564,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
|
||||
if (slot != null) {
|
||||
int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0;
|
||||
java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), symBg);
|
||||
haus.nightmare.lib3270j.graphics.PixelBuffer symPb = slot.getScaledPixelBuffer(cellWidth, cellHeight, fgColor.getRGB(), symBg);
|
||||
java.awt.image.BufferedImage img = AwtPixelBufferBridge.toBufferedImage(symPb);
|
||||
if (img != null) {
|
||||
g2.drawImage(img, x, y, null);
|
||||
drawnAsPs = true;
|
||||
@@ -1691,7 +1708,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
public void setHodWallpaper(haus.nightmare.lib3270j.graphics.HODWallpaper wallpaper) {
|
||||
this.hodWallpaper = wallpaper;
|
||||
if (wallpaper != null) {
|
||||
this.wallpaperImage = wallpaper.getHODImage();
|
||||
Object hodImg = wallpaper.getHODImage();
|
||||
if (hodImg instanceof Image) {
|
||||
this.wallpaperImage = (Image) hodImg;
|
||||
} else if (hodImg instanceof haus.nightmare.lib3270j.graphics.PixelBuffer) {
|
||||
this.wallpaperImage = AwtPixelBufferBridge.toBufferedImage((haus.nightmare.lib3270j.graphics.PixelBuffer) hodImg);
|
||||
}
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 13.3: Keyboard Remap (.kmp) File Importer.
|
||||
* Tests parsing of scan codes, modifier combinations, bind statements,
|
||||
* mnemonic actions, and comments.
|
||||
*/
|
||||
public class KmpKeymapImportTest {
|
||||
|
||||
@Test
|
||||
public void testParseScanCodeAssignments() throws Exception {
|
||||
String kmpContent =
|
||||
"# Sample keyboard profile mapping\n" +
|
||||
"; IBM scan code mappings\n" +
|
||||
"KEY43=[enter]\n" +
|
||||
"S-KEY112=[pf13]\n" +
|
||||
"C-KEY43=[newline]\n" +
|
||||
"A-KEY110=[reset]\n" +
|
||||
"KEY15=[tab]\n" +
|
||||
"S-KEY15=[backtab]\n";
|
||||
|
||||
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
|
||||
assertNotNull(imported);
|
||||
|
||||
// KEY43 is ENTER -> ENTER
|
||||
assertEquals("ENTER", imported.get("ENTER"));
|
||||
// S-KEY112 is Shift+F1 -> PF13
|
||||
assertEquals("shift F1", imported.get("PF13"));
|
||||
// C-KEY43 is Ctrl+ENTER -> NEWLINE
|
||||
assertEquals("ctrl ENTER", imported.get("NEWLINE"));
|
||||
// A-KEY110 is Alt+ESCAPE -> ESCAPE
|
||||
assertEquals("alt ESCAPE", imported.get("ESCAPE"));
|
||||
// KEY15 is TAB -> TAB
|
||||
assertEquals("TAB", imported.get("TAB"));
|
||||
// S-KEY15 is Shift+TAB -> shift TAB
|
||||
assertEquals("shift TAB", imported.get("shift TAB"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseBindStatements() throws Exception {
|
||||
String kmpContent =
|
||||
"bind [enter] Ctrl+Enter\n" +
|
||||
"bind [pf1] F1\n" +
|
||||
"bind [pf24] Shift+F12\n" +
|
||||
"bind [eraseeof] End\n" +
|
||||
"bind [clear] Pause\n";
|
||||
|
||||
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
|
||||
assertNotNull(imported);
|
||||
|
||||
assertEquals("ctrl ENTER", imported.get("ENTER"));
|
||||
assertEquals("F1", imported.get("PF1"));
|
||||
assertEquals("shift F12", imported.get("PF24"));
|
||||
assertEquals("END", imported.get("END"));
|
||||
assertEquals("PAUSE", imported.get("CLEAR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScanCodeWithMultipleModifiers() {
|
||||
// C-S-KEY85 -> Ctrl+Shift+PAGE_UP
|
||||
String res = KeyBindings.parseScanCodeEntry("CS-KEY85");
|
||||
assertEquals("ctrl shift PAGE_UP", res);
|
||||
|
||||
// 2-KEY80 -> Alt+HOME (2- prefix is Alt in scan code syntax)
|
||||
String altRes = KeyBindings.parseScanCodeEntry("2-KEY80");
|
||||
assertEquals("alt HOME", altRes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMnemonicResolution() {
|
||||
assertEquals("ENTER", KeyBindings.resolveMnemonic("[enter]"));
|
||||
assertEquals("ENTER", KeyBindings.resolveMnemonic("enterreset"));
|
||||
assertEquals("NEWLINE", KeyBindings.resolveMnemonic("[newline]"));
|
||||
assertEquals("NEWLINE", KeyBindings.resolveMnemonic("[field-exit]"));
|
||||
assertEquals("PF1", KeyBindings.resolveMnemonic("[pf1]"));
|
||||
assertEquals("PF12", KeyBindings.resolveMnemonic("[pf12]"));
|
||||
assertEquals("PF13", KeyBindings.resolveMnemonic("[spf1]"));
|
||||
assertEquals("ESCAPE", KeyBindings.resolveMnemonic("[reset]"));
|
||||
assertEquals("ERASE_INPUT", KeyBindings.resolveMnemonic("[erasefld]"));
|
||||
assertEquals("DOCMODE", KeyBindings.resolveMnemonic("[docmode]"));
|
||||
assertEquals("WORDWRAP", KeyBindings.resolveMnemonic("[wordwrap]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommentsAndBlankLinesIgnored() throws Exception {
|
||||
String kmpContent =
|
||||
"[ProfileHeader]\n" +
|
||||
" \n" +
|
||||
"# Full line comment\n" +
|
||||
"; Another comment\n" +
|
||||
"KEY42=[enter] ; inline comment\n" +
|
||||
" \n";
|
||||
|
||||
Map<String, String> imported = KeyBindings.parseKmp(new StringReader(kmpContent));
|
||||
assertEquals(1, imported.size());
|
||||
assertEquals("ENTER", imported.get("ENTER"));
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,17 @@ public class ConnectionConfig {
|
||||
private java.util.Map<String, String> environmentVariables = new java.util.LinkedHashMap<>();
|
||||
private java.util.Map<String, String> userVariables = new java.util.LinkedHashMap<>();
|
||||
|
||||
// IBM HoD autoSysUnlock parity
|
||||
private boolean autoSysUnlock = true;
|
||||
|
||||
// Phase 11: Enterprise Connection Resilience & Heartbeat
|
||||
private boolean keepAliveEnabled = true;
|
||||
private int keepAliveIntervalSeconds = 120;
|
||||
private String keepAliveType = "NOP";
|
||||
private boolean autoReconnect = false;
|
||||
private int reconnectMaxRetries = 5;
|
||||
private int tcpUserTimeoutMs = 0;
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
public ConnectionConfig(String host, int port) {
|
||||
@@ -280,6 +291,27 @@ public class ConnectionConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAutoSysUnlock() { return autoSysUnlock; }
|
||||
public void setAutoSysUnlock(boolean autoSysUnlock) { this.autoSysUnlock = autoSysUnlock; }
|
||||
|
||||
public boolean isKeepAliveEnabled() { return keepAliveEnabled; }
|
||||
public void setKeepAliveEnabled(boolean enabled) { this.keepAliveEnabled = enabled; }
|
||||
|
||||
public int getKeepAliveIntervalSeconds() { return keepAliveIntervalSeconds; }
|
||||
public void setKeepAliveIntervalSeconds(int seconds) { this.keepAliveIntervalSeconds = seconds; }
|
||||
|
||||
public String getKeepAliveType() { return keepAliveType; }
|
||||
public void setKeepAliveType(String type) { this.keepAliveType = (type != null) ? type.trim().toUpperCase() : "NOP"; }
|
||||
|
||||
public boolean isAutoReconnect() { return autoReconnect; }
|
||||
public void setAutoReconnect(boolean autoReconnect) { this.autoReconnect = autoReconnect; }
|
||||
|
||||
public int getReconnectMaxRetries() { return reconnectMaxRetries; }
|
||||
public void setReconnectMaxRetries(int retries) { this.reconnectMaxRetries = Math.max(0, retries); }
|
||||
|
||||
public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; }
|
||||
public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); }
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
|
||||
@@ -303,10 +335,21 @@ public class ConnectionConfig {
|
||||
String pUser = null;
|
||||
String pPass = null;
|
||||
|
||||
boolean keepAlive = true;
|
||||
boolean autoReconnect = false;
|
||||
|
||||
String[] tokens = s.split("\\s+");
|
||||
StringBuilder remaining = new StringBuilder();
|
||||
for (String tok : tokens) {
|
||||
if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
|
||||
if (tok.equalsIgnoreCase("--keepalive") || tok.equalsIgnoreCase("-keepalive")) {
|
||||
keepAlive = true;
|
||||
} else if (tok.equalsIgnoreCase("--no-keepalive") || tok.equalsIgnoreCase("-no-keepalive")) {
|
||||
keepAlive = false;
|
||||
} else if (tok.equalsIgnoreCase("--autoreconnect") || tok.equalsIgnoreCase("-autoreconnect")) {
|
||||
autoReconnect = true;
|
||||
} else if (tok.equalsIgnoreCase("--no-autoreconnect") || tok.equalsIgnoreCase("-no-autoreconnect")) {
|
||||
autoReconnect = false;
|
||||
} else if (tok.startsWith("--proxy=") || tok.startsWith("-proxy=")) {
|
||||
String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim();
|
||||
try {
|
||||
java.net.URI uri = new java.net.URI(proxyUrl);
|
||||
@@ -417,6 +460,8 @@ public class ConnectionConfig {
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
config.setUseTls(tls);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
config.setKeepAliveEnabled(keepAlive);
|
||||
config.setAutoReconnect(autoReconnect);
|
||||
if (dynamic) {
|
||||
config.setDynamicDimensions(dynRows, dynCols);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ public class Telnet3270Client {
|
||||
private final haus.nightmare.lib3270j.ecl.ECLXfer xfer;
|
||||
private TelnetConnection connection;
|
||||
|
||||
private final java.util.concurrent.atomic.AtomicBoolean reconnecting = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
private volatile Thread reconnectThread;
|
||||
|
||||
public Telnet3270Client(ConnectionConfig config) {
|
||||
this.config = config;
|
||||
this.translator = new EbcdicTranslator(config.getCodePage());
|
||||
@@ -58,6 +61,7 @@ public class Telnet3270Client {
|
||||
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
||||
}
|
||||
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
||||
this.dsProcessor.setAutoSysUnlock(config.isAutoSysUnlock());
|
||||
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
||||
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||
@@ -87,6 +91,33 @@ public class Telnet3270Client {
|
||||
@Override public void onSoundAlarm() {
|
||||
ps.notifyAlarm();
|
||||
}
|
||||
@Override public void onKeyboardUnlocked() {
|
||||
ps.notifyKeyUnlocked();
|
||||
}
|
||||
});
|
||||
|
||||
// Phase 11: Auto-reconnection and OIA status coordinator
|
||||
fsm.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
if (newState == ConnectionState.RECONNECTING) {
|
||||
if (oia != null) {
|
||||
oia.writeToOIA("X RECONNECT");
|
||||
}
|
||||
initiateAutoReconnect();
|
||||
} else if (newState == ConnectionState.NOT_CONNECTED) {
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(haus.nightmare.lib3270j.ecl.ECLOIA.INHIBIT_COMMCHECK);
|
||||
}
|
||||
} else if (newState.isFullSession()) {
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionError(String message) {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,11 +190,146 @@ public class Telnet3270Client {
|
||||
* Disconnect from the host.
|
||||
*/
|
||||
public void disconnect() {
|
||||
cancelAutoReconnect();
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
connection = null;
|
||||
}
|
||||
fsm.onDisconnect();
|
||||
fsm.onDisconnect(false);
|
||||
}
|
||||
|
||||
public void cancelAutoReconnect() {
|
||||
reconnecting.set(false);
|
||||
if (reconnectThread != null) {
|
||||
reconnectThread.interrupt();
|
||||
reconnectThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void initiateAutoReconnect() {
|
||||
if (config == null || !config.isAutoReconnect()) {
|
||||
return;
|
||||
}
|
||||
if (!reconnecting.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
reconnectThread = new Thread(this::runAutoReconnect, "TN3270-AutoReconnect");
|
||||
reconnectThread.setDaemon(true);
|
||||
reconnectThread.start();
|
||||
}
|
||||
|
||||
private void runAutoReconnect() {
|
||||
int maxRetries = (config != null) ? config.getReconnectMaxRetries() : 5;
|
||||
log.info("Starting automatic reconnection loop (maxRetries=" + maxRetries + ")");
|
||||
try {
|
||||
for (int attempt = 1; attempt <= maxRetries && reconnecting.get(); attempt++) {
|
||||
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s
|
||||
long delaySeconds = Math.min(30, (long) Math.pow(2, attempt - 1));
|
||||
log.info("Auto-reconnect attempt " + attempt + "/" + maxRetries + " scheduled in " + delaySeconds + "s");
|
||||
|
||||
for (int s = 0; s < delaySeconds * 10; s++) {
|
||||
if (!reconnecting.get() || Thread.currentThread().isInterrupted()) {
|
||||
log.info("Auto-reconnect cancelled during backoff delay");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
log.info("Auto-reconnect thread interrupted");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!reconnecting.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Executing auto-reconnect attempt " + attempt + "/" + maxRetries + " to " + config.getHost() + ":" + config.getPort());
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
connection = null;
|
||||
}
|
||||
fsm.resetSessionState();
|
||||
screenBuffer.erase(false);
|
||||
|
||||
connection = new TelnetConnection(config, fsm);
|
||||
fsm.setConnection(connection);
|
||||
connection.connect();
|
||||
fsm.onConnected();
|
||||
|
||||
log.info("Auto-reconnect successful on attempt " + attempt);
|
||||
reconnecting.set(false);
|
||||
fsm.notifyScreenUpdate();
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
log.warning("Auto-reconnect attempt " + attempt + " failed: " + e.getMessage());
|
||||
if (attempt < maxRetries && reconnecting.get()) {
|
||||
fsm.setConnectionState(ConnectionState.RECONNECTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All retries failed
|
||||
log.warning("All " + maxRetries + " automatic reconnection attempts failed");
|
||||
reconnecting.set(false);
|
||||
fsm.onDisconnect(false);
|
||||
fsm.onError("Automatic reconnection failed after " + maxRetries + " attempts");
|
||||
} finally {
|
||||
reconnecting.set(false);
|
||||
reconnectThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAutoReconnect() {
|
||||
return (config != null) && config.isAutoReconnect();
|
||||
}
|
||||
|
||||
public void setAutoReconnect(boolean autoReconnect) {
|
||||
if (config != null) {
|
||||
config.setAutoReconnect(autoReconnect);
|
||||
}
|
||||
}
|
||||
|
||||
public int getReconnectMaxRetries() {
|
||||
return (config != null) ? config.getReconnectMaxRetries() : 5;
|
||||
}
|
||||
|
||||
public void setReconnectMaxRetries(int retries) {
|
||||
if (config != null) {
|
||||
config.setReconnectMaxRetries(retries);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isReconnecting() {
|
||||
return reconnecting.get() || fsm.getConnectionState() == ConnectionState.RECONNECTING;
|
||||
}
|
||||
|
||||
public boolean isKeepAliveEnabled() {
|
||||
return (config != null) && config.isKeepAliveEnabled();
|
||||
}
|
||||
|
||||
public void setKeepAliveEnabled(boolean enabled) {
|
||||
if (config != null) {
|
||||
config.setKeepAliveEnabled(enabled);
|
||||
}
|
||||
if (connection != null) {
|
||||
if (enabled) connection.startKeepAlive();
|
||||
else connection.stopKeepAlive();
|
||||
}
|
||||
}
|
||||
|
||||
public int getKeepAliveIntervalSeconds() {
|
||||
return (config != null) ? config.getKeepAliveIntervalSeconds() : 120;
|
||||
}
|
||||
|
||||
public void setKeepAliveIntervalSeconds(int seconds) {
|
||||
if (config != null) {
|
||||
config.setKeepAliveIntervalSeconds(seconds);
|
||||
}
|
||||
if (connection != null && config != null && config.isKeepAliveEnabled()) {
|
||||
connection.startKeepAlive();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,6 +460,23 @@ public class Telnet3270Client {
|
||||
return fsm;
|
||||
}
|
||||
|
||||
public boolean isAutoSysUnlock() {
|
||||
return (config != null) ? config.isAutoSysUnlock() : true;
|
||||
}
|
||||
|
||||
public void setAutoSysUnlock(boolean autoSysUnlock) {
|
||||
if (config != null) {
|
||||
config.setAutoSysUnlock(autoSysUnlock);
|
||||
}
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.setAutoSysUnlock(autoSysUnlock);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isContentionResolution() {
|
||||
return fsm != null && fsm.isContentionResolutionNegotiated();
|
||||
}
|
||||
|
||||
/** Send an NVT ASCII character in NVT mode. */
|
||||
public void sendNVTChar(char c) throws IOException {
|
||||
fsm.sendNVTChar(c);
|
||||
|
||||
@@ -47,6 +47,13 @@ public class DataStreamProcessor {
|
||||
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
||||
private int currentGocaSubtype = 0;
|
||||
|
||||
// Phase 10: Auto-Unlock & Contention Resolution State
|
||||
private boolean autoSysUnlock = true;
|
||||
private boolean contentionResolution = false;
|
||||
private boolean unlockPending = false;
|
||||
private boolean unlockSysPending = false;
|
||||
private boolean rcvdRead = false;
|
||||
|
||||
/** Functional interface for sending output back through the telnet stack. */
|
||||
@FunctionalInterface
|
||||
public interface OutputSender {
|
||||
@@ -131,6 +138,21 @@ public class DataStreamProcessor {
|
||||
screenListeners.remove(l);
|
||||
}
|
||||
|
||||
public boolean isAutoSysUnlock() { return autoSysUnlock; }
|
||||
public void setAutoSysUnlock(boolean autoSysUnlock) { this.autoSysUnlock = autoSysUnlock; }
|
||||
|
||||
public boolean isContentionResolution() { return contentionResolution; }
|
||||
public void setContentionResolution(boolean cr) { this.contentionResolution = cr; }
|
||||
|
||||
public boolean isUnlockPending() { return unlockPending; }
|
||||
public void setUnlockPending(boolean pending) { this.unlockPending = pending; }
|
||||
|
||||
public boolean isUnlockSysPending() { return unlockSysPending; }
|
||||
public void setUnlockSysPending(boolean pending) { this.unlockSysPending = pending; }
|
||||
|
||||
public boolean isRcvdRead() { return rcvdRead; }
|
||||
public void setRcvdRead(boolean rcvdRead) { this.rcvdRead = rcvdRead; }
|
||||
|
||||
/**
|
||||
* Process a 3270 data stream record.
|
||||
*
|
||||
@@ -197,16 +219,19 @@ public class DataStreamProcessor {
|
||||
break;
|
||||
case CMD_RB:
|
||||
case SNA_CMD_RB:
|
||||
rcvdRead = true;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadBuffer();
|
||||
break;
|
||||
case CMD_RM:
|
||||
case SNA_CMD_RM:
|
||||
rcvdRead = true;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadModified(false);
|
||||
break;
|
||||
case CMD_RMA:
|
||||
case SNA_CMD_RMA:
|
||||
rcvdRead = true;
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadModified(true);
|
||||
break;
|
||||
@@ -235,7 +260,10 @@ public class DataStreamProcessor {
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
if (keyboardRestore && inputProcessor != null) {
|
||||
boolean isWriteCmd = (cmd == CMD_W || cmd == SNA_CMD_W ||
|
||||
cmd == CMD_EW || cmd == SNA_CMD_EW ||
|
||||
cmd == CMD_EWA || cmd == SNA_CMD_EWA);
|
||||
if (!isWriteCmd && keyboardRestore && inputProcessor != null && !contentionResolution) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
|
||||
@@ -333,10 +361,13 @@ public class DataStreamProcessor {
|
||||
log.fine("WCC: " + String.format("0x%02x", wcc) +
|
||||
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
|
||||
|
||||
if (kbdRestore || inputProcessor != null) {
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
if (kbdRestore) {
|
||||
unlockPending = true;
|
||||
unlockSysPending = true;
|
||||
}
|
||||
|
||||
if (!contentionResolution && kbdRestore && inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
|
||||
if (resetMdt) {
|
||||
@@ -1101,6 +1132,9 @@ public class DataStreamProcessor {
|
||||
}
|
||||
|
||||
private void notifyScreenUpdated() {
|
||||
if (screen != null) {
|
||||
screen.notifyScreenUpdate();
|
||||
}
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
@@ -1549,7 +1583,11 @@ public class DataStreamProcessor {
|
||||
log.fine("processWCC: " + String.format("0x%02x", wcc) +
|
||||
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
|
||||
|
||||
if (kbdRestore && inputProcessor != null) {
|
||||
if (kbdRestore) {
|
||||
unlockPending = true;
|
||||
unlockSysPending = true;
|
||||
}
|
||||
if (!contentionResolution && kbdRestore && inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
if (resetMdt) {
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
package haus.nightmare.lib3270j.eNetwork.ECL.event;
|
||||
|
||||
import java.awt.Image;
|
||||
import java.awt.Rectangle;
|
||||
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||
|
||||
/**
|
||||
* Drop-in IBM Host On-Demand compatible facade for ECLPSGraphicsEvent.
|
||||
@@ -12,11 +11,11 @@ public class ECLPSGraphicsEvent extends haus.nightmare.lib3270j.ecl.ECLPSGraphic
|
||||
super(source, id);
|
||||
}
|
||||
|
||||
public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Image image) {
|
||||
public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Object image) {
|
||||
super(source, id, image);
|
||||
}
|
||||
|
||||
public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Image image, Rectangle rectangle) {
|
||||
public ECLPSGraphicsEvent(haus.nightmare.lib3270j.ecl.ECLPS source, int id, Object image, Rectangle rectangle) {
|
||||
super(source, id, image, rectangle);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import haus.nightmare.lib3270j.graphics.Color;
|
||||
|
||||
/**
|
||||
* Drop-in IBM Host On-Demand compatible facade for FillArea.
|
||||
|
||||
+1
-3
@@ -1,12 +1,10 @@
|
||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||
|
||||
import java.awt.Component;
|
||||
|
||||
/**
|
||||
* Drop-in IBM Host On-Demand compatible facade for HODBitImage.
|
||||
*/
|
||||
public class HODBitImage extends haus.nightmare.lib3270j.graphics.HODBitImage {
|
||||
public HODBitImage(Component comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) {
|
||||
public HODBitImage(Object comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) {
|
||||
super(comp, width, height, data, baseColor, depth, useGraphicColors);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-6
@@ -1,8 +1,7 @@
|
||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Rectangle;
|
||||
import haus.nightmare.lib3270j.graphics.Dimension;
|
||||
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||
|
||||
/**
|
||||
* Drop-in IBM Host On-Demand compatible facade for HODPart.
|
||||
@@ -12,15 +11,15 @@ public class HODPart extends haus.nightmare.lib3270j.graphics.HODPart {
|
||||
super();
|
||||
}
|
||||
|
||||
public HODPart(Component component) {
|
||||
public HODPart(Object component) {
|
||||
super(component);
|
||||
}
|
||||
|
||||
public HODPart(Component component, Dimension dimension) {
|
||||
public HODPart(Object component, Dimension dimension) {
|
||||
super(component, dimension);
|
||||
}
|
||||
|
||||
public HODPart(Component component, Rectangle rectangle) {
|
||||
public HODPart(Object component, Rectangle rectangle) {
|
||||
super(component, rectangle);
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -1,7 +1,5 @@
|
||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||
|
||||
import java.awt.Image;
|
||||
|
||||
/**
|
||||
* Drop-in IBM Host On-Demand compatible facade for HODWallpaper.
|
||||
*/
|
||||
@@ -14,7 +12,7 @@ public class HODWallpaper extends haus.nightmare.lib3270j.graphics.HODWallpaper
|
||||
super(displayMode);
|
||||
}
|
||||
|
||||
public HODWallpaper(Image image, int displayMode) {
|
||||
public HODWallpaper(Object image, int displayMode) {
|
||||
super(image, displayMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import haus.nightmare.lib3270j.graphics.Color;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Standard implementation of ECLPSGraphicsServices conforming to IBM Host On-Demand ECL.
|
||||
* Completely decoupled from java.awt.
|
||||
*/
|
||||
public class DefaultPSGraphicsServices implements ECLPSGraphicsServices {
|
||||
|
||||
private final ECLPS ps;
|
||||
private Component visualComponent;
|
||||
private Object visualComponent;
|
||||
private Color[] colors;
|
||||
private final List<ECLPSGraphicsListener> listeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
@@ -20,11 +20,11 @@ public class DefaultPSGraphicsServices implements ECLPSGraphicsServices {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisualComponent(Component comp) {
|
||||
public void setVisualComponent(Object comp) {
|
||||
this.visualComponent = comp;
|
||||
}
|
||||
|
||||
public Component getVisualComponent() {
|
||||
public Object getVisualComponent() {
|
||||
return visualComponent;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@ public class ECLConnection {
|
||||
private String luName;
|
||||
private String workstationId = "";
|
||||
private boolean ssl = false;
|
||||
private boolean autoSysUnlock = true;
|
||||
private boolean keepAlive = true;
|
||||
private int keepAliveTimeout = 120;
|
||||
private String keepAliveType = "NOP";
|
||||
private boolean autoReconnect = false;
|
||||
private int maxRetry = 5;
|
||||
private boolean contentionResolution = false;
|
||||
private boolean luluSession = false;
|
||||
private boolean isNegCR = false;
|
||||
@@ -71,12 +77,49 @@ public class ECLConnection {
|
||||
if (props != null) {
|
||||
this.properties.putAll(props);
|
||||
convertData(this.properties);
|
||||
String asu = this.properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK);
|
||||
if (asu != null) {
|
||||
this.autoSysUnlock = "true".equalsIgnoreCase(asu) || "1".equals(asu);
|
||||
}
|
||||
String ka = this.properties.getProperty(ECLSession.SESSION_KEEPALIVE);
|
||||
if (ka != null) {
|
||||
this.keepAlive = "true".equalsIgnoreCase(ka) || "1".equals(ka);
|
||||
}
|
||||
String kat = this.properties.getProperty(ECLSession.KEY_KEEPALIVE_TIMEOUT);
|
||||
if (kat != null) {
|
||||
try { this.keepAliveTimeout = Integer.parseInt(kat.trim()); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
String katyp = this.properties.getProperty(ECLSession.KEY_KEEPALIVE_TYPE);
|
||||
if (katyp != null) {
|
||||
this.keepAliveType = katyp;
|
||||
}
|
||||
String ar = this.properties.getProperty(ECLSession.SESSION_AUTORECONNECT);
|
||||
if (ar != null) {
|
||||
this.autoReconnect = "true".equalsIgnoreCase(ar) || "1".equals(ar);
|
||||
}
|
||||
String mr = this.properties.getProperty(ECLSession.SESSION_RECONNECT_RETRIES);
|
||||
if (mr != null) {
|
||||
try { this.maxRetry = Integer.parseInt(mr.trim()); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ECLConnection(ECLSession session, Telnet3270Client client) {
|
||||
this.session = session;
|
||||
this.client = client;
|
||||
if (client != null && client.getConfig() != null) {
|
||||
this.autoSysUnlock = client.getConfig().isAutoSysUnlock();
|
||||
this.keepAlive = client.getConfig().isKeepAliveEnabled();
|
||||
this.keepAliveTimeout = client.getConfig().getKeepAliveIntervalSeconds();
|
||||
this.keepAliveType = client.getConfig().getKeepAliveType();
|
||||
this.autoReconnect = client.getConfig().isAutoReconnect();
|
||||
this.maxRetry = client.getConfig().getReconnectMaxRetries();
|
||||
} else if (session != null) {
|
||||
this.autoSysUnlock = session.isAutoSysUnlock();
|
||||
this.keepAlive = session.isKeepAlive();
|
||||
this.keepAliveTimeout = session.getKeepAliveTimeout();
|
||||
this.autoReconnect = session.isAutoReconnect();
|
||||
}
|
||||
|
||||
if (client != null) {
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@@ -114,6 +157,13 @@ public class ECLConnection {
|
||||
state, state, "TN3270E Negotiated", deviceType, deviceName);
|
||||
notifyCommEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTN3270EFunctionsNegotiated(boolean[] functions) {
|
||||
if (functions != null && functions.length > haus.nightmare.lib3270j.protocol.TN3270EConstants.FUNC_CONTENTION_RESOLUTION) {
|
||||
setContentionResolution(functions[haus.nightmare.lib3270j.protocol.TN3270EConstants.FUNC_CONTENTION_RESOLUTION]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -398,17 +448,138 @@ public class ECLConnection {
|
||||
}
|
||||
public void setSSL(boolean ssl) { SetSSL(ssl); }
|
||||
|
||||
public void setContentionResolution(boolean bl) { this.contentionResolution = bl; }
|
||||
public void setContentionResolution(boolean bl) {
|
||||
this.contentionResolution = bl;
|
||||
if (client != null && client.getTelnetFSM() != null) {
|
||||
client.getTelnetFSM().setContentionResolutionNegotiated(bl);
|
||||
}
|
||||
}
|
||||
public void SetContentionResolution(boolean bl) { setContentionResolution(bl); }
|
||||
public boolean getContentionResolution() { return contentionResolution; }
|
||||
public boolean isContentionResolution() { return contentionResolution; }
|
||||
public boolean getContentionResolution() {
|
||||
if (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().isTn3270eNegotiated()) {
|
||||
return client.getTelnetFSM().isContentionResolutionNegotiated();
|
||||
}
|
||||
return contentionResolution;
|
||||
}
|
||||
public boolean isContentionResolution() { return getContentionResolution(); }
|
||||
public boolean GetContentionResolution() { return getContentionResolution(); }
|
||||
public boolean IsContentionResolution() { return getContentionResolution(); }
|
||||
|
||||
public boolean isAutoSysUnlock() {
|
||||
if (client != null && client.getConfig() != null) {
|
||||
return client.getConfig().isAutoSysUnlock();
|
||||
}
|
||||
String s = properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK);
|
||||
if (s != null) {
|
||||
return "true".equalsIgnoreCase(s) || "1".equals(s);
|
||||
}
|
||||
return autoSysUnlock;
|
||||
}
|
||||
public boolean IsAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
public boolean getAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
public boolean GetAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
|
||||
public void setAutoSysUnlock(boolean unlock) {
|
||||
this.autoSysUnlock = unlock;
|
||||
properties.setProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK, String.valueOf(unlock));
|
||||
if (client != null) {
|
||||
client.setAutoSysUnlock(unlock);
|
||||
}
|
||||
}
|
||||
public void SetAutoSysUnlock(boolean unlock) { setAutoSysUnlock(unlock); }
|
||||
|
||||
public boolean isKeepAlive() {
|
||||
if (client != null && client.getConfig() != null) return client.getConfig().isKeepAliveEnabled();
|
||||
return keepAlive;
|
||||
}
|
||||
public boolean getKeepAlive() { return isKeepAlive(); }
|
||||
public boolean IsKeepAlive() { return isKeepAlive(); }
|
||||
public boolean GetKeepAlive() { return isKeepAlive(); }
|
||||
public void setKeepAlive(boolean ka) {
|
||||
this.keepAlive = ka;
|
||||
this.properties.setProperty(ECLSession.SESSION_KEEPALIVE, String.valueOf(ka));
|
||||
if (client != null) {
|
||||
client.setKeepAliveEnabled(ka);
|
||||
}
|
||||
}
|
||||
public void SetKeepAlive(boolean ka) { setKeepAlive(ka); }
|
||||
|
||||
public int getKeepAliveTimeout() {
|
||||
if (client != null && client.getConfig() != null) return client.getConfig().getKeepAliveIntervalSeconds();
|
||||
return keepAliveTimeout;
|
||||
}
|
||||
public int GetKeepAliveTimeout() { return getKeepAliveTimeout(); }
|
||||
public void setKeepAliveTimeout(int timeout) {
|
||||
this.keepAliveTimeout = timeout;
|
||||
this.properties.setProperty(ECLSession.KEY_KEEPALIVE_TIMEOUT, String.valueOf(timeout));
|
||||
if (client != null) {
|
||||
client.setKeepAliveIntervalSeconds(timeout);
|
||||
}
|
||||
}
|
||||
public void SetKeepAliveTimeout(int timeout) { setKeepAliveTimeout(timeout); }
|
||||
|
||||
public String getKeepAliveType() {
|
||||
if (client != null && client.getConfig() != null) return client.getConfig().getKeepAliveType();
|
||||
return keepAliveType;
|
||||
}
|
||||
public String GetKeepAliveType() { return getKeepAliveType(); }
|
||||
public void setKeepAliveType(String type) {
|
||||
this.keepAliveType = type;
|
||||
this.properties.setProperty(ECLSession.KEY_KEEPALIVE_TYPE, type != null ? type : "");
|
||||
if (client != null && client.getConfig() != null) {
|
||||
client.getConfig().setKeepAliveType(type);
|
||||
}
|
||||
}
|
||||
public void SetKeepAliveType(String type) { setKeepAliveType(type); }
|
||||
|
||||
public boolean isAutoReconnect() {
|
||||
if (client != null && client.getConfig() != null) return client.getConfig().isAutoReconnect();
|
||||
return autoReconnect;
|
||||
}
|
||||
public boolean getAutoReconnect() { return isAutoReconnect(); }
|
||||
public boolean IsAutoReconnect() { return isAutoReconnect(); }
|
||||
public boolean GetAutoReconnect() { return isAutoReconnect(); }
|
||||
public void setAutoReconnect(boolean ar) {
|
||||
this.autoReconnect = ar;
|
||||
this.properties.setProperty(ECLSession.SESSION_AUTORECONNECT, String.valueOf(ar));
|
||||
if (client != null) {
|
||||
client.setAutoReconnect(ar);
|
||||
}
|
||||
}
|
||||
public void SetAutoReconnect(boolean ar) { setAutoReconnect(ar); }
|
||||
|
||||
public int getMaxRetry() {
|
||||
if (client != null && client.getConfig() != null) return client.getConfig().getReconnectMaxRetries();
|
||||
return maxRetry;
|
||||
}
|
||||
public int GetMaxRetry() { return getMaxRetry(); }
|
||||
public void setMaxRetry(int retries) {
|
||||
this.maxRetry = retries;
|
||||
this.properties.setProperty(ECLSession.SESSION_RECONNECT_RETRIES, String.valueOf(retries));
|
||||
if (client != null && client.getConfig() != null) {
|
||||
client.getConfig().setReconnectMaxRetries(retries);
|
||||
}
|
||||
}
|
||||
public void SetMaxRetry(int retries) { setMaxRetry(retries); }
|
||||
public int getReconnectMaxRetries() { return getMaxRetry(); }
|
||||
public void setReconnectMaxRetries(int retries) { setMaxRetry(retries); }
|
||||
|
||||
public void set_LULU_Session(boolean bl) { this.luluSession = bl; }
|
||||
public boolean is_LULU_Session() { return luluSession; }
|
||||
public boolean get_LULU_Session() { return luluSession; }
|
||||
|
||||
public boolean isNegotiateCResolution() { return isNegCR; }
|
||||
public void setNegotiatedCResolution(boolean bl) { this.isNegCR = bl; }
|
||||
public boolean isNegotiateCResolution() {
|
||||
if (client != null && client.getTelnetFSM() != null) {
|
||||
return client.getTelnetFSM().isNegotiateContentionResolution();
|
||||
}
|
||||
return isNegCR;
|
||||
}
|
||||
public void setNegotiatedCResolution(boolean bl) {
|
||||
this.isNegCR = bl;
|
||||
if (client != null && client.getTelnetFSM() != null) {
|
||||
client.getTelnetFSM().setNegotiateContentionResolution(bl);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBIND7FArchitectureViolation() { return isBIND7FArchitectureViolation; }
|
||||
public void setBIND7FArchitectureViolation(boolean bl) { this.isBIND7FArchitectureViolation = bl; }
|
||||
|
||||
@@ -2,6 +2,9 @@ package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
@@ -178,7 +181,20 @@ public class ECLOIA implements ECLConstants {
|
||||
}
|
||||
}
|
||||
|
||||
private final ReentrantLock oiaLock = new ReentrantLock();
|
||||
private final Condition oiaCondition = oiaLock.newCondition();
|
||||
|
||||
public void signalWaiters() {
|
||||
oiaLock.lock();
|
||||
try {
|
||||
oiaCondition.signalAll();
|
||||
} finally {
|
||||
oiaLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void notifyOIAChanged() {
|
||||
signalWaiters();
|
||||
ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(),
|
||||
getAlphanumericType(), isInsertMode(), getStatusString());
|
||||
for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) {
|
||||
@@ -413,19 +429,28 @@ public class ECLOIA implements ECLConstants {
|
||||
* @return true if keyboard unlocked, false if timeout occurred.
|
||||
*/
|
||||
public boolean waitForInput(long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (getInputInhibited() == INHIBIT_NOT_INHIBITED) {
|
||||
return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
if (getInputInhibited() == INHIBIT_NOT_INHIBITED) {
|
||||
return true;
|
||||
}
|
||||
oiaLock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (getInputInhibited() != INHIBIT_NOT_INHIBITED) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = oiaCondition.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
oiaLock.unlock();
|
||||
}
|
||||
return getInputInhibited() == INHIBIT_NOT_INHIBITED;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -458,20 +483,26 @@ public class ECLOIA implements ECLConstants {
|
||||
* Block until any OIA transition occurs.
|
||||
*/
|
||||
public boolean waitForTransition(long timeoutMs) {
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
int initialInhibit = getInputInhibited();
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (getInputInhibited() != initialInhibit) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
oiaLock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (getInputInhibited() == initialInhibit) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = oiaCondition.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
oiaLock.unlock();
|
||||
}
|
||||
return getInputInhibited() != initialInhibit;
|
||||
}
|
||||
|
||||
public boolean WaitForTransition(long timeoutMs) {
|
||||
|
||||
@@ -2,8 +2,12 @@ package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
@@ -37,6 +41,31 @@ public class ECLPS implements ECLConstants {
|
||||
private final java.util.Map<ECLPSListener, ECLScreenDesc> descriptorListeners = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private final java.util.Map<ECLPSListener, Integer> listenerEventTypes = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
// Synchronization primitives for reactive automation waits (Phase 12)
|
||||
private final ReentrantLock fallbackLock = new ReentrantLock();
|
||||
private final Condition fallbackCondition = fallbackLock.newCondition();
|
||||
|
||||
public ReentrantLock getSyncLock() {
|
||||
return (screen != null) ? screen.getSyncLock() : fallbackLock;
|
||||
}
|
||||
|
||||
public Condition getSyncCondition() {
|
||||
return (screen != null) ? screen.getSyncCondition() : fallbackCondition;
|
||||
}
|
||||
|
||||
public void signalWaiters() {
|
||||
if (screen != null) {
|
||||
screen.signalWaiters();
|
||||
} else {
|
||||
fallbackLock.lock();
|
||||
try {
|
||||
fallbackCondition.signalAll();
|
||||
} finally {
|
||||
fallbackLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) {
|
||||
this.screen = screen;
|
||||
this.inputProcessor = inputProcessor;
|
||||
@@ -46,6 +75,27 @@ public class ECLPS implements ECLConstants {
|
||||
this.bidiServices = new DefaultPSBIDIServices(this);
|
||||
this.hindiServices = new DefaultPSHindiServices(this);
|
||||
this.thaiServices = new DefaultPSTHAIServices(this);
|
||||
|
||||
if (this.screen != null) {
|
||||
this.screen.addUpdateListener(new ScreenUpdateListener() {
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
signalWaiters();
|
||||
}
|
||||
@Override
|
||||
public void onCursorMoved(int oldAddress, int newAddress) {
|
||||
signalWaiters();
|
||||
}
|
||||
@Override
|
||||
public void onScreenSizeChanged(int rows, int cols) {
|
||||
signalWaiters();
|
||||
}
|
||||
@Override
|
||||
public void onKeyboardUnlocked() {
|
||||
signalWaiters();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public ECLPS(ECLSession session) {
|
||||
@@ -494,11 +544,71 @@ public class ECLPS implements ECLConstants {
|
||||
return copyString(sRow, sCol, eRow, eCol);
|
||||
}
|
||||
|
||||
private boolean enablePasteFromExcel = true;
|
||||
private boolean pasteStopAtProtectedLine = false;
|
||||
|
||||
public boolean isEnablePasteFromExcel() {
|
||||
if (session != null && session.getProperties() != null) {
|
||||
String p = session.getProperties().getProperty(ECLSession.ENABLE_PASTE_FROM_EXCEL);
|
||||
if (p != null) return Boolean.parseBoolean(p);
|
||||
}
|
||||
return enablePasteFromExcel;
|
||||
}
|
||||
|
||||
public boolean IsEnablePasteFromExcel() { return isEnablePasteFromExcel(); }
|
||||
|
||||
public void setEnablePasteFromExcel(boolean val) {
|
||||
this.enablePasteFromExcel = val;
|
||||
if (session != null && session.getProperties() != null) {
|
||||
session.getProperties().setProperty(ECLSession.ENABLE_PASTE_FROM_EXCEL, String.valueOf(val));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetEnablePasteFromExcel(boolean val) { setEnablePasteFromExcel(val); }
|
||||
|
||||
public boolean isPasteStopAtProtectedLine() {
|
||||
if (session != null && session.getProperties() != null) {
|
||||
String p = session.getProperties().getProperty(ECLSession.PASTE_STOP_AT_PROTECTED_LINE);
|
||||
if (p != null) return Boolean.parseBoolean(p);
|
||||
}
|
||||
return pasteStopAtProtectedLine;
|
||||
}
|
||||
|
||||
public boolean IsPasteStopAtProtectedLine() { return isPasteStopAtProtectedLine(); }
|
||||
|
||||
public void setPasteStopAtProtectedLine(boolean val) {
|
||||
this.pasteStopAtProtectedLine = val;
|
||||
if (session != null && session.getProperties() != null) {
|
||||
session.getProperties().setProperty(ECLSession.PASTE_STOP_AT_PROTECTED_LINE, String.valueOf(val));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPasteStopAtProtectedLine(boolean val) { setPasteStopAtProtectedLine(val); }
|
||||
|
||||
public synchronized int pasteFromExcel(String text, int row, int col) {
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
if (row >= 0 && col >= 0) {
|
||||
setCursorPos(row, col);
|
||||
}
|
||||
if (inputProcessor != null) {
|
||||
return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int PasteFromExcel(String text, int row, int col) {
|
||||
return pasteFromExcel(text, row, col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste a multi-line rectangular block of text starting at (row, col).
|
||||
*/
|
||||
public synchronized int pasteString(String text, int row, int col) {
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine())) {
|
||||
setCursorPos(row, col);
|
||||
return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine());
|
||||
}
|
||||
int rows = screen.getRows();
|
||||
int cols = screen.getCols();
|
||||
if (rows <= 0 || cols <= 0) return 0;
|
||||
@@ -532,7 +642,33 @@ public class ECLPS implements ECLConstants {
|
||||
}
|
||||
|
||||
public int pasteRectangular(String text, int row, int col) {
|
||||
return pasteString(text, row, col);
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
int rows = screen.getRows();
|
||||
int cols = screen.getCols();
|
||||
if (rows <= 0 || cols <= 0) return 0;
|
||||
|
||||
String[] lines = text.split("\r?\n");
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
int targetRow = (row + i) % rows;
|
||||
String line = lines[i];
|
||||
for (int c = 0; c < line.length() && (col + c) < cols; c++) {
|
||||
int pos = targetRow * cols + (col + c);
|
||||
if (screen.isFormatted()) {
|
||||
byte fa = screen.getFieldAttributeAt(pos);
|
||||
if (faIsProtected(fa & 0xFF) || screen.getCell(pos).isFieldAttribute()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
setCursorPos(pos);
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.typeCharacter(line.charAt(c));
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public int PasteRectangular(String text, int row, int col) {
|
||||
@@ -848,6 +984,20 @@ public class ECLPS implements ECLConstants {
|
||||
UnregisterPSEvent(listener);
|
||||
}
|
||||
|
||||
public void dispatchEvent(ECLPSEvent event) {
|
||||
notifyPSEvent(event);
|
||||
}
|
||||
|
||||
public void notifyKeyUnlocked() {
|
||||
int r = (screen != null) ? screen.getRows() : 0;
|
||||
int c = (screen != null) ? screen.getCols() : 0;
|
||||
int cur = (screen != null) ? screen.getCursorAddress() : 0;
|
||||
int ring = ringCounter.incrementAndGet();
|
||||
signalWaiters();
|
||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.EVENT_KEY_UNLOCKED, HOST_EVENTS, 0, 0,
|
||||
Math.max(0, r - 1), Math.max(0, c - 1), cur, cur, r, c, false, cursorVisible, ring, false, null));
|
||||
}
|
||||
|
||||
public void notifyPSEvent(ECLPSEvent event) {
|
||||
for (ECLPSListener l : psListeners) {
|
||||
ECLScreenDesc desc = descriptorListeners.get(l);
|
||||
@@ -899,6 +1049,7 @@ public class ECLPS implements ECLConstants {
|
||||
int c = (screen != null) ? screen.getCols() : 0;
|
||||
int cur = (screen != null) ? screen.getCursorAddress() : 0;
|
||||
int ring = ringCounter.incrementAndGet();
|
||||
signalWaiters();
|
||||
ECLPSEvent evt = new ECLPSEvent(this, ECLPSEvent.PS_UPDATE, type, startRow, startCol, endRow, endCol,
|
||||
cur, cur, r, c, full, cursorVisible, ring, startPrinter, null);
|
||||
notifyPSEvent(evt);
|
||||
@@ -910,12 +1061,14 @@ public class ECLPS implements ECLConstants {
|
||||
int row = (c > 0) ? newAddress / c : 0;
|
||||
int col = (c > 0) ? newAddress % c : 0;
|
||||
int ring = ringCounter.incrementAndGet();
|
||||
signalWaiters();
|
||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_CURSOR, USER_EVENTS, row, col, row, col,
|
||||
oldAddress, newAddress, r, c, false, cursorVisible, ring, false, null));
|
||||
}
|
||||
|
||||
public void notifyAlarm() {
|
||||
int ring = ringCounter.incrementAndGet();
|
||||
signalWaiters();
|
||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_ALARM, HOST_EVENTS, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, false, cursorVisible, ring, false, null));
|
||||
}
|
||||
@@ -923,6 +1076,7 @@ public class ECLPS implements ECLConstants {
|
||||
public void notifyScreenResized(int rows, int cols) {
|
||||
int cur = (screen != null) ? screen.getCursorAddress() : 0;
|
||||
int ring = ringCounter.incrementAndGet();
|
||||
signalWaiters();
|
||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_RESIZE, HOST_EVENTS, 0, 0, rows - 1, cols - 1,
|
||||
cur, cur, rows, cols, true, cursorVisible, ring, false, null));
|
||||
}
|
||||
@@ -1067,20 +1221,30 @@ public class ECLPS implements ECLConstants {
|
||||
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
if (desc == null) return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
long start = System.currentTimeMillis();
|
||||
ECLOIA oia = (session != null) ? session.GetOIA() : null;
|
||||
while (System.currentTimeMillis() - start < limit) {
|
||||
if (desc.Matches(this, oia)) {
|
||||
return true;
|
||||
if (desc.Matches(this, oia)) {
|
||||
return true;
|
||||
}
|
||||
ReentrantLock lock = getSyncLock();
|
||||
Condition cond = getSyncCondition();
|
||||
lock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (!desc.Matches(this, oia)) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return desc.Matches(this, oia);
|
||||
}
|
||||
|
||||
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
@@ -1097,20 +1261,30 @@ public class ECLPS implements ECLConstants {
|
||||
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
if (desc == null) return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
long start = System.currentTimeMillis();
|
||||
ECLOIA oia = (session != null) ? session.GetOIA() : null;
|
||||
while (System.currentTimeMillis() - start < limit) {
|
||||
if (!desc.Matches(this, oia)) {
|
||||
return true;
|
||||
if (!desc.Matches(this, oia)) {
|
||||
return true;
|
||||
}
|
||||
ReentrantLock lock = getSyncLock();
|
||||
Condition cond = getSyncCondition();
|
||||
lock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (desc.Matches(this, oia)) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return !desc.Matches(this, oia);
|
||||
}
|
||||
|
||||
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
@@ -1121,69 +1295,178 @@ public class ECLPS implements ECLConstants {
|
||||
* Block until the specified text appears anywhere on the presentation space.
|
||||
*/
|
||||
public boolean waitForScreen(String text, long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (searchString(text) >= 0) {
|
||||
return true;
|
||||
if (text == null || text.isEmpty()) return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
if (searchString(text) >= 0) {
|
||||
return true;
|
||||
}
|
||||
ReentrantLock lock = getSyncLock();
|
||||
Condition cond = getSyncCondition();
|
||||
lock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (searchString(text) < 0) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return searchString(text) >= 0;
|
||||
}
|
||||
|
||||
public boolean WaitForScreen(String text, long timeoutMs) {
|
||||
return waitForScreen(text, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text) {
|
||||
return waitForScreen(text, -1L);
|
||||
}
|
||||
|
||||
public boolean WaitForString(String text) {
|
||||
return waitForScreen(text, -1L);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text, long timeoutMs) {
|
||||
return waitForScreen(text, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean WaitForString(String text, long timeoutMs) {
|
||||
return waitForScreen(text, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until the specified text appears at the given (row, col) coordinate.
|
||||
*/
|
||||
public boolean waitForScreen(String text, int row, int col, long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
String onScreen = getString(row, col, text.length());
|
||||
if (text.equals(onScreen)) {
|
||||
return true;
|
||||
if (text == null) return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
if (text.equals(getString(row, col, text.length()))) {
|
||||
return true;
|
||||
}
|
||||
ReentrantLock lock = getSyncLock();
|
||||
Condition cond = getSyncCondition();
|
||||
lock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (!text.equals(getString(row, col, text.length()))) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return text.equals(getString(row, col, text.length()));
|
||||
}
|
||||
|
||||
public boolean WaitForScreen(String text, int row, int col, long timeoutMs) {
|
||||
return waitForScreen(text, row, col, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text, int row, int col, long timeoutMs) {
|
||||
return waitForScreen(text, row, col, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean WaitForString(String text, int row, int col, long timeoutMs) {
|
||||
return waitForScreen(text, row, col, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until the cursor moves to (row, col).
|
||||
*/
|
||||
public boolean waitForCursor(int row, int col, long timeoutMs) {
|
||||
long start = System.currentTimeMillis();
|
||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
||||
if (getCursorRow() == row && getCursorCol() == col) {
|
||||
return true;
|
||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||
if (getCursorRow() == row && getCursorCol() == col) {
|
||||
return true;
|
||||
}
|
||||
ReentrantLock lock = getSyncLock();
|
||||
Condition cond = getSyncCondition();
|
||||
lock.lock();
|
||||
try {
|
||||
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||
while (getCursorRow() != row || getCursorCol() != col) {
|
||||
if (remainingNanos <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return getCursorRow() == row && getCursorCol() == col;
|
||||
}
|
||||
|
||||
public boolean WaitForCursor(int row, int col, long timeoutMs) {
|
||||
return waitForCursor(row, col, timeoutMs);
|
||||
}
|
||||
|
||||
protected boolean locked_SYSLOCK = false;
|
||||
protected boolean locked_TWAIT = false;
|
||||
|
||||
public void lockKeyboard() {
|
||||
lockKeyboard(8);
|
||||
}
|
||||
|
||||
public void lockKeyboard(int reason) {
|
||||
if (reason == 7) locked_TWAIT = true;
|
||||
if (reason == 8) {
|
||||
locked_SYSLOCK = true;
|
||||
if (session != null && session.getOIA() != null) {
|
||||
session.getOIA().setDoNotEnter(8, 0);
|
||||
}
|
||||
}
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void unlockKeyboard() {
|
||||
unlockKeyboard(8);
|
||||
}
|
||||
|
||||
public void unlockKeyboard(int reason) {
|
||||
if (reason == 7) locked_TWAIT = false;
|
||||
if (reason == 8) {
|
||||
locked_SYSLOCK = false;
|
||||
if (session != null && session.getOIA() != null) {
|
||||
session.getOIA().clearDoNotEnter();
|
||||
}
|
||||
}
|
||||
if (!locked_TWAIT && !locked_SYSLOCK) {
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean islocked_TWAIT() {
|
||||
return locked_TWAIT;
|
||||
}
|
||||
|
||||
public boolean islocked_SYSLOCK() {
|
||||
if (locked_SYSLOCK) return true;
|
||||
if (session != null && session.getOIA() != null && session.getOIA().isXSystem()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public class ECLPSEvent extends EventObject {
|
||||
public static final int EVENT_ALARM = PS_ALARM;
|
||||
public static final int EVENT_RESIZE = PS_RESIZE;
|
||||
public static final int EVENT_CLOSE = PS_CLOSE;
|
||||
public static final int EVENT_KEY_UNLOCKED = PS_UPDATE; // HoD event type 1 for keyboard unlock / update
|
||||
|
||||
private final int eventType;
|
||||
private final int type;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import java.awt.Image;
|
||||
import java.awt.Rectangle;
|
||||
import haus.nightmare.lib3270j.graphics.PixelBuffer;
|
||||
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||
|
||||
/**
|
||||
* Conforms to IBM Host On-Demand ECLPSGraphicsEvent.
|
||||
* Decoupled from java.awt.
|
||||
*/
|
||||
public class ECLPSGraphicsEvent {
|
||||
public static final int GRAPHICS_CURSOR_ON = 1;
|
||||
@@ -14,7 +15,7 @@ public class ECLPSGraphicsEvent {
|
||||
public static final int GRAPHICS_UPDATED = 5;
|
||||
|
||||
private int id;
|
||||
private Image image;
|
||||
private Object image;
|
||||
private Rectangle rect;
|
||||
private ECLPS source;
|
||||
|
||||
@@ -23,13 +24,13 @@ public class ECLPSGraphicsEvent {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public ECLPSGraphicsEvent(ECLPS source, int id, Image image) {
|
||||
public ECLPSGraphicsEvent(ECLPS source, int id, Object image) {
|
||||
this.source = source;
|
||||
this.id = id;
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
public ECLPSGraphicsEvent(ECLPS source, int id, Image image, Rectangle rectangle) {
|
||||
public ECLPSGraphicsEvent(ECLPS source, int id, Object image, Rectangle rectangle) {
|
||||
this.source = source;
|
||||
this.id = id;
|
||||
this.image = image;
|
||||
@@ -46,9 +47,13 @@ public class ECLPSGraphicsEvent {
|
||||
public int getID() { return this.id; }
|
||||
public int GetID() { return this.id; }
|
||||
|
||||
public void setImage(Image image) { this.image = image; }
|
||||
public Image getImage() { return this.image; }
|
||||
public Image GetImage() { return this.image; }
|
||||
public void setImage(Object image) { this.image = image; }
|
||||
public Object getImage() { return this.image; }
|
||||
public Object GetImage() { return this.image; }
|
||||
|
||||
public PixelBuffer getPixelBuffer() {
|
||||
return (this.image instanceof PixelBuffer) ? (PixelBuffer) this.image : null;
|
||||
}
|
||||
|
||||
public void setRectangle(Rectangle rect) { this.rect = rect; }
|
||||
public Rectangle getRectangle() { return this.rect; }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import haus.nightmare.lib3270j.graphics.Color;
|
||||
|
||||
/**
|
||||
* Presentation Space graphics services interface conforming to IBM Host On-Demand ECL.
|
||||
* Completely decoupled from java.awt.
|
||||
*/
|
||||
public interface ECLPSGraphicsServices {
|
||||
void setVisualComponent(Component comp);
|
||||
void setVisualComponent(Object comp);
|
||||
void setGraphicColor(Color[] colors, boolean b);
|
||||
void mousePressed(int x, int y, int button);
|
||||
void addGraphicsListener(ECLPSGraphicsListener listener);
|
||||
|
||||
@@ -18,7 +18,7 @@ public class ECLSession {
|
||||
|
||||
private static final Logger log = Logger.getLogger(ECLSession.class.getName());
|
||||
|
||||
// Standard IBM HoD Session Property Keys
|
||||
// Standard Session Property Keys
|
||||
public static final String SESSION_HOST = "SESSION_HOST";
|
||||
public static final String SESSION_PORT = "SESSION_PORT";
|
||||
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
|
||||
@@ -29,6 +29,24 @@ public class ECLSession {
|
||||
public static final String SESSION_TN3270E = "SESSION_TN3270E";
|
||||
public static final String SESSION_WIN_TITLE = "SESSION_WIN_TITLE";
|
||||
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
|
||||
public static final String SESSION_AUTO_SYS_UNLOCK = "autoSysUnlock";
|
||||
public static final String SESSION_KEEPALIVE = "SESSION_KEEPALIVE";
|
||||
public static final String KEY_KEEPALIVE_TYPE = "keepAliveType";
|
||||
public static final String KEY_KEEPALIVE_TIMEOUT = "keepAliveTimeout";
|
||||
public static final String SESSION_AUTORECONNECT = "SESSION_AUTORECONNECT";
|
||||
public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES";
|
||||
public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel";
|
||||
public static final String PASTE_TAB_OPTIONS = "pasteTabOptions";
|
||||
public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine";
|
||||
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
|
||||
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
|
||||
public static final String ENTRYASSIST_DOCMODE = "EntryAssist_DOCmode";
|
||||
public static final String ENTRYASSIST_DOCWORDWRAP = "EntryAssist_DOCwordWrap";
|
||||
public static final String ENTRYASSIST_STARTCOL = "EntryAssist_startCol";
|
||||
public static final String ENTRYASSIST_ENDCOL = "EntryAssist_endCol";
|
||||
public static final String ENTRYASSIST_BELL = "EntryAssist_bell";
|
||||
public static final String ENTRYASSIST_BELLCOL = "EntryAssist_bellCol";
|
||||
public static final String ENTRYASSIST_TABSTOPS = "EntryAssist_tabstops";
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private final ECLConnection connection;
|
||||
@@ -98,6 +116,9 @@ public class ECLSession {
|
||||
String tn3270eStr = getProp(props, SESSION_TN3270E, "tn3270e", "TN3270E", "true");
|
||||
config.setTn3270eEnabled("true".equalsIgnoreCase(tn3270eStr) || "yes".equalsIgnoreCase(tn3270eStr) || "1".equals(tn3270eStr));
|
||||
|
||||
String autoSysStr = getProp(props, SESSION_AUTO_SYS_UNLOCK, "autoSysUnlock", "AutoSysUnlock", "true");
|
||||
config.setAutoSysUnlock("true".equalsIgnoreCase(autoSysStr) || "yes".equalsIgnoreCase(autoSysStr) || "1".equals(autoSysStr));
|
||||
|
||||
String certUrl = getProp(props, "certificateURL", "CERTIFICATE_URL", "certificate_url", null);
|
||||
if (certUrl != null) config.setKeyStorePath(certUrl);
|
||||
String certPwd = getProp(props, "certificatePassword", "CERTIFICATE_PASSWORD", "certificate_password", null);
|
||||
@@ -118,6 +139,25 @@ public class ECLSession {
|
||||
config.setEnabledProtocols(tlsVer);
|
||||
}
|
||||
|
||||
String keepAliveStr = getProp(props, SESSION_KEEPALIVE, "keepAlive", "keepalive", "true");
|
||||
config.setKeepAliveEnabled("true".equalsIgnoreCase(keepAliveStr) || "yes".equalsIgnoreCase(keepAliveStr) || "1".equals(keepAliveStr));
|
||||
|
||||
String kaTimeoutStr = getProp(props, KEY_KEEPALIVE_TIMEOUT, "keepAliveTimeout", "keepalivetimeout", null);
|
||||
if (kaTimeoutStr != null) {
|
||||
try { config.setKeepAliveIntervalSeconds(Integer.parseInt(kaTimeoutStr.trim())); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
String kaTypeStr = getProp(props, KEY_KEEPALIVE_TYPE, "keepAliveType", "keepalivetype", null);
|
||||
if (kaTypeStr != null) config.setKeepAliveType(kaTypeStr);
|
||||
|
||||
String autoReconnectStr = getProp(props, SESSION_AUTORECONNECT, "autoReconnect", "autoreconnect", "false");
|
||||
config.setAutoReconnect("true".equalsIgnoreCase(autoReconnectStr) || "yes".equalsIgnoreCase(autoReconnectStr) || "1".equals(autoReconnectStr));
|
||||
|
||||
String retriesStr = getProp(props, SESSION_RECONNECT_RETRIES, "reconnectMaxRetries", "reconnectRetries", null);
|
||||
if (retriesStr != null) {
|
||||
try { config.setReconnectMaxRetries(Integer.parseInt(retriesStr.trim())); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -161,6 +201,7 @@ public class ECLSession {
|
||||
properties.setProperty(SESSION_SSL, String.valueOf(cfg.isUseTls()));
|
||||
if (cfg.getLuName() != null) properties.setProperty(SESSION_LU_NAME, cfg.getLuName());
|
||||
properties.setProperty(SESSION_TN3270E, String.valueOf(cfg.isTn3270eEnabled()));
|
||||
properties.setProperty(SESSION_AUTO_SYS_UNLOCK, String.valueOf(cfg.isAutoSysUnlock()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,6 +412,42 @@ public class ECLSession {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isKeepAlive() {
|
||||
return (client != null) ? client.isKeepAliveEnabled() : true;
|
||||
}
|
||||
public boolean IsKeepAlive() { return isKeepAlive(); }
|
||||
public void setKeepAlive(boolean keepAlive) {
|
||||
this.properties.setProperty(SESSION_KEEPALIVE, String.valueOf(keepAlive));
|
||||
if (client != null) {
|
||||
client.setKeepAliveEnabled(keepAlive);
|
||||
}
|
||||
}
|
||||
public void SetKeepAlive(boolean keepAlive) { setKeepAlive(keepAlive); }
|
||||
|
||||
public int getKeepAliveTimeout() {
|
||||
return (client != null) ? client.getKeepAliveIntervalSeconds() : 120;
|
||||
}
|
||||
public int GetKeepAliveTimeout() { return getKeepAliveTimeout(); }
|
||||
public void setKeepAliveTimeout(int timeout) {
|
||||
this.properties.setProperty(KEY_KEEPALIVE_TIMEOUT, String.valueOf(timeout));
|
||||
if (client != null) {
|
||||
client.setKeepAliveIntervalSeconds(timeout);
|
||||
}
|
||||
}
|
||||
public void SetKeepAliveTimeout(int timeout) { setKeepAliveTimeout(timeout); }
|
||||
|
||||
public boolean isAutoReconnect() {
|
||||
return (client != null) ? client.isAutoReconnect() : false;
|
||||
}
|
||||
public boolean IsAutoReconnect() { return isAutoReconnect(); }
|
||||
public void setAutoReconnect(boolean autoReconnect) {
|
||||
this.properties.setProperty(SESSION_AUTORECONNECT, String.valueOf(autoReconnect));
|
||||
if (client != null) {
|
||||
client.setAutoReconnect(autoReconnect);
|
||||
}
|
||||
}
|
||||
public void SetAutoReconnect(boolean autoReconnect) { setAutoReconnect(autoReconnect); }
|
||||
|
||||
// ========== Automation Keystrokes & Waits ==========
|
||||
|
||||
/**
|
||||
@@ -455,6 +532,41 @@ public class ECLSession {
|
||||
dispose();
|
||||
}
|
||||
|
||||
public boolean isAutoSysUnlock() {
|
||||
if (client != null && client.getConfig() != null) {
|
||||
return client.getConfig().isAutoSysUnlock();
|
||||
}
|
||||
String s = properties.getProperty(SESSION_AUTO_SYS_UNLOCK);
|
||||
return s != null ? Boolean.parseBoolean(s) : true;
|
||||
}
|
||||
public boolean getAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
public boolean IsAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
public boolean GetAutoSysUnlock() { return isAutoSysUnlock(); }
|
||||
|
||||
public void setAutoSysUnlock(boolean unlock) {
|
||||
properties.setProperty(SESSION_AUTO_SYS_UNLOCK, String.valueOf(unlock));
|
||||
if (client != null) {
|
||||
client.setAutoSysUnlock(unlock);
|
||||
}
|
||||
}
|
||||
public void SetAutoSysUnlock(boolean unlock) { setAutoSysUnlock(unlock); }
|
||||
|
||||
public boolean getContentionResolution() {
|
||||
if (connection != null) return connection.getContentionResolution();
|
||||
if (client != null) return client.isContentionResolution();
|
||||
return false;
|
||||
}
|
||||
public boolean isContentionResolution() { return getContentionResolution(); }
|
||||
public boolean GetContentionResolution() { return getContentionResolution(); }
|
||||
public boolean IsContentionResolution() { return getContentionResolution(); }
|
||||
|
||||
public void setContentionResolution(boolean cr) {
|
||||
if (connection != null) {
|
||||
connection.setContentionResolution(cr);
|
||||
}
|
||||
}
|
||||
public void SetContentionResolution(boolean cr) { setContentionResolution(cr); }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ECLSession[host=%s, port=%d, connected=%b, state=%s]",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Platform-neutral Color representation encapsulating 32-bit ARGB.
|
||||
* Completely decouples lib3270j from java.awt.Color.
|
||||
*/
|
||||
public class Color implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final Color WHITE = new Color(0xFFFFFFFF);
|
||||
public static final Color LIGHT_GRAY = new Color(0xFFC0C0C0);
|
||||
public static final Color GRAY = new Color(0xFF808080);
|
||||
public static final Color DARK_GRAY = new Color(0xFF404040);
|
||||
public static final Color BLACK = new Color(0xFF000000);
|
||||
public static final Color RED = new Color(0xFFFF0000);
|
||||
public static final Color PINK = new Color(0xFFFFAFAF);
|
||||
public static final Color ORANGE = new Color(0xFFFFC800);
|
||||
public static final Color YELLOW = new Color(0xFFFFFF00);
|
||||
public static final Color GREEN = new Color(0xFF00FF00);
|
||||
public static final Color MAGENTA = new Color(0xFFFF00FF);
|
||||
public static final Color CYAN = new Color(0xFF00FFFF);
|
||||
public static final Color BLUE = new Color(0xFF0000FF);
|
||||
|
||||
private final int value;
|
||||
|
||||
public Color(int rgb) {
|
||||
this.value = 0xFF000000 | rgb;
|
||||
}
|
||||
|
||||
public Color(int rgba, boolean hasAlpha) {
|
||||
if (hasAlpha) {
|
||||
this.value = rgba;
|
||||
} else {
|
||||
this.value = 0xFF000000 | rgba;
|
||||
}
|
||||
}
|
||||
|
||||
public Color(int r, int g, int b) {
|
||||
this(r, g, b, 255);
|
||||
}
|
||||
|
||||
public Color(int r, int g, int b, int a) {
|
||||
this.value = ((a & 0xFF) << 24) |
|
||||
((r & 0xFF) << 16) |
|
||||
((g & 0xFF) << 8) |
|
||||
(b & 0xFF);
|
||||
}
|
||||
|
||||
public Color(float r, float g, float b) {
|
||||
this((int) (r * 255 + 0.5), (int) (g * 255 + 0.5), (int) (b * 255 + 0.5));
|
||||
}
|
||||
|
||||
public Color(float r, float g, float b, float a) {
|
||||
this((int) (r * 255 + 0.5), (int) (g * 255 + 0.5), (int) (b * 255 + 0.5), (int) (a * 255 + 0.5));
|
||||
}
|
||||
|
||||
public int getRGB() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public int getRed() {
|
||||
return (value >> 16) & 0xFF;
|
||||
}
|
||||
|
||||
public int getGreen() {
|
||||
return (value >> 8) & 0xFF;
|
||||
}
|
||||
|
||||
public int getBlue() {
|
||||
return value & 0xFF;
|
||||
}
|
||||
|
||||
public int getAlpha() {
|
||||
return (value >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Color)) return false;
|
||||
return this.value == ((Color) obj).value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + "[r=" + getRed() + ",g=" + getGreen() + ",b=" + getBlue() + ",a=" + getAlpha() + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Default standalone implementation of PixelBuffer.
|
||||
* Provides pure Java software rasterization into a 32-bit ARGB contiguous integer array.
|
||||
*/
|
||||
public class DefaultPixelBuffer implements PixelBuffer {
|
||||
|
||||
private int width;
|
||||
private int height;
|
||||
private int[] pixels;
|
||||
|
||||
private boolean hasClip = false;
|
||||
private int clipX;
|
||||
private int clipY;
|
||||
private int clipWidth;
|
||||
private int clipHeight;
|
||||
|
||||
public DefaultPixelBuffer(int width, int height) {
|
||||
this.width = Math.max(1, width);
|
||||
this.height = Math.max(1, height);
|
||||
this.pixels = new int[this.width * this.height];
|
||||
}
|
||||
|
||||
public DefaultPixelBuffer(int width, int height, int[] existingPixels) {
|
||||
this.width = Math.max(1, width);
|
||||
this.height = Math.max(1, height);
|
||||
if (existingPixels != null && existingPixels.length >= this.width * this.height) {
|
||||
this.pixels = existingPixels;
|
||||
} else {
|
||||
this.pixels = new int[this.width * this.height];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getPixels() {
|
||||
return pixels;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getPixel(int x, int y) {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return 0;
|
||||
return pixels[y * width + x];
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setPixelDirect(int x, int y, int argb) {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
||||
if (isClipped(x, y)) return;
|
||||
pixels[y * width + x] = argb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setPixel(int x, int y, int argb) {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
||||
if (isClipped(x, y)) return;
|
||||
|
||||
int srcA = (argb >>> 24) & 0xFF;
|
||||
if (srcA == 0) return;
|
||||
|
||||
int idx = y * width + x;
|
||||
if (srcA == 255) {
|
||||
pixels[idx] = argb;
|
||||
return;
|
||||
}
|
||||
|
||||
int dst = pixels[idx];
|
||||
int dstA = (dst >>> 24) & 0xFF;
|
||||
if (dstA == 0) {
|
||||
pixels[idx] = argb;
|
||||
return;
|
||||
}
|
||||
|
||||
int srcR = (argb >>> 16) & 0xFF;
|
||||
int srcG = (argb >>> 8) & 0xFF;
|
||||
int srcB = argb & 0xFF;
|
||||
|
||||
int dstR = (dst >>> 16) & 0xFF;
|
||||
int dstG = (dst >>> 8) & 0xFF;
|
||||
int dstB = dst & 0xFF;
|
||||
|
||||
int outA = srcA + dstA * (255 - srcA) / 255;
|
||||
if (outA == 0) {
|
||||
pixels[idx] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
int outR = (srcR * srcA + dstR * dstA * (255 - srcA) / 255) / outA;
|
||||
int outG = (srcG * srcA + dstG * dstA * (255 - srcA) / 255) / outA;
|
||||
int outB = (srcB * srcA + dstB * dstA * (255 - srcA) / 255) / outA;
|
||||
|
||||
pixels[idx] = (outA << 24) | (outR << 16) | (outG << 8) | outB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear() {
|
||||
clear(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear(int argb) {
|
||||
Arrays.fill(pixels, argb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setClip(int x, int y, int width, int height) {
|
||||
this.hasClip = true;
|
||||
this.clipX = x;
|
||||
this.clipY = y;
|
||||
this.clipWidth = Math.max(0, width);
|
||||
this.clipHeight = Math.max(0, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clearClip() {
|
||||
this.hasClip = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClipped(int x, int y) {
|
||||
if (!hasClip) return false;
|
||||
return x < clipX || x >= (clipX + clipWidth) || y < clipY || y >= (clipY + clipHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void fillRect(int x, int y, int w, int h, int argb) {
|
||||
if (w <= 0 || h <= 0) return;
|
||||
int x1 = Math.max(0, x);
|
||||
int y1 = Math.max(0, y);
|
||||
int x2 = Math.min(width, x + w);
|
||||
int y2 = Math.min(height, y + h);
|
||||
|
||||
if (hasClip) {
|
||||
x1 = Math.max(x1, clipX);
|
||||
y1 = Math.max(y1, clipY);
|
||||
x2 = Math.min(x2, clipX + clipWidth);
|
||||
y2 = Math.min(y2, clipY + clipHeight);
|
||||
}
|
||||
|
||||
for (int row = y1; row < y2; row++) {
|
||||
int rowOffset = row * width;
|
||||
Arrays.fill(pixels, rowOffset + x1, rowOffset + x2, argb);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void blit(int[] srcPixels, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY) {
|
||||
if (srcPixels == null || srcW <= 0 || srcH <= 0) return;
|
||||
|
||||
for (int r = 0; r < srcH; r++) {
|
||||
int sy = srcY + r;
|
||||
int dy = dstY + r;
|
||||
if (dy < 0 || dy >= height) continue;
|
||||
|
||||
for (int c = 0; c < srcW; c++) {
|
||||
int sx = srcX + c;
|
||||
int dx = dstX + c;
|
||||
if (dx < 0 || dx >= width) continue;
|
||||
if (isClipped(dx, dy)) continue;
|
||||
|
||||
int sp = srcPixels[sy * srcW + sx];
|
||||
setPixel(dx, dy, sp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void drawLine(int x1, int y1, int x2, int y2, int argb) {
|
||||
drawLineBresenham(x1, y1, x2, y2, argb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void drawLineBresenham(int x0, int y0, int x1, int y1, int color) {
|
||||
int dx = Math.abs(x1 - x0);
|
||||
int dy = Math.abs(y1 - y0);
|
||||
int sx = (x0 < x1) ? 1 : -1;
|
||||
int sy = (y0 < y1) ? 1 : -1;
|
||||
int err = dx - dy;
|
||||
|
||||
int curX = x0;
|
||||
int curY = y0;
|
||||
|
||||
while (true) {
|
||||
setPixel(curX, curY, color);
|
||||
if (curX == x1 && curY == y1) break;
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
curX += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
curY += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
|
||||
int lw = Math.max(1, lineWidth);
|
||||
int ix0 = (int) Math.round(x0);
|
||||
int iy0 = (int) Math.round(y0);
|
||||
int ix1 = (int) Math.round(x1);
|
||||
int iy1 = (int) Math.round(y1);
|
||||
|
||||
if (lw == 1) {
|
||||
drawLineBresenham(ix0, iy0, ix1, iy1, colorArgb);
|
||||
} else {
|
||||
int half = lw / 2;
|
||||
for (int ox = -half; ox <= half; ox++) {
|
||||
for (int oy = -half; oy <= half; oy++) {
|
||||
drawLineBresenham(ix0 + ox, iy0 + oy, ix1 + ox, iy1 + oy, colorArgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void drawLineAA(double x0, double y0, double x1, double y1, int colorArgb, double strokeWidth) {
|
||||
// Pure Java anti-aliased line rendering
|
||||
double dx = x1 - x0;
|
||||
double dy = y1 - y0;
|
||||
double len = Math.hypot(dx, dy);
|
||||
if (len < 1e-4) {
|
||||
setPixel((int) Math.round(x0), (int) Math.round(y0), colorArgb);
|
||||
return;
|
||||
}
|
||||
|
||||
double radius = Math.max(0.5, strokeWidth * 0.5);
|
||||
int minX = (int) Math.floor(Math.min(x0, x1) - radius - 1);
|
||||
int maxX = (int) Math.ceil(Math.max(x0, x1) + radius + 1);
|
||||
int minY = (int) Math.floor(Math.min(y0, y1) - radius - 1);
|
||||
int maxY = (int) Math.ceil(Math.max(y0, y1) + radius + 1);
|
||||
|
||||
minX = Math.max(0, minX);
|
||||
maxX = Math.min(width - 1, maxX);
|
||||
minY = Math.max(0, minY);
|
||||
maxY = Math.min(height - 1, maxY);
|
||||
|
||||
int baseAlpha = (colorArgb >>> 24) & 0xFF;
|
||||
if (baseAlpha == 0) baseAlpha = 255;
|
||||
int rgbOnly = colorArgb & 0x00FFFFFF;
|
||||
|
||||
double invLenSq = 1.0 / (len * len);
|
||||
|
||||
for (int py = minY; py <= maxY; py++) {
|
||||
for (int px = minX; px <= maxX; px++) {
|
||||
double u = ((px - x0) * dx + (py - y0) * dy) * invLenSq;
|
||||
u = Math.max(0.0, Math.min(1.0, u));
|
||||
double projX = x0 + u * dx;
|
||||
double projY = y0 + u * dy;
|
||||
double dist = Math.hypot(px - projX, py - projY);
|
||||
|
||||
if (dist <= radius) {
|
||||
double coverage = 1.0 - (dist / radius);
|
||||
coverage = Math.sin(coverage * Math.PI * 0.5); // Smooth cosine roll-off
|
||||
int effectiveAlpha = (int) (baseAlpha * coverage);
|
||||
if (effectiveAlpha > 0) {
|
||||
setPixel(px, py, (effectiveAlpha << 24) | rgbOnly);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Lightweight pure-Java POJO dimension for 2D width and height.
|
||||
* Completely decouples lib3270j from java.awt.Dimension.
|
||||
*/
|
||||
public class Dimension implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int width;
|
||||
public int height;
|
||||
|
||||
public Dimension() {
|
||||
this(0, 0);
|
||||
}
|
||||
|
||||
public Dimension(Dimension d) {
|
||||
this(d != null ? d.width : 0, d != null ? d.height : 0);
|
||||
}
|
||||
|
||||
public Dimension(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setSize(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public void setSize(Dimension d) {
|
||||
if (d != null) {
|
||||
this.width = d.width;
|
||||
this.height = d.height;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Dimension)) return false;
|
||||
Dimension d = (Dimension) obj;
|
||||
return (width == d.width) && (height == d.height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + "[width=" + width + ",height=" + height + "]";
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class FillArea {
|
||||
/**
|
||||
* IBM Host On-Demand multi-polygon constructor.
|
||||
*/
|
||||
public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, java.awt.Color color) {
|
||||
public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, Color color) {
|
||||
this();
|
||||
if (color != null) {
|
||||
this.fillColor = color.getRGB();
|
||||
@@ -72,9 +72,28 @@ public class FillArea {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized java.awt.Rectangle getBounds() {
|
||||
public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, int argb) {
|
||||
this();
|
||||
this.fillColor = argb;
|
||||
if (px != null && py != null && polyCounts != null) {
|
||||
int offset = 0;
|
||||
for (int i = 0; i < numPolys && i < polyCounts.length; i++) {
|
||||
int count = polyCounts[i];
|
||||
if (count >= 2 && offset + count <= px.length && offset + count <= py.length) {
|
||||
int[] sx = new int[count];
|
||||
int[] sy = new int[count];
|
||||
System.arraycopy(px, offset, sx, 0, count);
|
||||
System.arraycopy(py, offset, sy, 0, count);
|
||||
addPolygon(sx, sy, count);
|
||||
}
|
||||
offset += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized Rectangle getBounds() {
|
||||
if (edges.isEmpty()) {
|
||||
return new java.awt.Rectangle(0, 0, 0, 0);
|
||||
return new Rectangle(0, 0, 0, 0);
|
||||
}
|
||||
double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE;
|
||||
double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE;
|
||||
@@ -88,7 +107,7 @@ public class FillArea {
|
||||
int y = (int) Math.floor(minY);
|
||||
int w = (int) Math.ceil(maxX) - x + 1;
|
||||
int h = (int) Math.ceil(maxY) - y + 1;
|
||||
return new java.awt.Rectangle(x, y, Math.max(0, w), Math.max(0, h));
|
||||
return new Rectangle(x, y, Math.max(0, w), Math.max(0, h));
|
||||
}
|
||||
|
||||
public synchronized void setFillModeOR() {
|
||||
@@ -123,18 +142,20 @@ public class FillArea {
|
||||
this.pixelPattern = pat;
|
||||
}
|
||||
|
||||
public synchronized java.awt.Image getImage() {
|
||||
java.awt.Rectangle b = getBounds();
|
||||
public synchronized PixelBuffer getPixelBuffer() {
|
||||
Rectangle b = getBounds();
|
||||
if (b.width <= 0 || b.height <= 0) {
|
||||
return new java.awt.image.BufferedImage(1, 1, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
return new DefaultPixelBuffer(1, 1);
|
||||
}
|
||||
GraphicsPlane tempPlane = new GraphicsPlane(b.x + b.width, b.y + b.height);
|
||||
fill(tempPlane, fillColor, 0, solidFill ? GocaConstants.PT_SOLID : 0, false, 0, 0, 1, 0, 0, null);
|
||||
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(b.width, b.height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
java.awt.Graphics g = img.getGraphics();
|
||||
g.drawImage(tempPlane.getImage(), -b.x, -b.y, null);
|
||||
g.dispose();
|
||||
return img;
|
||||
DefaultPixelBuffer cropped = new DefaultPixelBuffer(b.width, b.height);
|
||||
cropped.blit(tempPlane.getRgbBuffer(), b.x, b.y, b.width, b.height, 0, 0);
|
||||
return cropped;
|
||||
}
|
||||
|
||||
public synchronized Object getImage() {
|
||||
return getPixelBuffer();
|
||||
}
|
||||
|
||||
public synchronized void dispose() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Point;
|
||||
|
||||
/**
|
||||
* Dedicated scaling layer for IBM 3179G / GDDM GOCA graphics presentation space.
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Image;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBuffer;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.awt.image.DirectColorModel;
|
||||
import java.awt.image.Raster;
|
||||
import java.awt.image.SinglePixelPackedSampleModel;
|
||||
import java.awt.image.WritableRaster;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -23,9 +9,9 @@ import java.util.logging.Logger;
|
||||
/**
|
||||
* Offscreen rendering surface for GOCA vector graphics.
|
||||
* Maintained as an ARGB 32-bit integer pixel buffer that overlays the 3270 character cell matrix.
|
||||
* Pure Java software rasterizer compatible with standard Java SE (Swing) and Android (Bitmap).
|
||||
* Pure Java software rasterizer compatible with standard Java SE, Android, and headless environments.
|
||||
*/
|
||||
public class GraphicsPlane {
|
||||
public class GraphicsPlane implements PixelBuffer {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GraphicsPlane.class.getName());
|
||||
|
||||
@@ -111,28 +97,118 @@ public class GraphicsPlane {
|
||||
drawLine((double) x1, (double) y1, (double) x2, (double) y2, currentColorArgb, currentLineType, currentLineWidth);
|
||||
}
|
||||
|
||||
private BufferedImage canvasImage;
|
||||
@Override
|
||||
public synchronized void drawLine(int x1, int y1, int x2, int y2, int argb) {
|
||||
drawLine((double) x1, (double) y1, (double) x2, (double) y2, argb, currentLineType, currentLineWidth);
|
||||
}
|
||||
|
||||
public synchronized BufferedImage toBufferedImage() {
|
||||
if (canvasImage == null && rgbBuffer != null && canvasWidth > 0 && canvasHeight > 0) {
|
||||
DataBufferInt db = new DataBufferInt(rgbBuffer, rgbBuffer.length);
|
||||
DirectColorModel cm = new DirectColorModel(32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000);
|
||||
WritableRaster raster = Raster.createWritableRaster(
|
||||
new SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, canvasWidth, canvasHeight,
|
||||
new int[]{0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000}),
|
||||
db, null
|
||||
);
|
||||
canvasImage = new BufferedImage(cm, raster, false, null);
|
||||
@Override
|
||||
public synchronized int getWidth() {
|
||||
return canvasWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getHeight() {
|
||||
return canvasHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int[] getPixels() {
|
||||
return rgbBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void fillRect(int x, int y, int width, int height, int argb) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
int x1 = Math.max(0, x);
|
||||
int y1 = Math.max(0, y);
|
||||
int x2 = Math.min(canvasWidth, x + width);
|
||||
int y2 = Math.min(canvasHeight, y + height);
|
||||
for (int cy = y1; cy < y2; cy++) {
|
||||
for (int cx = x1; cx < x2; cx++) {
|
||||
setPixel(cx, cy, argb);
|
||||
}
|
||||
}
|
||||
return canvasImage;
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized Image getImage() {
|
||||
return toBufferedImage();
|
||||
@Override
|
||||
public synchronized void blit(int[] srcPixels, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY) {
|
||||
if (srcPixels == null || srcW <= 0 || srcH <= 0) return;
|
||||
for (int r = 0; r < srcH; r++) {
|
||||
int sy = srcY + r;
|
||||
int dy = dstY + r;
|
||||
if (dy < 0 || dy >= canvasHeight) continue;
|
||||
|
||||
for (int c = 0; c < srcW; c++) {
|
||||
int sx = srcX + c;
|
||||
int dx = dstX + c;
|
||||
if (dx < 0 || dx >= canvasWidth) continue;
|
||||
if (isClipped(dx, dy)) continue;
|
||||
|
||||
int idx = sy * srcW + sx;
|
||||
if (idx < srcPixels.length) {
|
||||
setPixel(dx, dy, srcPixels[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
public synchronized Graphics getGraphics() {
|
||||
return toBufferedImage().getGraphics();
|
||||
public synchronized void copyPixels(int[] srcPixels, int srcOffset, int srcScan, int dstX, int dstY, int width, int height) {
|
||||
if (srcPixels == null || width <= 0 || height <= 0) return;
|
||||
for (int r = 0; r < height; r++) {
|
||||
int cy = dstY + r;
|
||||
if (cy < 0 || cy >= canvasHeight) continue;
|
||||
int srcRowStart = srcOffset + r * srcScan;
|
||||
for (int c = 0; c < width; c++) {
|
||||
int cx = dstX + c;
|
||||
if (cx < 0 || cx >= canvasWidth) continue;
|
||||
int p = srcPixels[srcRowStart + c];
|
||||
setPixel(cx, cy, p);
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setClip(int x, int y, int width, int height) {
|
||||
if (width < 0 || height < 0) {
|
||||
clearClip();
|
||||
} else {
|
||||
this.clipPixelXMin = x;
|
||||
this.clipPixelYMin = y;
|
||||
this.clipPixelXMax = x + width;
|
||||
this.clipPixelYMax = y + height;
|
||||
this.viewingWindowActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clearClip() {
|
||||
this.clipPixelXMin = 0;
|
||||
this.clipPixelYMin = 0;
|
||||
this.clipPixelXMax = canvasWidth;
|
||||
this.clipPixelYMax = canvasHeight;
|
||||
this.viewingWindowActive = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClipped(int x, int y) {
|
||||
if (!viewingWindowActive) {
|
||||
return x < 0 || x >= canvasWidth || y < 0 || y >= canvasHeight;
|
||||
}
|
||||
return x < clipPixelXMin || x > clipPixelXMax || y < clipPixelYMin || y > clipPixelYMax;
|
||||
}
|
||||
|
||||
public synchronized Rectangle getClip() {
|
||||
if (!viewingWindowActive) {
|
||||
return new Rectangle(0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
return new Rectangle(clipPixelXMin, clipPixelYMin, clipPixelXMax - clipPixelXMin, clipPixelYMax - clipPixelYMin);
|
||||
}
|
||||
|
||||
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
||||
@@ -230,16 +306,21 @@ public class GraphicsPlane {
|
||||
this.canvasWidth = w;
|
||||
this.canvasHeight = h;
|
||||
this.rgbBuffer = newBuffer;
|
||||
this.canvasImage = null;
|
||||
updateViewingWindowPixels();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear() {
|
||||
clear(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear(int argb) {
|
||||
if (rgbBuffer != null) {
|
||||
Arrays.fill(rgbBuffer, 0);
|
||||
Arrays.fill(rgbBuffer, argb);
|
||||
}
|
||||
this.currentMixMode = 0;
|
||||
hasContent = false;
|
||||
hasContent = (argb != 0);
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
@@ -536,6 +617,7 @@ public class GraphicsPlane {
|
||||
/**
|
||||
* Standard integer Bresenham line algorithm matching IBM 3179G / Host On-Demand 1-pixel rasterization.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void drawLineBresenham(int x0, int y0, int x1, int y1, int color) {
|
||||
int dx = Math.abs(x1 - x0);
|
||||
int dy = Math.abs(y1 - y0);
|
||||
@@ -598,6 +680,7 @@ public class GraphicsPlane {
|
||||
/**
|
||||
* Draws a line matching IBM 3179G / Host On-Demand rasterization.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
|
||||
int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : (GocaConstants.GOCA_COLORS[0] & 0x00FFFFFF);
|
||||
int ix0 = (int) Math.round(x0);
|
||||
@@ -628,6 +711,111 @@ public class GraphicsPlane {
|
||||
drawLine((double) x1, (double) y1, (double) x2, (double) y2, colorArgb, lineType, lineWidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void drawLineAA(double x0, double y0, double x1, double y1, int argb, double strokeWidth) {
|
||||
drawLineAA(x0, y0, x1, y1, argb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an anti-aliased line using Xiaolin Wu's algorithm directly into the pixel buffer.
|
||||
* Pure Java implementation replacing AWT Graphics2D rendering for sub-pixel vector strokes.
|
||||
*/
|
||||
public synchronized void drawLineAA(double x0, double y0, double x1, double y1, int color) {
|
||||
boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
|
||||
if (steep) {
|
||||
double t = x0; x0 = y0; y0 = t;
|
||||
t = x1; x1 = y1; y1 = t;
|
||||
}
|
||||
if (x0 > x1) {
|
||||
double t = x0; x0 = x1; x1 = t;
|
||||
t = y0; y0 = y1; y1 = t;
|
||||
}
|
||||
|
||||
double dx = x1 - x0;
|
||||
double dy = y1 - y0;
|
||||
double gradient = (dx == 0.0) ? 1.0 : dy / dx;
|
||||
|
||||
// Handle first endpoint
|
||||
double xend = Math.round(x0);
|
||||
double yend = y0 + gradient * (xend - x0);
|
||||
double xgap = 1.0 - (x0 + 0.5 - Math.floor(x0 + 0.5));
|
||||
int xpxl1 = (int) xend;
|
||||
int ypxl1 = (int) Math.floor(yend);
|
||||
if (steep) {
|
||||
plotAA(ypxl1, xpxl1, (1.0 - (yend - Math.floor(yend))) * xgap, color);
|
||||
plotAA(ypxl1 + 1, xpxl1, (yend - Math.floor(yend)) * xgap, color);
|
||||
} else {
|
||||
plotAA(xpxl1, ypxl1, (1.0 - (yend - Math.floor(yend))) * xgap, color);
|
||||
plotAA(xpxl1, ypxl1 + 1, (yend - Math.floor(yend)) * xgap, color);
|
||||
}
|
||||
double intery = yend + gradient;
|
||||
|
||||
// Handle second endpoint
|
||||
xend = Math.round(x1);
|
||||
yend = y1 + gradient * (xend - x1);
|
||||
xgap = x1 + 0.5 - Math.floor(x1 + 0.5);
|
||||
int xpxl2 = (int) xend;
|
||||
int ypxl2 = (int) Math.floor(yend);
|
||||
if (steep) {
|
||||
plotAA(ypxl2, xpxl2, (1.0 - (yend - Math.floor(yend))) * xgap, color);
|
||||
plotAA(ypxl2 + 1, xpxl2, (yend - Math.floor(yend)) * xgap, color);
|
||||
} else {
|
||||
plotAA(xpxl2, ypxl2, (1.0 - (yend - Math.floor(yend))) * xgap, color);
|
||||
plotAA(xpxl2, ypxl2 + 1, (yend - Math.floor(yend)) * xgap, color);
|
||||
}
|
||||
|
||||
// Main loop
|
||||
if (steep) {
|
||||
for (int x = xpxl1 + 1; x < xpxl2; x++) {
|
||||
int iy = (int) Math.floor(intery);
|
||||
double fpart = intery - iy;
|
||||
plotAA(iy, x, 1.0 - fpart, color);
|
||||
plotAA(iy + 1, x, fpart, color);
|
||||
intery += gradient;
|
||||
}
|
||||
} else {
|
||||
for (int x = xpxl1 + 1; x < xpxl2; x++) {
|
||||
int iy = (int) Math.floor(intery);
|
||||
double fpart = intery - iy;
|
||||
plotAA(x, iy, 1.0 - fpart, color);
|
||||
plotAA(x, iy + 1, fpart, color);
|
||||
intery += gradient;
|
||||
}
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
}
|
||||
|
||||
private void plotAA(int x, int y, double brightness, int color) {
|
||||
if (x < 0 || x >= canvasWidth || y < 0 || y >= canvasHeight || brightness <= 0.0) return;
|
||||
if (brightness > 1.0) brightness = 1.0;
|
||||
int sa = (color >>> 24) & 0xFF;
|
||||
if (sa == 0) sa = 0xFF;
|
||||
int alpha = (int) Math.round(sa * brightness);
|
||||
if (alpha <= 0) return;
|
||||
|
||||
int sr = (color >>> 16) & 0xFF;
|
||||
int sg = (color >>> 8) & 0xFF;
|
||||
int sb = color & 0xFF;
|
||||
|
||||
int idx = y * canvasWidth + x;
|
||||
int dst = rgbBuffer[idx];
|
||||
int da = (dst >>> 24) & 0xFF;
|
||||
if (da == 0) {
|
||||
rgbBuffer[idx] = (alpha << 24) | (sr << 16) | (sg << 8) | sb;
|
||||
} else {
|
||||
int dr = (dst >>> 16) & 0xFF;
|
||||
int dg = (dst >>> 8) & 0xFF;
|
||||
int db = dst & 0xFF;
|
||||
int invA = 255 - alpha;
|
||||
int outR = (sr * alpha + dr * invA) / 255;
|
||||
int outG = (sg * alpha + dg * invA) / 255;
|
||||
int outB = (sb * alpha + db * invA) / 255;
|
||||
int outA = Math.min(255, da + alpha);
|
||||
rgbBuffer[idx] = (outA << 24) | (outR << 16) | (outG << 8) | outB;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
|
||||
int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
|
||||
@@ -1231,56 +1419,46 @@ public class GraphicsPlane {
|
||||
double tanShear = Math.tan(Math.toRadians(shearAngle));
|
||||
|
||||
if (ch < 6.0) {
|
||||
BufferedImage img = toBufferedImage();
|
||||
if (img != null) {
|
||||
Graphics2D g = img.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
float strokeW = (float) Math.max(0.5, Math.min(0.75, ch / 5.0));
|
||||
g.setStroke(new BasicStroke(strokeW, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||
g.setColor(new Color(color, true));
|
||||
int ptr = offset;
|
||||
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
||||
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
||||
if (order == 0xC1) {
|
||||
int byteLen = VectorSymbolData.vss_data[ptr + 1] & 0xFF;
|
||||
int numPoints = byteLen / 4;
|
||||
int dataPtr = ptr + 2;
|
||||
|
||||
int ptr = offset;
|
||||
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
||||
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
||||
if (order == 0xC1) {
|
||||
int byteLen = VectorSymbolData.vss_data[ptr + 1] & 0xFF;
|
||||
int numPoints = byteLen / 4;
|
||||
int dataPtr = ptr + 2;
|
||||
if (numPoints >= 2) {
|
||||
double[] px = new double[numPoints];
|
||||
double[] py = new double[numPoints];
|
||||
for (int p = 0; p < numPoints; p++) {
|
||||
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
||||
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
||||
|
||||
if (numPoints >= 2) {
|
||||
Path2D.Double path = new Path2D.Double();
|
||||
for (int p = 0; p < numPoints; p++) {
|
||||
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
|
||||
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
||||
double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
||||
double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
|
||||
|
||||
double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
|
||||
double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
|
||||
double sx = nx - ny * tanShear;
|
||||
double sy = ny;
|
||||
|
||||
double sx = nx - ny * tanShear;
|
||||
double sy = ny;
|
||||
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
|
||||
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
|
||||
|
||||
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
|
||||
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
|
||||
px[p] = x + rx;
|
||||
py[p] = y + ry;
|
||||
}
|
||||
|
||||
double px = x + rx;
|
||||
double py = y + ry;
|
||||
|
||||
if (p == 0) path.moveTo(px, py);
|
||||
else path.lineTo(px, py);
|
||||
}
|
||||
g.draw(path);
|
||||
for (int p = 0; p < numPoints - 1; p++) {
|
||||
drawLineAA(px[p], py[p], px[p + 1], py[p + 1], color);
|
||||
}
|
||||
ptr += 2 + byteLen;
|
||||
} else {
|
||||
ptr++;
|
||||
}
|
||||
ptr += 2 + byteLen;
|
||||
} else {
|
||||
ptr++;
|
||||
}
|
||||
g.dispose();
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
return;
|
||||
}
|
||||
hasContent = true;
|
||||
updateCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
int ptr = offset;
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Image;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.image.FilteredImageSource;
|
||||
import java.awt.image.MemoryImageSource;
|
||||
|
||||
/**
|
||||
* Bitmap image container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBitImage).
|
||||
* Pure Java software implementation completely decoupled from java.awt.
|
||||
*/
|
||||
public class HODBitImage {
|
||||
protected Component vComponent;
|
||||
protected Object vComponent;
|
||||
protected Dimension iSize = new Dimension();
|
||||
protected Dimension iScaledSize = new Dimension();
|
||||
protected int iDepth;
|
||||
protected int iScanLength;
|
||||
protected boolean _iUseGraphicColors;
|
||||
protected byte[] iScaledImageData;
|
||||
protected Image[] hImage;
|
||||
protected Image[] iScaledImage;
|
||||
protected PixelBuffer[] hImage;
|
||||
protected PixelBuffer[] iScaledImage;
|
||||
protected int transparentBG = 0;
|
||||
protected byte[] hImageData;
|
||||
protected int iBaseColor;
|
||||
|
||||
public HODBitImage(Component comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) {
|
||||
public HODBitImage(Object comp, int width, int height, byte[] data, int baseColor, int depth, boolean useGraphicColors) {
|
||||
this.vComponent = comp;
|
||||
this.iSize.width = width;
|
||||
this.iSize.height = height;
|
||||
@@ -32,8 +26,8 @@ public class HODBitImage {
|
||||
this.iBaseColor = baseColor;
|
||||
this.iDepth = depth;
|
||||
this._iUseGraphicColors = useGraphicColors;
|
||||
this.hImage = new Image[depth == 1 ? 17 : 1];
|
||||
this.iScaledImage = new Image[depth == 1 ? 17 : 1];
|
||||
this.hImage = new PixelBuffer[depth == 1 ? 17 : 1];
|
||||
this.iScaledImage = new PixelBuffer[depth == 1 ? 17 : 1];
|
||||
this.buildHODImage();
|
||||
}
|
||||
|
||||
@@ -72,11 +66,19 @@ public class HODBitImage {
|
||||
return nArray;
|
||||
}
|
||||
|
||||
public Image getHODImage(int colorIdx) {
|
||||
return this.getHODImage(this.iSize.width, this.iSize.height, colorIdx);
|
||||
public Object getHODImage(int colorIdx) {
|
||||
return this.getHODPixelBuffer(this.iSize.width, this.iSize.height, colorIdx);
|
||||
}
|
||||
|
||||
public Image getHODImage(int w, int h, int colorIdx) {
|
||||
public Object getHODImage(int w, int h, int colorIdx) {
|
||||
return this.getHODPixelBuffer(w, h, colorIdx);
|
||||
}
|
||||
|
||||
public PixelBuffer getHODPixelBuffer(int colorIdx) {
|
||||
return this.getHODPixelBuffer(this.iSize.width, this.iSize.height, colorIdx);
|
||||
}
|
||||
|
||||
public PixelBuffer getHODPixelBuffer(int w, int h, int colorIdx) {
|
||||
if (w <= 0 || h <= 0) return null;
|
||||
boolean diffColor = this.iBaseColor != colorIdx && this.iDepth == 1;
|
||||
boolean matchesBase = this.iSize.width == w && this.iSize.height == h;
|
||||
@@ -88,13 +90,18 @@ public class HODBitImage {
|
||||
this.iScaledSize.height = h;
|
||||
this.scaleHODImage();
|
||||
}
|
||||
Image image = matchesBase ? this.hImage[colorIdx] : this.iScaledImage[colorIdx];
|
||||
PixelBuffer image = matchesBase ? this.hImage[colorIdx] : this.iScaledImage[colorIdx];
|
||||
if (image == null) {
|
||||
HODColorChangeFilter filter = new HODColorChangeFilter(this.getHODColor(colorIdx));
|
||||
Image base = matchesBase ? this.hImage[this.iBaseColor] : this.iScaledImage[this.iBaseColor];
|
||||
PixelBuffer base = matchesBase ? this.hImage[this.iBaseColor] : this.iScaledImage[this.iBaseColor];
|
||||
if (base != null) {
|
||||
FilteredImageSource source = new FilteredImageSource(base.getSource(), filter);
|
||||
image = Toolkit.getDefaultToolkit().createImage(source);
|
||||
int bw = base.getWidth();
|
||||
int bh = base.getHeight();
|
||||
int[] basePixels = base.getPixels();
|
||||
int[] filtered = new int[bw * bh];
|
||||
System.arraycopy(basePixels, 0, filtered, 0, filtered.length);
|
||||
filter.apply(filtered, 0, filtered.length);
|
||||
image = new DefaultPixelBuffer(bw, bh, filtered);
|
||||
if (matchesBase) {
|
||||
this.hImage[colorIdx] = image;
|
||||
} else {
|
||||
@@ -121,27 +128,47 @@ public class HODBitImage {
|
||||
private void buildHODImage() {
|
||||
if (this.iSize.width <= 0 || this.iSize.height <= 0) return;
|
||||
int[] pixels = getHODImageData(this.iBaseColor);
|
||||
MemoryImageSource mis = new MemoryImageSource(this.iSize.width, this.iSize.height, pixels, 0, this.iSize.width);
|
||||
Image img = Toolkit.getDefaultToolkit().createImage(mis);
|
||||
PixelBuffer buf = new DefaultPixelBuffer(this.iSize.width, this.iSize.height, pixels);
|
||||
if (this.iDepth == 1) {
|
||||
this.hImage[this.iBaseColor] = img;
|
||||
this.hImage[this.iBaseColor] = buf;
|
||||
} else {
|
||||
this.hImage[0] = img;
|
||||
this.hImage[0] = buf;
|
||||
}
|
||||
}
|
||||
|
||||
private void scaleHODImage() {
|
||||
if (this.iScaledSize.width <= 0 || this.iScaledSize.height <= 0) return;
|
||||
Image base = this.hImage[this.iDepth == 1 ? this.iBaseColor : 0];
|
||||
int sw = this.iScaledSize.width;
|
||||
int sh = this.iScaledSize.height;
|
||||
if (sw <= 0 || sh <= 0) return;
|
||||
PixelBuffer base = this.hImage[this.iDepth == 1 ? this.iBaseColor : 0];
|
||||
if (base != null) {
|
||||
this.iScaledImage[this.iDepth == 1 ? this.iBaseColor : 0] =
|
||||
base.getScaledInstance(this.iScaledSize.width, this.iScaledSize.height, Image.SCALE_FAST);
|
||||
int bw = base.getWidth();
|
||||
int bh = base.getHeight();
|
||||
int[] src = base.getPixels();
|
||||
int[] dst = new int[sw * sh];
|
||||
for (int dy = 0; dy < sh; dy++) {
|
||||
int sy = dy * bh / sh;
|
||||
int srcOffset = sy * bw;
|
||||
int dstOffset = dy * sw;
|
||||
for (int dx = 0; dx < sw; dx++) {
|
||||
int sx = dx * bw / sw;
|
||||
dst[dstOffset + dx] = src[srcOffset + sx];
|
||||
}
|
||||
}
|
||||
this.iScaledImage[this.iDepth == 1 ? this.iBaseColor : 0] = new DefaultPixelBuffer(sw, sh, dst);
|
||||
}
|
||||
}
|
||||
|
||||
private int getHODColor(int idx) {
|
||||
if (idx == 0 && this.vComponent != null) {
|
||||
return this.vComponent.getBackground().getRGB();
|
||||
try {
|
||||
java.lang.reflect.Method m = this.vComponent.getClass().getMethod("getBackground");
|
||||
Object bg = m.invoke(this.vComponent);
|
||||
if (bg != null) {
|
||||
java.lang.reflect.Method mRgb = bg.getClass().getMethod("getRGB");
|
||||
return ((Number) mRgb.invoke(bg)).intValue();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return GocaConstants.getGocaColorArgb(idx);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
|
||||
/**
|
||||
* Bounding box encapsulation matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBounds).
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.RGBImageFilter;
|
||||
|
||||
/**
|
||||
* Image filter that replaces occurrences of one color with another color.
|
||||
* Conforms to IBM Host On-Demand HODColorChangeFilter.
|
||||
* Platform-neutral implementation independent of java.awt.
|
||||
*/
|
||||
public class HODColorChangeFilter extends RGBImageFilter {
|
||||
public class HODColorChangeFilter {
|
||||
|
||||
protected boolean canFilterIndexColorModel = true;
|
||||
private int oldRgb;
|
||||
private int newRgb;
|
||||
|
||||
public HODColorChangeFilter(int newRgb) {
|
||||
this.canFilterIndexColorModel = true;
|
||||
this.oldRgb = -1;
|
||||
this.newRgb = newRgb | 0xFF000000;
|
||||
}
|
||||
@@ -23,7 +21,6 @@ public class HODColorChangeFilter extends RGBImageFilter {
|
||||
}
|
||||
|
||||
public HODColorChangeFilter(int oldRgb, int newRgb) {
|
||||
this.canFilterIndexColorModel = true;
|
||||
this.oldRgb = oldRgb & 0x00FFFFFF;
|
||||
this.newRgb = newRgb;
|
||||
}
|
||||
@@ -48,7 +45,6 @@ public class HODColorChangeFilter extends RGBImageFilter {
|
||||
this.newRgb = newRgb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int filterRGB(int x, int y, int rgb) {
|
||||
if (oldRgb == -1) {
|
||||
if ((rgb & 0xFF000000) != 0) {
|
||||
@@ -61,4 +57,20 @@ public class HODColorChangeFilter extends RGBImageFilter {
|
||||
}
|
||||
return rgb;
|
||||
}
|
||||
|
||||
public void apply(int[] pixels, int offset, int length) {
|
||||
if (pixels == null) return;
|
||||
int end = Math.min(pixels.length, offset + length);
|
||||
for (int i = offset; i < end; i++) {
|
||||
pixels[i] = filterRGB(0, 0, pixels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void apply(PixelBuffer buffer) {
|
||||
if (buffer == null) return;
|
||||
int[] pixels = buffer.getPixels();
|
||||
if (pixels != null) {
|
||||
apply(pixels, 0, pixels.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Image;
|
||||
import java.awt.Point;
|
||||
import java.awt.Polygon;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* Headless graphics plane facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODGraphicsPlane).
|
||||
* Pure Java software implementation completely decoupled from java.awt.
|
||||
*/
|
||||
public class HODGraphicsPlane {
|
||||
private final GraphicsPlane delegate;
|
||||
@@ -40,6 +32,10 @@ public class HODGraphicsPlane {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public PixelBuffer getPixelBuffer() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public void resize(Dimension dim, boolean keepContent) {
|
||||
if (dim != null) {
|
||||
delegate.setDimensions(dim.width, dim.height);
|
||||
@@ -52,15 +48,19 @@ public class HODGraphicsPlane {
|
||||
this.bounds.set(0, 0, delegate.getCanvasWidth(), delegate.getCanvasHeight());
|
||||
}
|
||||
|
||||
public Graphics getHODGraphics() {
|
||||
return delegate.getGraphics();
|
||||
public Object getHODGraphics() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public Image getHODImage() {
|
||||
return delegate.getImage();
|
||||
public Object getHODImage() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public void setHODTemporaryGraphics(Graphics g) {
|
||||
public PixelBuffer getHODPixelBuffer() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public void setHODTemporaryGraphics(Object g) {
|
||||
// No-op or temporary override
|
||||
}
|
||||
|
||||
@@ -116,35 +116,32 @@ public class HODGraphicsPlane {
|
||||
}
|
||||
|
||||
public void drawHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
||||
Graphics g = delegate.getGraphics();
|
||||
if (g != null) {
|
||||
g.setColor(currentColor);
|
||||
g.drawArc(x, y, width, height, startAngle, arcAngle);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + width, y + height);
|
||||
}
|
||||
double rx = width / 2.0;
|
||||
double ry = height / 2.0;
|
||||
double cx = x + rx;
|
||||
double cy = y + ry;
|
||||
delegate.drawArc(cx, cy, rx, ry, startAngle, arcAngle, currentColor.getRGB(), currentLineType, currentLineWidth, false);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + width, y + height);
|
||||
}
|
||||
|
||||
public void fillHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
||||
Graphics g = delegate.getGraphics();
|
||||
if (g != null) {
|
||||
g.setColor(currentColor);
|
||||
g.fillArc(x, y, width, height, startAngle, arcAngle);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + width, y + height);
|
||||
}
|
||||
double rx = width / 2.0;
|
||||
double ry = height / 2.0;
|
||||
double cx = x + rx;
|
||||
double cy = y + ry;
|
||||
delegate.drawArc(cx, cy, rx, ry, startAngle, arcAngle, currentColor.getRGB(), currentLineType, currentLineWidth, true);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + width, y + height);
|
||||
}
|
||||
|
||||
public void drawHODImage(HODBitImage bitImage, int x, int y, int w, int h) {
|
||||
if (bitImage != null) {
|
||||
Image img = bitImage.getHODImage(w, h, currentColorIndex);
|
||||
if (img != null) {
|
||||
Graphics g = delegate.getGraphics();
|
||||
if (g != null) {
|
||||
g.drawImage(img, x, y, null);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + w, y + h);
|
||||
}
|
||||
PixelBuffer buf = bitImage.getHODPixelBuffer(w, h, currentColorIndex);
|
||||
if (buf != null) {
|
||||
delegate.blit(buf.getPixels(), 0, 0, buf.getWidth(), buf.getHeight(), x, y);
|
||||
updateHODBounds(x, y);
|
||||
updateHODBounds(x + w, y + h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Rectangle;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Visual part container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODPart).
|
||||
* Completely decoupled from java.awt.
|
||||
*/
|
||||
public class HODPart extends Rectangle implements Serializable {
|
||||
protected Component hodParent;
|
||||
protected Font _hodFont;
|
||||
protected Object hodParent;
|
||||
protected Object _hodFont;
|
||||
protected Color foregroundColor;
|
||||
protected Color backgroundColor;
|
||||
protected Boolean isTransparent;
|
||||
@@ -22,33 +17,36 @@ public class HODPart extends Rectangle implements Serializable {
|
||||
|
||||
protected HODPart() {}
|
||||
|
||||
public HODPart(Component component) {
|
||||
public HODPart(Object component) {
|
||||
this();
|
||||
this.setHODParent(component);
|
||||
}
|
||||
|
||||
public HODPart(Component component, Dimension dimension) {
|
||||
super(dimension);
|
||||
public HODPart(Object component, Dimension dimension) {
|
||||
super(0, 0, dimension != null ? dimension.width : 0, dimension != null ? dimension.height : 0);
|
||||
this.setHODParent(component);
|
||||
}
|
||||
|
||||
public HODPart(Component component, Rectangle rectangle) {
|
||||
super(rectangle);
|
||||
public HODPart(Object component, Rectangle rectangle) {
|
||||
super(rectangle != null ? rectangle.x : 0, rectangle != null ? rectangle.y : 0,
|
||||
rectangle != null ? rectangle.width : 0, rectangle != null ? rectangle.height : 0);
|
||||
this.setHODParent(component);
|
||||
}
|
||||
|
||||
public HODPart(HODPart hODPart) {
|
||||
this(hODPart.getHODParent(), hODPart.getSize());
|
||||
this.setHODBackground(hODPart.getHODBackground());
|
||||
this.setHODForeground(hODPart.getHODForeground());
|
||||
this.setHODFont(hODPart.getHODFont());
|
||||
this(hODPart != null ? hODPart.getHODParent() : null, hODPart != null ? hODPart.getSize() : null);
|
||||
if (hODPart != null) {
|
||||
this.setHODBackground(hODPart.getHODBackground());
|
||||
this.setHODForeground(hODPart.getHODForeground());
|
||||
this.setHODFont(hODPart.getHODFont());
|
||||
}
|
||||
}
|
||||
|
||||
public Component getHODParent() {
|
||||
public Object getHODParent() {
|
||||
return this.hodParent;
|
||||
}
|
||||
|
||||
public void setHODParent(Component component) {
|
||||
public void setHODParent(Object component) {
|
||||
if (component != null && !component.equals(this.hodParent)) {
|
||||
this.hodParent = component;
|
||||
}
|
||||
@@ -56,18 +54,28 @@ public class HODPart extends Rectangle implements Serializable {
|
||||
|
||||
public void repaint() {
|
||||
if (this.hodParent != null) {
|
||||
this.hodParent.repaint(this.x, this.y, this.width, this.height);
|
||||
try {
|
||||
Method m = this.hodParent.getClass().getMethod("repaint", int.class, int.class, int.class, int.class);
|
||||
m.invoke(this.hodParent, this.x, this.y, this.width, this.height);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void paint(Component component, Graphics graphics, int x, int y, int w, int h) {
|
||||
public void paint(Object component, Object graphics, int x, int y, int w, int h) {
|
||||
this.setBounds(x, y, w, h);
|
||||
if (Boolean.TRUE.equals(this._visible)) {
|
||||
this.paintHODView(graphics);
|
||||
}
|
||||
}
|
||||
|
||||
protected void paintHODView(Graphics graphics) {}
|
||||
public void paint(PixelBuffer buffer, int x, int y, int w, int h) {
|
||||
this.setBounds(x, y, w, h);
|
||||
if (Boolean.TRUE.equals(this._visible)) {
|
||||
this.paintHODView(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
protected void paintHODView(Object graphics) {}
|
||||
|
||||
public Color getHODBackground() {
|
||||
return this.backgroundColor;
|
||||
@@ -85,11 +93,11 @@ public class HODPart extends Rectangle implements Serializable {
|
||||
this.foregroundColor = color;
|
||||
}
|
||||
|
||||
public Font getHODFont() {
|
||||
public Object getHODFont() {
|
||||
return this._hodFont;
|
||||
}
|
||||
|
||||
public void setHODFont(Font font) {
|
||||
public void setHODFont(Object font) {
|
||||
this._hodFont = font;
|
||||
}
|
||||
|
||||
|
||||
+50
-9
@@ -1,12 +1,10 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Point;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Programmed Symbol Set manager facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODProgramSymbolManager).
|
||||
* Completely decoupled from java.awt.
|
||||
*/
|
||||
public class HODProgramSymbolManager {
|
||||
public static final int MAX_HOD_SLOT = 254;
|
||||
@@ -51,14 +49,57 @@ public class HODProgramSymbolManager {
|
||||
delegate.loadProgrammedSymbolSet(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
public void drawHODImageCharacter(Graphics g, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) {
|
||||
if (g == null || pt == null) return;
|
||||
public void drawHODImageCharacter(PixelBuffer pb, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) {
|
||||
if (pb == null || pt == null) return;
|
||||
ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint);
|
||||
if (slot != null) {
|
||||
int fg = GocaConstants.getGocaColorArgb(colorIdx);
|
||||
BufferedImage img = slot.getScaledImage(cellW, cellH, fg, 0);
|
||||
if (img != null) {
|
||||
g.drawImage(img, pt.x, pt.y, null);
|
||||
PixelBuffer glyph = slot.getScaledPixelBuffer(cellW, cellH, fg, 0);
|
||||
if (glyph != null) {
|
||||
int sw = glyph.getWidth();
|
||||
int sh = glyph.getHeight();
|
||||
int[] srcPx = glyph.getPixels();
|
||||
int[] dstPx = pb.getPixels();
|
||||
int pw = pb.getWidth();
|
||||
int ph = pb.getHeight();
|
||||
for (int r = 0; r < sh; r++) {
|
||||
int dy = pt.y + r;
|
||||
if (dy < 0 || dy >= ph) continue;
|
||||
for (int c = 0; c < sw; c++) {
|
||||
int dx = pt.x + c;
|
||||
if (dx < 0 || dx >= pw) continue;
|
||||
int p = srcPx[r * sw + c];
|
||||
if ((p & 0xFF000000) != 0) {
|
||||
dstPx[dy * pw + dx] = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void drawHODImageCharacter(Object g, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) {
|
||||
if (g == null || pt == null) return;
|
||||
if (g instanceof PixelBuffer) {
|
||||
drawHODImageCharacter((PixelBuffer) g, lcid, codepoint, pt, colorIdx, cellW, cellH);
|
||||
return;
|
||||
}
|
||||
// Fallback for AWT Graphics if passed reflectively
|
||||
ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint);
|
||||
if (slot != null) {
|
||||
int fg = GocaConstants.getGocaColorArgb(colorIdx);
|
||||
PixelBuffer glyph = slot.getScaledPixelBuffer(cellW, cellH, fg, 0);
|
||||
if (glyph != null) {
|
||||
try {
|
||||
Class<?> biClass = Class.forName("java.awt.image.BufferedImage");
|
||||
Object bi = biClass.getConstructor(int.class, int.class, int.class)
|
||||
.newInstance(glyph.getWidth(), glyph.getHeight(), 2); // TYPE_INT_ARGB
|
||||
Method setRGB = biClass.getMethod("setRGB", int.class, int.class, int.class, int.class, int[].class, int.class, int.class);
|
||||
setRGB.invoke(bi, 0, 0, glyph.getWidth(), glyph.getHeight(), glyph.getPixels(), 0, glyph.getWidth());
|
||||
|
||||
Method drawImg = g.getClass().getMethod("drawImage", Class.forName("java.awt.Image"), int.class, int.class, Class.forName("java.awt.image.ImageObserver"));
|
||||
drawImg.invoke(g, bi, pt.x, pt.y, null);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
|
||||
/**
|
||||
* Coordinate transform adapter matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODTransform).
|
||||
|
||||
+19
-6
@@ -1,18 +1,16 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.RGBImageFilter;
|
||||
|
||||
/**
|
||||
* Image filter that keys out a specific background color by setting its alpha to 0x00.
|
||||
* Conforms to IBM Host On-Demand HODTransparentColorFilter.
|
||||
* Platform-neutral implementation independent of java.awt.
|
||||
*/
|
||||
public class HODTransparentColorFilter extends RGBImageFilter {
|
||||
public class HODTransparentColorFilter {
|
||||
|
||||
protected boolean canFilterIndexColorModel = true;
|
||||
private int transparentRgb;
|
||||
|
||||
public HODTransparentColorFilter(int rgb) {
|
||||
this.canFilterIndexColorModel = true;
|
||||
this.transparentRgb = rgb & 0x00FFFFFF;
|
||||
}
|
||||
|
||||
@@ -28,11 +26,26 @@ public class HODTransparentColorFilter extends RGBImageFilter {
|
||||
this.transparentRgb = rgb & 0x00FFFFFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int filterRGB(int x, int y, int rgb) {
|
||||
if ((rgb & 0x00FFFFFF) == transparentRgb) {
|
||||
return 0x00000000;
|
||||
}
|
||||
return rgb;
|
||||
}
|
||||
|
||||
public void apply(int[] pixels, int offset, int length) {
|
||||
if (pixels == null) return;
|
||||
int end = Math.min(pixels.length, offset + length);
|
||||
for (int i = offset; i < end; i++) {
|
||||
pixels[i] = filterRGB(0, 0, pixels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void apply(PixelBuffer buffer) {
|
||||
if (buffer == null) return;
|
||||
int[] pixels = buffer.getPixels();
|
||||
if (pixels != null) {
|
||||
apply(pixels, 0, pixels.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Image;
|
||||
import java.awt.Insets;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Wallpaper background manager matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODWallpaper).
|
||||
* Completely decoupled from java.awt.
|
||||
*/
|
||||
public class HODWallpaper extends HODPart {
|
||||
public static final int HOD_TILE = 0;
|
||||
@@ -17,8 +12,8 @@ public class HODWallpaper extends HODPart {
|
||||
public static final int HOD_STRETCH = 2;
|
||||
|
||||
private int _display = HOD_CENTER;
|
||||
private Image rawImage;
|
||||
private Image backgroundImage;
|
||||
private Object rawImage;
|
||||
private Object backgroundImage;
|
||||
|
||||
public HODWallpaper() {
|
||||
this(HOD_CENTER);
|
||||
@@ -28,7 +23,7 @@ public class HODWallpaper extends HODPart {
|
||||
this.setDisplay(displayMode);
|
||||
}
|
||||
|
||||
public HODWallpaper(Image image, int displayMode) {
|
||||
public HODWallpaper(Object image, int displayMode) {
|
||||
this(displayMode);
|
||||
this.setImage(image);
|
||||
}
|
||||
@@ -46,30 +41,40 @@ public class HODWallpaper extends HODPart {
|
||||
return this._display;
|
||||
}
|
||||
|
||||
public void setImage(Image image) {
|
||||
public void setImage(Object image) {
|
||||
this.rawImage = image;
|
||||
this.backgroundImage = null;
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
public Image getHODImage() {
|
||||
public Object getHODImage() {
|
||||
return this.rawImage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Component component, Graphics graphics, int x, int y, int w, int h) {
|
||||
public void paint(Object component, Object graphics, int x, int y, int w, int h) {
|
||||
this.setBounds(x, y, w, h);
|
||||
this.setHODParent(component);
|
||||
super.paint(component, graphics, x, y, w, h);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintHODView(Graphics graphics) {
|
||||
Image image = this.getHODImage();
|
||||
int display = this.getDisplay();
|
||||
Component component = this.getHODParent();
|
||||
public void paint(PixelBuffer buffer, int x, int y, int w, int h) {
|
||||
this.setBounds(x, y, w, h);
|
||||
super.paint(buffer, x, y, w, h);
|
||||
}
|
||||
|
||||
if (image == null || component == null) {
|
||||
@Override
|
||||
protected void paintHODView(Object graphics) {
|
||||
Object image = this.getHODImage();
|
||||
int display = this.getDisplay();
|
||||
|
||||
if (image == null || graphics == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (graphics instanceof PixelBuffer && image instanceof PixelBuffer) {
|
||||
paintHODViewBuffer((PixelBuffer) graphics, (PixelBuffer) image);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,7 +87,83 @@ public class HODWallpaper extends HODPart {
|
||||
}
|
||||
}
|
||||
|
||||
protected void hodtileImage(Graphics graphics, Image image) {
|
||||
public void paintHODViewBuffer(PixelBuffer dst, PixelBuffer src) {
|
||||
int display = this.getDisplay();
|
||||
Insets insets = this.getInsets();
|
||||
int availX = this.x + insets.left;
|
||||
int availY = this.y + insets.top;
|
||||
int availW = this.width - (insets.left + insets.right);
|
||||
int availH = this.height - (insets.top + insets.bottom);
|
||||
if (availW <= 0 || availH <= 0 || src.getWidth() <= 0 || src.getHeight() <= 0) return;
|
||||
|
||||
if (display == HOD_CENTER) {
|
||||
int cx = availX + (availW - src.getWidth()) / 2;
|
||||
int cy = availY + (availH - src.getHeight()) / 2;
|
||||
blitBuffer(dst, src, cx, cy);
|
||||
} else if (display == HOD_TILE) {
|
||||
int cols = (availW / src.getWidth()) + 1;
|
||||
int rows = (availH / src.getHeight()) + 1;
|
||||
int curX = availX;
|
||||
for (int i = 0; i < cols; i++) {
|
||||
int curY = availY;
|
||||
for (int j = 0; j < rows; j++) {
|
||||
blitBuffer(dst, src, curX, curY);
|
||||
curY += src.getHeight();
|
||||
}
|
||||
curX += src.getWidth();
|
||||
}
|
||||
} else if (display == HOD_STRETCH) {
|
||||
scaleBuffer(dst, src, availX, availY, availW, availH);
|
||||
}
|
||||
}
|
||||
|
||||
private void blitBuffer(PixelBuffer dst, PixelBuffer src, int dstX, int dstY) {
|
||||
int sw = src.getWidth();
|
||||
int sh = src.getHeight();
|
||||
int dw = dst.getWidth();
|
||||
int dh = dst.getHeight();
|
||||
int[] srcPx = src.getPixels();
|
||||
int[] dstPx = dst.getPixels();
|
||||
|
||||
for (int r = 0; r < sh; r++) {
|
||||
int dy = dstY + r;
|
||||
if (dy < 0 || dy >= dh) continue;
|
||||
for (int c = 0; c < sw; c++) {
|
||||
int dx = dstX + c;
|
||||
if (dx < 0 || dx >= dw) continue;
|
||||
int p = srcPx[r * sw + c];
|
||||
if ((p & 0xFF000000) != 0) {
|
||||
dstPx[dy * dw + dx] = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void scaleBuffer(PixelBuffer dst, PixelBuffer src, int dx, int dy, int dw, int dh) {
|
||||
int sw = src.getWidth();
|
||||
int sh = src.getHeight();
|
||||
int targetW = dst.getWidth();
|
||||
int targetH = dst.getHeight();
|
||||
int[] srcPx = src.getPixels();
|
||||
int[] dstPx = dst.getPixels();
|
||||
|
||||
for (int r = 0; r < dh; r++) {
|
||||
int outY = dy + r;
|
||||
if (outY < 0 || outY >= targetH) continue;
|
||||
int sy = r * sh / dh;
|
||||
for (int c = 0; c < dw; c++) {
|
||||
int outX = dx + c;
|
||||
if (outX < 0 || outX >= targetW) continue;
|
||||
int sx = c * sw / dw;
|
||||
int p = srcPx[sy * sw + sx];
|
||||
if ((p & 0xFF000000) != 0) {
|
||||
dstPx[outY * targetW + outX] = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void hodtileImage(Object graphics, Object image) {
|
||||
Dimension imgSize = getImageSize(image);
|
||||
if (imgSize.width <= 0 || imgSize.height <= 0) return;
|
||||
|
||||
@@ -99,40 +180,86 @@ public class HODWallpaper extends HODPart {
|
||||
for (int i = 0; i < cols; i++) {
|
||||
int curY = startY;
|
||||
for (int j = 0; j < rows; j++) {
|
||||
graphics.drawImage(image, curX, curY, this.getHODParent());
|
||||
invokeDrawImage(graphics, image, curX, curY);
|
||||
curY += imgSize.height;
|
||||
}
|
||||
curX += imgSize.width;
|
||||
}
|
||||
}
|
||||
|
||||
protected void centerHODImage(Graphics graphics, Image image) {
|
||||
protected void centerHODImage(Object graphics, Object image) {
|
||||
Dimension imgSize = getImageSize(image);
|
||||
if (imgSize.width <= 0 || imgSize.height <= 0) return;
|
||||
|
||||
Insets insets = this.getInsets();
|
||||
int cx = this.x + insets.left + (this.width - imgSize.width) / 2;
|
||||
int cy = this.y + insets.top + (this.height - imgSize.height) / 2;
|
||||
graphics.drawImage(image, cx, cy, this.getHODParent());
|
||||
invokeDrawImage(graphics, image, cx, cy);
|
||||
}
|
||||
|
||||
protected void stretchHODImage(Graphics graphics, Image image) {
|
||||
protected void stretchHODImage(Object graphics, Object image) {
|
||||
Insets insets = this.getInsets();
|
||||
int sx = this.x + insets.left;
|
||||
int sy = this.y + insets.top;
|
||||
int sw = this.width - (insets.left + insets.right);
|
||||
int sh = this.height - (insets.top + insets.bottom);
|
||||
graphics.drawImage(image, sx, sy, sw, sh, this.getHODParent());
|
||||
invokeDrawImage(graphics, image, sx, sy, sw, sh);
|
||||
}
|
||||
|
||||
private Dimension getImageSize(Image image) {
|
||||
private void invokeDrawImage(Object graphics, Object img, int x, int y) {
|
||||
if (graphics == null || img == null) return;
|
||||
try {
|
||||
for (Method m : graphics.getClass().getMethods()) {
|
||||
if (m.getName().equals("drawImage")) {
|
||||
Class<?>[] pts = m.getParameterTypes();
|
||||
if (pts.length == 4 && pts[1] == int.class && pts[2] == int.class) {
|
||||
m.invoke(graphics, img, x, y, this.getHODParent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
private void invokeDrawImage(Object graphics, Object img, int x, int y, int w, int h) {
|
||||
if (graphics == null || img == null) return;
|
||||
try {
|
||||
for (Method m : graphics.getClass().getMethods()) {
|
||||
if (m.getName().equals("drawImage")) {
|
||||
Class<?>[] pts = m.getParameterTypes();
|
||||
if (pts.length == 6 && pts[1] == int.class && pts[2] == int.class && pts[3] == int.class && pts[4] == int.class) {
|
||||
m.invoke(graphics, img, x, y, w, h, this.getHODParent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
private Dimension getImageSize(Object image) {
|
||||
if (image == null) return new Dimension(0, 0);
|
||||
Component c = this.getHODParent();
|
||||
int w = image.getWidth(c);
|
||||
int h = image.getHeight(c);
|
||||
if (w <= 0 && image instanceof BufferedImage) {
|
||||
w = ((BufferedImage) image).getWidth();
|
||||
h = ((BufferedImage) image).getHeight();
|
||||
if (image instanceof PixelBuffer) {
|
||||
PixelBuffer pb = (PixelBuffer) image;
|
||||
return new Dimension(pb.getWidth(), pb.getHeight());
|
||||
}
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
try {
|
||||
Method mw = image.getClass().getMethod("getWidth");
|
||||
w = ((Number) mw.invoke(image)).intValue();
|
||||
Method mh = image.getClass().getMethod("getHeight");
|
||||
h = ((Number) mh.invoke(image)).intValue();
|
||||
} catch (Throwable ignored) {
|
||||
try {
|
||||
for (Method m : image.getClass().getMethods()) {
|
||||
if (m.getName().equals("getWidth") && m.getParameterCount() == 1) {
|
||||
w = ((Number) m.invoke(image, this.getHODParent())).intValue();
|
||||
}
|
||||
if (m.getName().equals("getHeight") && m.getParameterCount() == 1) {
|
||||
h = ((Number) m.invoke(image, this.getHODParent())).intValue();
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored2) {}
|
||||
}
|
||||
return new Dimension(Math.max(0, w), Math.max(0, h));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Lightweight pure-Java POJO representing border insets.
|
||||
* Completely decouples lib3270j from java.awt.Insets.
|
||||
*/
|
||||
public class Insets implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int top;
|
||||
public int left;
|
||||
public int bottom;
|
||||
public int right;
|
||||
|
||||
public Insets(int top, int left, int bottom, int right) {
|
||||
this.top = top;
|
||||
this.left = left;
|
||||
this.bottom = bottom;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Insets)) return false;
|
||||
Insets i = (Insets) obj;
|
||||
return top == i.top && left == i.left && bottom == i.bottom && right == i.right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(top, left, bottom, right);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + "[top=" + top + ",left=" + left + ",bottom=" + bottom + ",right=" + right + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Platform-neutral pixel buffer interface for 32-bit ARGB offscreen rasterization.
|
||||
* Compatible with pure Java SE (Swing), Android (Bitmap), and headless environments.
|
||||
*/
|
||||
public interface PixelBuffer {
|
||||
|
||||
/** Returns width of the pixel buffer in pixels. */
|
||||
int getWidth();
|
||||
|
||||
/** Returns height of the pixel buffer in pixels. */
|
||||
int getHeight();
|
||||
|
||||
/** Returns contiguous 32-bit ARGB pixel array. */
|
||||
int[] getPixels();
|
||||
|
||||
/** Returns ARGB pixel at specified coordinate, or 0 if out of bounds. */
|
||||
int getPixel(int x, int y);
|
||||
|
||||
/** Sets ARGB pixel at specified coordinate with alpha blending and mix mode. */
|
||||
void setPixel(int x, int y, int argb);
|
||||
|
||||
/** Sets ARGB pixel directly without blending. */
|
||||
void setPixelDirect(int x, int y, int argb);
|
||||
|
||||
/** Clears buffer to fully transparent (0x00000000). */
|
||||
void clear();
|
||||
|
||||
/** Clears buffer to specified ARGB color. */
|
||||
void clear(int argb);
|
||||
|
||||
/** Draws a 1-pixel line using integer Bresenham algorithm. */
|
||||
void drawLine(int x1, int y1, int x2, int y2, int argb);
|
||||
|
||||
/** Draws a 1-pixel line using Bresenham algorithm. */
|
||||
void drawLineBresenham(int x0, int y0, int x1, int y1, int argb);
|
||||
|
||||
/** Draws a stroked line with line type and line width. */
|
||||
void drawLine(double x0, double y0, double x1, double y1, int argb, int lineType, int lineWidth);
|
||||
|
||||
/** Draws an anti-aliased sub-pixel line segment with stroke width. */
|
||||
void drawLineAA(double x0, double y0, double x1, double y1, int argb, double strokeWidth);
|
||||
|
||||
/** Fills a rectangular region with specified ARGB color. */
|
||||
void fillRect(int x, int y, int width, int height, int argb);
|
||||
|
||||
/** Sets clipping rectangle. */
|
||||
void setClip(int x, int y, int width, int height);
|
||||
|
||||
/** Clears clipping rectangle. */
|
||||
void clearClip();
|
||||
|
||||
/** Checks if coordinate falls outside current clipping bounds. */
|
||||
boolean isClipped(int x, int y);
|
||||
|
||||
/** Copies a rectangular block of pixels from source array into this buffer. */
|
||||
void blit(int[] srcPixels, int srcX, int srcY, int srcW, int srcH, int dstX, int dstY);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Lightweight pure-Java POJO point for 2D presentation coordinates.
|
||||
* Completely decouples lib3270j from java.awt.Point.
|
||||
*/
|
||||
public class Point implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int x;
|
||||
public int y;
|
||||
|
||||
public Point() {
|
||||
this(0, 0);
|
||||
}
|
||||
|
||||
public Point(Point p) {
|
||||
this(p != null ? p.x : 0, p != null ? p.y : 0);
|
||||
}
|
||||
|
||||
public Point(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setLocation(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public void setLocation(Point p) {
|
||||
if (p != null) {
|
||||
this.x = p.x;
|
||||
this.y = p.y;
|
||||
}
|
||||
}
|
||||
|
||||
public void translate(int dx, int dy) {
|
||||
this.x += dx;
|
||||
this.y += dy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Point)) return false;
|
||||
Point pt = (Point) obj;
|
||||
return (x == pt.x) && (y == pt.y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + "[x=" + x + ",y=" + y + "]";
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package haus.nightmare.lib3270j.graphics;
|
||||
/**
|
||||
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
|
||||
* Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
|
||||
* Decoupled from java.awt using PixelBuffer.
|
||||
*/
|
||||
public class ProgramSymbolSet {
|
||||
|
||||
@@ -64,8 +65,8 @@ public class ProgramSymbolSet {
|
||||
private int[] cachedRgbArray;
|
||||
private int cachedFgRgb = -1;
|
||||
private int cachedBgRgb = -1;
|
||||
private java.awt.image.BufferedImage cachedImage;
|
||||
private java.awt.image.BufferedImage cachedScaledImage;
|
||||
private PixelBuffer cachedPixelBuffer;
|
||||
private PixelBuffer cachedScaledPixelBuffer;
|
||||
private int cachedTargetW = 0;
|
||||
private int cachedTargetH = 0;
|
||||
private int cachedScaledFgRgb = -1;
|
||||
@@ -103,23 +104,21 @@ public class ProgramSymbolSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
|
||||
* Enables unscaled 1:1 hardware blitting in Java2D.
|
||||
* Returns a PixelBuffer scaled directly to target dimensions (cellWidth x cellHeight).
|
||||
*/
|
||||
public synchronized java.awt.image.BufferedImage getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) {
|
||||
public synchronized PixelBuffer getScaledPixelBuffer(int targetW, int targetH, int fgArgb, int bgArgb) {
|
||||
if (targetW <= 0 || targetH <= 0) {
|
||||
return getImage(fgArgb, bgArgb);
|
||||
return getPixelBuffer(fgArgb, bgArgb);
|
||||
}
|
||||
if (targetW == width && targetH == height) {
|
||||
return getImage(fgArgb, bgArgb);
|
||||
return getPixelBuffer(fgArgb, bgArgb);
|
||||
}
|
||||
if (cachedScaledImage != null && cachedTargetW == targetW && cachedTargetH == targetH
|
||||
if (cachedScaledPixelBuffer != null && cachedTargetW == targetW && cachedTargetH == targetH
|
||||
&& cachedScaledFgRgb == fgArgb && cachedScaledBgRgb == bgArgb) {
|
||||
return cachedScaledImage;
|
||||
return cachedScaledPixelBuffer;
|
||||
}
|
||||
int[] srcRgb = getRgbPixels(fgArgb, bgArgb);
|
||||
java.awt.image.BufferedImage scaled = new java.awt.image.BufferedImage(targetW, targetH, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
int[] dstRgb = ((java.awt.image.DataBufferInt) scaled.getRaster().getDataBuffer()).getData();
|
||||
int[] dstRgb = new int[targetW * targetH];
|
||||
|
||||
for (int dy = 0; dy < targetH; dy++) {
|
||||
int sy = dy * height / targetH;
|
||||
@@ -131,30 +130,36 @@ public class ProgramSymbolSet {
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedScaledImage = scaled;
|
||||
this.cachedScaledPixelBuffer = new DefaultPixelBuffer(targetW, targetH, dstRgb);
|
||||
this.cachedTargetW = targetW;
|
||||
this.cachedTargetH = targetH;
|
||||
this.cachedScaledFgRgb = fgArgb;
|
||||
this.cachedScaledBgRgb = bgArgb;
|
||||
return scaled;
|
||||
return this.cachedScaledPixelBuffer;
|
||||
}
|
||||
|
||||
public synchronized PixelBuffer getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) {
|
||||
return getScaledPixelBuffer(targetW, targetH, fgArgb, bgArgb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and returns the cached BufferedImage for this symbol glyph.
|
||||
* Eliminates per-cell heap allocations during high frame rate rendering.
|
||||
* Computes and returns the cached PixelBuffer for this symbol glyph.
|
||||
*/
|
||||
public synchronized java.awt.image.BufferedImage getImage(int fgArgb, int bgArgb) {
|
||||
if (cachedImage != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
||||
return cachedImage;
|
||||
public synchronized PixelBuffer getPixelBuffer(int fgArgb, int bgArgb) {
|
||||
if (cachedPixelBuffer != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
||||
return cachedPixelBuffer;
|
||||
}
|
||||
int[] rgb = getRgbPixels(fgArgb, bgArgb);
|
||||
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
int[] imgData = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData();
|
||||
int[] imgData = new int[rgb.length];
|
||||
System.arraycopy(rgb, 0, imgData, 0, rgb.length);
|
||||
this.cachedImage = img;
|
||||
this.cachedPixelBuffer = new DefaultPixelBuffer(width, height, imgData);
|
||||
this.cachedFgRgb = fgArgb;
|
||||
this.cachedBgRgb = bgArgb;
|
||||
return img;
|
||||
return this.cachedPixelBuffer;
|
||||
}
|
||||
|
||||
public synchronized PixelBuffer getImage(int fgArgb, int bgArgb) {
|
||||
return getPixelBuffer(fgArgb, bgArgb);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Lightweight pure-Java POJO rectangle for 2D presentation coordinates.
|
||||
* Completely decouples lib3270j from java.awt.Rectangle.
|
||||
*/
|
||||
public class Rectangle implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int x;
|
||||
public int y;
|
||||
public int width;
|
||||
public int height;
|
||||
|
||||
public Rectangle() {
|
||||
this(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public Rectangle(Rectangle r) {
|
||||
this(r != null ? r.x : 0, r != null ? r.y : 0, r != null ? r.width : 0, r != null ? r.height : 0);
|
||||
}
|
||||
|
||||
public Rectangle(int x, int y, int width, int height) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Rectangle(int width, int height) {
|
||||
this(0, 0, width, height);
|
||||
}
|
||||
|
||||
public Rectangle(Point p, Dimension d) {
|
||||
this(p != null ? p.x : 0, p != null ? p.y : 0, d != null ? d.width : 0, d != null ? d.height : 0);
|
||||
}
|
||||
|
||||
public Rectangle(Point p) {
|
||||
this(p != null ? p.x : 0, p != null ? p.y : 0, 0, 0);
|
||||
}
|
||||
|
||||
public Rectangle(Dimension d) {
|
||||
this(0, 0, d != null ? d.width : 0, d != null ? d.height : 0);
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setBounds(int x, int y, int width, int height) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public void setBounds(Rectangle r) {
|
||||
if (r != null) {
|
||||
setBounds(r.x, r.y, r.width, r.height);
|
||||
}
|
||||
}
|
||||
|
||||
public Point getLocation() {
|
||||
return new Point(x, y);
|
||||
}
|
||||
|
||||
public void setLocation(Point p) {
|
||||
if (p != null) {
|
||||
this.x = p.x;
|
||||
this.y = p.y;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLocation(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public Dimension getSize() {
|
||||
return new Dimension(width, height);
|
||||
}
|
||||
|
||||
public void setSize(Dimension d) {
|
||||
if (d != null) {
|
||||
this.width = d.width;
|
||||
this.height = d.height;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSize(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public boolean contains(int X, int Y) {
|
||||
int w = this.width;
|
||||
int h = this.height;
|
||||
if ((w | h) < 0) return false;
|
||||
int x = this.x;
|
||||
int y = this.y;
|
||||
if (X < x || Y < y) return false;
|
||||
w += x;
|
||||
h += y;
|
||||
return ((w < x || w > X) && (h < y || h > Y));
|
||||
}
|
||||
|
||||
public boolean contains(Point p) {
|
||||
return p != null && contains(p.x, p.y);
|
||||
}
|
||||
|
||||
public boolean intersects(Rectangle r) {
|
||||
if (r == null) return false;
|
||||
int tw = this.width;
|
||||
int th = this.height;
|
||||
int rw = r.width;
|
||||
int rh = r.height;
|
||||
if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) return false;
|
||||
int tx = this.x;
|
||||
int ty = this.y;
|
||||
int rx = r.x;
|
||||
int ry = r.y;
|
||||
rw += rx;
|
||||
rh += ry;
|
||||
tw += tx;
|
||||
th += ty;
|
||||
return ((rw < rx || rw > tx) &&
|
||||
(rh < ry || rh > ty) &&
|
||||
(tw < tx || tw > rx) &&
|
||||
(th < ty || th > ry));
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return (width <= 0) || (height <= 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Rectangle)) return false;
|
||||
Rectangle r = (Rectangle) obj;
|
||||
return ((x == r.x) &&
|
||||
(y == r.y) &&
|
||||
(width == r.width) &&
|
||||
(height == r.height));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(x, y, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + "[x=" + x + ",y=" + y + ",width=" + width + ",height=" + height + "]";
|
||||
}
|
||||
}
|
||||
@@ -412,6 +412,155 @@ public class InputProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all character locations on the given row are protected.
|
||||
*/
|
||||
public boolean isLineProtected(int row) {
|
||||
if (screen == null || !screen.isFormatted()) return false;
|
||||
int cols = screen.getCols();
|
||||
int rows = screen.getRows();
|
||||
if (row < 0 || row >= rows) return false;
|
||||
int start = row * cols;
|
||||
int end = start + cols;
|
||||
for (int i = start; i < end; i++) {
|
||||
if (screen.getCell(i).isFieldAttribute()) continue;
|
||||
byte fa = screen.getFieldAttributeAt(i);
|
||||
if (!faIsProtected(fa & 0xFF)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste text into the presentation space.
|
||||
* When enablePasteFromExcel is true:
|
||||
* - '\t' triggers a Tab action advancing to the next unprotected input field (SBA).
|
||||
* - '\n' or '\r\n' triggers a NewLine action advancing to the first unprotected field of the next line.
|
||||
* When pasteStopAtProtectedLine is true:
|
||||
* - Halts or truncates paste if cursor reaches a protected line/boundary or if the current field is full.
|
||||
*
|
||||
* @param text text to paste
|
||||
* @param enablePasteFromExcel whether to parse tabs as field advances and newlines as row advances
|
||||
* @param pasteStopAtProtectedLine whether to halt paste when encountering protected boundaries
|
||||
* @return number of characters pasted
|
||||
*/
|
||||
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) {
|
||||
if (text == null || text.isEmpty() || screen == null || keyboardLocked) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isNvtMode()) {
|
||||
try {
|
||||
fsm.sendNVTString(text);
|
||||
return text.length();
|
||||
} catch (IOException e) {
|
||||
log.warning("Failed to send NVT paste: " + e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
int len = text.length();
|
||||
int i = 0;
|
||||
|
||||
while (i < len && !keyboardLocked) {
|
||||
char ch = text.charAt(i);
|
||||
|
||||
// Handle newline sequences: \r\n, \r, or \n
|
||||
if (ch == '\r' || ch == '\n') {
|
||||
if (ch == '\r' && (i + 1) < len && text.charAt(i + 1) == '\n') {
|
||||
i++; // skip \n of \r\n
|
||||
}
|
||||
if (enablePasteFromExcel) {
|
||||
int curRow = screen.getCursorRow();
|
||||
int nextRow = (curRow + 1) % screen.getRows();
|
||||
if (pasteStopAtProtectedLine && isLineProtected(nextRow)) {
|
||||
break; // Stop paste when next line is protected
|
||||
}
|
||||
newline();
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle tab character
|
||||
if (ch == '\t') {
|
||||
if (enablePasteFromExcel) {
|
||||
tab();
|
||||
int newAddr = screen.getCursorAddress();
|
||||
if (pasteStopAtProtectedLine && screen.isFormatted()) {
|
||||
byte fa = screen.getFieldAttributeAt(newAddr);
|
||||
if (faIsProtected(fa & 0xFF)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular printable character
|
||||
if (ch >= 0x20 && ch != 0x7F) {
|
||||
if (screen.isFormatted()) {
|
||||
int baddr = screen.getCursorAddress();
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
if (ea.isFieldAttribute()) {
|
||||
baddr = (baddr + 1) % (screen.getRows() * screen.getCols());
|
||||
}
|
||||
byte faVal = screen.getFieldAttributeAt(baddr);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
if (pasteStopAtProtectedLine) {
|
||||
break; // Stop paste immediately at protected boundary
|
||||
} else {
|
||||
tab();
|
||||
baddr = screen.getCursorAddress();
|
||||
if (faIsProtected(screen.getFieldAttributeAt(baddr) & 0xFF)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typeCharacter(ch);
|
||||
count++;
|
||||
|
||||
// If pasteStopAtProtectedLine is enabled, check if the cursor after typing has hit a protected field
|
||||
if (pasteStopAtProtectedLine && screen.isFormatted() && !keyboardLocked) {
|
||||
int curAddr = screen.getCursorAddress();
|
||||
ExtendedAttribute curCell = screen.getCell(curAddr);
|
||||
if (curCell.isFieldAttribute()) {
|
||||
byte nextFa = curCell.fa;
|
||||
if (faIsProtected(nextFa & 0xFF)) {
|
||||
if (i + 1 < len) {
|
||||
char nextCh = text.charAt(i + 1);
|
||||
if (nextCh != '\t' && nextCh != '\r' && nextCh != '\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
byte curFa = screen.getFieldAttributeAt(curAddr);
|
||||
if (faIsProtected(curFa & 0xFF)) {
|
||||
if (i + 1 < len) {
|
||||
char nextCh = text.charAt(i + 1);
|
||||
if (nextCh != '\t' && nextCh != '\r' && nextCh != '\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build inbound 3270 Read Modified data stream (AID + Cursor + SBA + Modified fields).
|
||||
*/
|
||||
|
||||
@@ -14,4 +14,7 @@ public interface ConnectionListener {
|
||||
|
||||
/** Called when TN3270E negotiation completes. */
|
||||
default void onTN3270ENegotiated(String deviceType, String deviceName) {}
|
||||
|
||||
/** Called when TN3270E functions negotiation completes or changes. */
|
||||
default void onTN3270EFunctionsNegotiated(boolean[] functions) {}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,7 @@ public interface ScreenUpdateListener {
|
||||
|
||||
/** Called when the screen size changes (erase/write vs erase/write alternate). */
|
||||
default void onScreenSizeChanged(int rows, int cols) {}
|
||||
|
||||
/** Called when the keyboard is unlocked (e.g. via WCC restore, AUTO_SYS_UNLOCK, or Contention Resolution SDI). */
|
||||
default void onKeyboardUnlocked() {}
|
||||
}
|
||||
|
||||
@@ -1305,6 +1305,9 @@ public class NvtProcessor {
|
||||
}
|
||||
|
||||
private void notifyScreenUpdated() {
|
||||
if (screenBuffer != null) {
|
||||
screenBuffer.notifyScreenUpdate();
|
||||
}
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
|
||||
/**
|
||||
@@ -60,6 +65,61 @@ public class ScreenBuffer {
|
||||
private final EbcdicTranslator translator;
|
||||
private final Object renderLock = new Object();
|
||||
|
||||
// Synchronization primitives and screen update listeners (Phase 12)
|
||||
private final ReentrantLock syncLock = new ReentrantLock();
|
||||
private final Condition syncCondition = syncLock.newCondition();
|
||||
private final List<ScreenUpdateListener> updateListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
public ReentrantLock getSyncLock() {
|
||||
return syncLock;
|
||||
}
|
||||
|
||||
public Condition getSyncCondition() {
|
||||
return syncCondition;
|
||||
}
|
||||
|
||||
public void addUpdateListener(ScreenUpdateListener l) {
|
||||
if (l != null && !updateListeners.contains(l)) {
|
||||
updateListeners.add(l);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeUpdateListener(ScreenUpdateListener l) {
|
||||
updateListeners.remove(l);
|
||||
}
|
||||
|
||||
public void signalWaiters() {
|
||||
syncLock.lock();
|
||||
try {
|
||||
syncCondition.signalAll();
|
||||
} finally {
|
||||
syncLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyScreenUpdate() {
|
||||
updateDisplaySnapshot();
|
||||
signalWaiters();
|
||||
for (ScreenUpdateListener l : updateListeners) {
|
||||
try {
|
||||
l.onScreenUpdated();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyCursorMoved(int oldAddress, int newAddress) {
|
||||
signalWaiters();
|
||||
for (ScreenUpdateListener l : updateListeners) {
|
||||
try {
|
||||
l.onCursorMoved(oldAddress, newAddress);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyCursorMoved() {
|
||||
notifyCursorMoved(cursorAddress, cursorAddress);
|
||||
}
|
||||
|
||||
public Object getRenderLock() {
|
||||
return renderLock;
|
||||
}
|
||||
@@ -272,9 +332,16 @@ public class ScreenBuffer {
|
||||
|
||||
// ========== Cursor ==========
|
||||
public int getCursorAddress() { return cursorAddress; }
|
||||
public synchronized void setCursorAddress(int addr) {
|
||||
this.cursorAddress = addr;
|
||||
this.displayCursorAddress = addr;
|
||||
public void setCursorAddress(int addr) {
|
||||
int oldAddr;
|
||||
synchronized (this) {
|
||||
oldAddr = this.cursorAddress;
|
||||
this.cursorAddress = addr;
|
||||
this.displayCursorAddress = addr;
|
||||
}
|
||||
if (oldAddr != addr) {
|
||||
notifyCursorMoved(oldAddr, addr);
|
||||
}
|
||||
}
|
||||
public synchronized void setCursorPosition(int row, int col) {
|
||||
int r = Math.max(0, Math.min(row, rows - 1));
|
||||
@@ -831,19 +898,28 @@ public class ScreenBuffer {
|
||||
}
|
||||
|
||||
public synchronized void setText(String text) {
|
||||
setText(text, 0);
|
||||
}
|
||||
|
||||
public synchronized void setText(String text, int pos) {
|
||||
if (text == null) return;
|
||||
int size = rows * cols;
|
||||
int len = Math.min(text.length(), size);
|
||||
if (pos < 0 || pos >= size) return;
|
||||
int len = Math.min(text.length(), size - pos);
|
||||
for (int i = 0; i < len; i++) {
|
||||
char ch = text.charAt(i);
|
||||
int ebc = translator.unicodeToEbcdic(ch);
|
||||
buffer[i].ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||
buffer[i].ucs4 = ch;
|
||||
buffer[pos + i].ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||
buffer[pos + i].ucs4 = ch;
|
||||
}
|
||||
screenChanged = true;
|
||||
updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void setText(String text, int row, int col) {
|
||||
setText(text, row * cols + col);
|
||||
}
|
||||
|
||||
public int searchString(String target) {
|
||||
if (target == null || target.isEmpty()) return -1;
|
||||
String full = getText();
|
||||
@@ -1119,8 +1195,10 @@ public class ScreenBuffer {
|
||||
ExtendedAttribute[] wordCells = new ExtendedAttribute[wordLen];
|
||||
for (int i = 0; i < wordLen; i++) {
|
||||
wordCells[i] = new ExtendedAttribute();
|
||||
wordCells[i].copyFrom(getCell(wordStartAddr + i));
|
||||
getCell(wordStartAddr + i).clear();
|
||||
ExtendedAttribute srcCell = getCell(wordStartAddr + i);
|
||||
wordCells[i].copyFrom(srcCell);
|
||||
srcCell.ec = 0;
|
||||
srcCell.ucs4 = 0;
|
||||
}
|
||||
|
||||
int nextRow = (curRow + 1) % rows;
|
||||
|
||||
@@ -6,6 +6,8 @@ import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -28,6 +30,10 @@ public class TelnetConnection {
|
||||
private final TelnetFSM fsm;
|
||||
private final ConnectionConfig config;
|
||||
|
||||
private final AtomicLong lastActivityTime = new AtomicLong(System.currentTimeMillis());
|
||||
private ScheduledExecutorService keepAliveExecutor;
|
||||
private volatile boolean intentionalDisconnect = false;
|
||||
|
||||
private javax.net.ssl.SSLSession sslSession;
|
||||
|
||||
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||
@@ -91,6 +97,7 @@ public class TelnetConnection {
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
rawSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
applyExtendedSocketOptions(rawSocket);
|
||||
rawSocket.connect(new InetSocketAddress(connectHost, connectPort), config.getConnectTimeoutMs());
|
||||
|
||||
// Perform proxy handshake if configured
|
||||
@@ -123,6 +130,7 @@ public class TelnetConnection {
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
applyExtendedSocketOptions(sslSocket);
|
||||
applyTlsSocketSettings(sslSocket);
|
||||
sslSocket.startHandshake();
|
||||
socket = sslSocket;
|
||||
@@ -143,10 +151,13 @@ public class TelnetConnection {
|
||||
|
||||
log.info("Connected to " + socket.getRemoteSocketAddress());
|
||||
|
||||
intentionalDisconnect = false;
|
||||
lastActivityTime.set(System.currentTimeMillis());
|
||||
running = true;
|
||||
readerThread = new Thread(this::readLoop, "TN3270-Reader");
|
||||
readerThread.setDaemon(true);
|
||||
readerThread.start();
|
||||
startKeepAlive();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,6 +178,7 @@ public class TelnetConnection {
|
||||
if (config.getSoTimeoutMs() > 0) {
|
||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||
}
|
||||
applyExtendedSocketOptions(sslSocket);
|
||||
applyTlsSocketSettings(sslSocket);
|
||||
sslSocket.startHandshake();
|
||||
this.socket = sslSocket;
|
||||
@@ -200,6 +212,106 @@ public class TelnetConnection {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyExtendedSocketOptions(Socket s) {
|
||||
if (s == null) return;
|
||||
try {
|
||||
Class<?> extClass = Class.forName("jdk.net.ExtendedSocketOptions");
|
||||
// TCP_KEEPIDLE, TCP_KEEPINTERVAL, TCP_KEEPCOUNT
|
||||
if (config != null && config.isSoKeepAlive() && config.getKeepAliveIntervalSeconds() > 0) {
|
||||
try {
|
||||
java.lang.reflect.Field fIdle = extClass.getField("TCP_KEEPIDLE");
|
||||
@SuppressWarnings("unchecked")
|
||||
SocketOption<Integer> optIdle = (SocketOption<Integer>) fIdle.get(null);
|
||||
s.setOption(optIdle, config.getKeepAliveIntervalSeconds());
|
||||
} catch (Throwable ignored) {}
|
||||
try {
|
||||
java.lang.reflect.Field fIntv = extClass.getField("TCP_KEEPINTERVAL");
|
||||
@SuppressWarnings("unchecked")
|
||||
SocketOption<Integer> optIntv = (SocketOption<Integer>) fIntv.get(null);
|
||||
s.setOption(optIntv, Math.max(1, Math.min(10, config.getKeepAliveIntervalSeconds())));
|
||||
} catch (Throwable ignored) {}
|
||||
try {
|
||||
java.lang.reflect.Field fCnt = extClass.getField("TCP_KEEPCOUNT");
|
||||
@SuppressWarnings("unchecked")
|
||||
SocketOption<Integer> optCnt = (SocketOption<Integer>) fCnt.get(null);
|
||||
s.setOption(optCnt, 3);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
// TCP_USER_TIMEOUT
|
||||
if (config != null && config.getTcpUserTimeoutMs() > 0) {
|
||||
try {
|
||||
java.lang.reflect.Field fTimeout = extClass.getField("TCP_USER_TIMEOUT");
|
||||
@SuppressWarnings("unchecked")
|
||||
SocketOption<Integer> optTimeout = (SocketOption<Integer>) fTimeout.get(null);
|
||||
s.setOption(optTimeout, config.getTcpUserTimeoutMs());
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// Extended socket options not supported on this platform/runtime (e.g. macOS/Android)
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void startKeepAlive() {
|
||||
stopKeepAlive();
|
||||
if (config != null && config.isKeepAliveEnabled() && config.getKeepAliveIntervalSeconds() > 0) {
|
||||
keepAliveExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "TN3270-KeepAlive");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
int interval = config.getKeepAliveIntervalSeconds();
|
||||
long checkPeriod = Math.max(1, Math.min(5, interval));
|
||||
keepAliveExecutor.scheduleWithFixedDelay(this::checkAndSendKeepAlive, checkPeriod, checkPeriod, TimeUnit.SECONDS);
|
||||
log.fine("Keep-Alive heartbeat scheduled every " + interval + "s (check every " + checkPeriod + "s)");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stopKeepAlive() {
|
||||
if (keepAliveExecutor != null) {
|
||||
keepAliveExecutor.shutdownNow();
|
||||
keepAliveExecutor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndSendKeepAlive() {
|
||||
if (!running || !isConnected()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
long elapsed = now - lastActivityTime.get();
|
||||
long intervalMs = (config != null ? config.getKeepAliveIntervalSeconds() : 120) * 1000L;
|
||||
if (elapsed >= intervalMs) {
|
||||
try {
|
||||
sendKeepAliveHeartbeat();
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "Failed to send keep-alive heartbeat", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void sendKeepAliveHeartbeat() throws IOException {
|
||||
if (!isConnected() || outputStream == null) {
|
||||
return;
|
||||
}
|
||||
String type = (config != null) ? config.getKeepAliveType() : "NOP";
|
||||
if ("TIMING-MARK".equalsIgnoreCase(type)) {
|
||||
log.fine("Transmitting Keep-Alive heartbeat: IAC DO TIMING-MARK");
|
||||
byte[] tm = new byte[] { (byte) IAC, (byte) DO, (byte) TELOPT_TM };
|
||||
outputStream.write(tm);
|
||||
outputStream.flush();
|
||||
} else {
|
||||
log.fine("Transmitting Keep-Alive heartbeat: IAC NOP");
|
||||
byte[] nop = new byte[] { (byte) IAC, (byte) NOP };
|
||||
outputStream.write(nop);
|
||||
outputStream.flush();
|
||||
}
|
||||
lastActivityTime.set(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public long getLastActivityTime() {
|
||||
return lastActivityTime.get();
|
||||
}
|
||||
|
||||
private void establishHttpProxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException {
|
||||
OutputStream out = s.getOutputStream();
|
||||
InputStream in = s.getInputStream();
|
||||
@@ -411,6 +523,7 @@ public class TelnetConnection {
|
||||
if (outputStream == null) return;
|
||||
outputStream.write(data, offset, length);
|
||||
outputStream.flush();
|
||||
lastActivityTime.set(System.currentTimeMillis());
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine("SENT " + length + " bytes: " + formatHex(data, offset, length));
|
||||
}
|
||||
@@ -432,13 +545,16 @@ public class TelnetConnection {
|
||||
byte[] escaped = out.toByteArray();
|
||||
outputStream.write(escaped, 0, escaped.length);
|
||||
outputStream.flush();
|
||||
lastActivityTime.set(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the host.
|
||||
*/
|
||||
public void disconnect() {
|
||||
intentionalDisconnect = true;
|
||||
running = false;
|
||||
stopKeepAlive();
|
||||
try {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.shutdownInput();
|
||||
@@ -471,10 +587,12 @@ public class TelnetConnection {
|
||||
int n = inputStream.read(buf);
|
||||
if (n < 0) {
|
||||
log.info("Host disconnected (EOF)");
|
||||
fsm.onDisconnect();
|
||||
boolean unexpected = !intentionalDisconnect;
|
||||
fsm.onDisconnect(unexpected);
|
||||
break;
|
||||
}
|
||||
if (n > 0) {
|
||||
lastActivityTime.set(System.currentTimeMillis());
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
|
||||
}
|
||||
@@ -486,14 +604,23 @@ public class TelnetConnection {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketTimeoutException e) {
|
||||
if (running && !intentionalDisconnect) {
|
||||
log.warning("Socket read timeout (" + (config != null ? config.getSoTimeoutMs() : 0) + "ms): " + e.getMessage());
|
||||
fsm.onDisconnect(true);
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
if (running) {
|
||||
log.info("Socket closed: " + e.getMessage());
|
||||
fsm.onDisconnect();
|
||||
if (running && !intentionalDisconnect) {
|
||||
log.info("Socket closed unexpectedly: " + e.getMessage());
|
||||
fsm.onDisconnect(true);
|
||||
} else if (running) {
|
||||
fsm.onDisconnect(false);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
log.log(Level.WARNING, "Read error", e);
|
||||
if (running && !intentionalDisconnect) {
|
||||
log.log(Level.WARNING, "Read error: " + e.getMessage());
|
||||
fsm.onDisconnect(true);
|
||||
} else if (running) {
|
||||
fsm.onError("Read error: " + e.getMessage());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
@@ -501,6 +628,8 @@ public class TelnetConnection {
|
||||
log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t);
|
||||
fsm.onError("Network loop error: " + t.getMessage());
|
||||
}
|
||||
} finally {
|
||||
stopKeepAlive();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,11 @@ public class TelnetFSM {
|
||||
private String connectedLu;
|
||||
private String connectedType;
|
||||
|
||||
// Phase 10: Contention Resolution & Auto-Unlock State
|
||||
private boolean sdi_flag = false;
|
||||
private boolean kri_flag = false;
|
||||
private boolean negotiateContentionResolution = true;
|
||||
|
||||
public enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
|
||||
|
||||
public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) {
|
||||
@@ -190,7 +195,7 @@ public class TelnetFSM {
|
||||
eFuncs[FUNC_SYSREQ] = true;
|
||||
eFuncs[FUNC_SNA_SENSE] = true;
|
||||
eFuncs[FUNC_DATA_STREAM_CTL] = true;
|
||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = true;
|
||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = negotiateContentionResolution;
|
||||
|
||||
statusDisplay(STATUS_CONNECTING, "Connecting to host");
|
||||
changeState(ConnectionState.TELNET_PENDING);
|
||||
@@ -951,6 +956,10 @@ public class TelnetFSM {
|
||||
log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames());
|
||||
log.info("TN3270E negotiation complete");
|
||||
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.setContentionResolution(isContentionResolutionNegotiated());
|
||||
}
|
||||
|
||||
// RFC 2355: If BIND-IMAGE function is not negotiated, the emulation session is considered
|
||||
// to be bound immediately upon completion of the FUNCTIONS negotiation.
|
||||
if (eFuncs[FUNC_BIND_IMAGE]) {
|
||||
@@ -963,6 +972,7 @@ public class TelnetFSM {
|
||||
// Notify listeners
|
||||
for (ConnectionListener l : connectionListeners) {
|
||||
l.onTN3270ENegotiated(connectedType, connectedLu);
|
||||
l.onTN3270EFunctionsNegotiated(eFuncs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -996,9 +1006,59 @@ public class TelnetFSM {
|
||||
processTN3270ERecord(data);
|
||||
} else {
|
||||
// Plain TN3270 mode: data is raw 3270 data stream
|
||||
dsProcessor.processRecord(data, 0, data.length, true);
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.processRecord(data, 0, data.length, false);
|
||||
}
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
|
||||
// Phase 10: Contention Resolution & AUTO_SYS_UNLOCK handling on EOR
|
||||
if (dsProcessor != null) {
|
||||
boolean crActive = isContentionResolutionNegotiated();
|
||||
if (crActive) {
|
||||
if (this.sdi_flag && !dsProcessor.isRcvdRead()) {
|
||||
if (dsProcessor.getInputProcessor() != null) {
|
||||
dsProcessor.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
this.sdi_flag = false;
|
||||
dsProcessor.setUnlockPending(false);
|
||||
if (dsProcessor.isUnlockSysPending() || this.kri_flag) {
|
||||
if (dsProcessor.getInputProcessor() != null && dsProcessor.getInputProcessor().getOIA() != null) {
|
||||
dsProcessor.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
this.kri_flag = false;
|
||||
dsProcessor.setUnlockSysPending(false);
|
||||
notifyKeyboardUnlocked();
|
||||
}
|
||||
this.kri_flag = false;
|
||||
} else {
|
||||
// Contention Resolution is not active
|
||||
boolean autoSysUnlock = (config != null) ? config.isAutoSysUnlock() : true;
|
||||
if (autoSysUnlock && !dsProcessor.isRcvdRead()) {
|
||||
boolean sysLocked = false;
|
||||
if (dsProcessor.getInputProcessor() != null) {
|
||||
if (dsProcessor.getInputProcessor().isKeyboardLocked() ||
|
||||
(dsProcessor.getInputProcessor().getOIA() != null && dsProcessor.getInputProcessor().getOIA().isXSystem())) {
|
||||
sysLocked = true;
|
||||
}
|
||||
}
|
||||
if (sysLocked) {
|
||||
if (dsProcessor.getInputProcessor() != null) {
|
||||
dsProcessor.getInputProcessor().setKeyboardLocked(false);
|
||||
if (dsProcessor.getInputProcessor().getOIA() != null) {
|
||||
dsProcessor.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
dsProcessor.setUnlockSysPending(false);
|
||||
notifyKeyboardUnlocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
this.sdi_flag = false;
|
||||
this.kri_flag = false;
|
||||
dsProcessor.setRcvdRead(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void processTn3270eHeader(byte[] data) {
|
||||
@@ -1016,6 +1076,12 @@ public class TelnetFSM {
|
||||
int responseFlag = data[2] & 0xFF;
|
||||
int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF);
|
||||
|
||||
this.sdi_flag = (requestFlag & 0x01) != 0;
|
||||
this.kri_flag = (requestFlag & 0x02) != 0;
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.setContentionResolution(isContentionResolutionNegotiated());
|
||||
}
|
||||
|
||||
log.fine("TN3270E header: type=" + TN3270EConstants.dataTypeName(dataType) +
|
||||
" rqf=" + requestFlag + " rsf=" + responseFlag + " seq=" + seqNumber);
|
||||
|
||||
@@ -1033,7 +1099,7 @@ public class TelnetFSM {
|
||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||
}
|
||||
try {
|
||||
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true);
|
||||
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, false);
|
||||
notifyScreenUpdate();
|
||||
// Send positive response if required
|
||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||
@@ -1423,8 +1489,49 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
public void setConnectionState(ConnectionState newState) {
|
||||
changeState(newState);
|
||||
}
|
||||
|
||||
public void onDisconnect() {
|
||||
changeState(ConnectionState.NOT_CONNECTED);
|
||||
onDisconnect(false);
|
||||
}
|
||||
|
||||
public void onDisconnect(boolean unexpected) {
|
||||
if (unexpected && config != null && config.isAutoReconnect()) {
|
||||
log.info("Unexpected connection loss — transitioning to RECONNECTING");
|
||||
changeState(ConnectionState.RECONNECTING);
|
||||
} else {
|
||||
changeState(ConnectionState.NOT_CONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanly reset Telnet and TN3270E session state prior to reconnecting.
|
||||
*/
|
||||
public synchronized void resetSessionState() {
|
||||
state = TNS_DATA;
|
||||
java.util.Arrays.fill(myOpts, false);
|
||||
java.util.Arrays.fill(hisOpts, false);
|
||||
ibuf.reset();
|
||||
sbbuf.reset();
|
||||
tn3270eNegotiated = false;
|
||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||
tn3270eBound = false;
|
||||
java.util.Arrays.fill(eFuncs, false);
|
||||
eXmitSeq = 0;
|
||||
lastRcvSeq = 0;
|
||||
lastRespType = 0;
|
||||
lastRespCode = 0;
|
||||
responseRequired = RSF_NO_RESPONSE;
|
||||
deferredWillTtype = false;
|
||||
tn3270eDeviceTypeSent = false;
|
||||
ttypeIndex = 0;
|
||||
luIndex = 0;
|
||||
connectedLu = null;
|
||||
connectedType = null;
|
||||
sdi_flag = false;
|
||||
kri_flag = false;
|
||||
}
|
||||
|
||||
public void onError(String message) {
|
||||
@@ -1434,7 +1541,10 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyScreenUpdate() {
|
||||
public void notifyScreenUpdate() {
|
||||
if (screenBuffer != null) {
|
||||
screenBuffer.notifyScreenUpdate();
|
||||
}
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
l.onScreenUpdated();
|
||||
}
|
||||
@@ -1810,4 +1920,41 @@ public class TelnetFSM {
|
||||
public boolean isFunctionNegotiated(int func) {
|
||||
return func >= 0 && func < eFuncs.length && eFuncs[func];
|
||||
}
|
||||
|
||||
public boolean isContentionResolutionNegotiated() {
|
||||
return tn3270eNegotiated && eFuncs[FUNC_CONTENTION_RESOLUTION];
|
||||
}
|
||||
|
||||
public boolean isNegotiateContentionResolution() {
|
||||
return negotiateContentionResolution;
|
||||
}
|
||||
|
||||
public void setNegotiateContentionResolution(boolean neg) {
|
||||
this.negotiateContentionResolution = neg;
|
||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = neg;
|
||||
}
|
||||
|
||||
public void setContentionResolutionNegotiated(boolean cr) {
|
||||
if (cr) {
|
||||
this.tn3270eNegotiated = true;
|
||||
}
|
||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = cr;
|
||||
if (dsProcessor != null) {
|
||||
dsProcessor.setContentionResolution(cr);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSdi_flag() { return sdi_flag; }
|
||||
public void setSdi_flag(boolean sdi) { this.sdi_flag = sdi; }
|
||||
|
||||
public boolean isKri_flag() { return kri_flag; }
|
||||
public void setKri_flag(boolean kri) { this.kri_flag = kri; }
|
||||
|
||||
public void notifyKeyboardUnlocked() {
|
||||
for (ScreenUpdateListener l : screenListeners) {
|
||||
try {
|
||||
l.onKeyboardUnlocked();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,14 @@ public class DS3270 {
|
||||
public boolean suppressClearEvent;
|
||||
public boolean wsfvalid = true;
|
||||
|
||||
// Phase 10: State tracking matching HoD DS3270
|
||||
protected boolean autoSysUnlock = true;
|
||||
protected boolean sdi_flag = false;
|
||||
protected boolean kri_flag = false;
|
||||
protected boolean unlock_pending = false;
|
||||
protected boolean unlock_sys_pending = false;
|
||||
protected boolean rcvdRead = false;
|
||||
|
||||
// Underlying lib3270j data stream processor
|
||||
private final DataStreamProcessor delegate;
|
||||
private ECLSession session;
|
||||
@@ -172,9 +180,15 @@ public class DS3270 {
|
||||
public DS3270(ECLSession session, ECLPS ps) {
|
||||
this.session = session;
|
||||
this.ps = ps;
|
||||
if (session != null) {
|
||||
this.autoSysUnlock = session.isAutoSysUnlock();
|
||||
}
|
||||
ScreenBuffer sb = (ps != null) ? ps.getScreenBuffer() : new ScreenBuffer();
|
||||
EbcdicTranslator trans = (ps != null) ? ps.getTranslator() : new EbcdicTranslator();
|
||||
this.delegate = new DataStreamProcessor(sb, trans);
|
||||
if (session != null) {
|
||||
this.delegate.setAutoSysUnlock(session.isAutoSysUnlock());
|
||||
}
|
||||
if (ps != null && ps.getInputProcessor() != null) {
|
||||
this.delegate.setInputProcessor(ps.getInputProcessor());
|
||||
}
|
||||
@@ -214,8 +228,135 @@ public class DS3270 {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isAutoSysUnlock() {
|
||||
return autoSysUnlock;
|
||||
}
|
||||
|
||||
public void setAutoSysUnlock(boolean autoSysUnlock) {
|
||||
this.autoSysUnlock = autoSysUnlock;
|
||||
if (delegate != null) {
|
||||
delegate.setAutoSysUnlock(autoSysUnlock);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSdi_flag() { return sdi_flag; }
|
||||
public void setSdi_flag(boolean sdi) { this.sdi_flag = sdi; }
|
||||
|
||||
public boolean isKri_flag() { return kri_flag; }
|
||||
public void setKri_flag(boolean kri) { this.kri_flag = kri; }
|
||||
|
||||
public boolean isUnlock_pending() { return unlock_pending; }
|
||||
public void setUnlock_pending(boolean pending) { this.unlock_pending = pending; }
|
||||
|
||||
public boolean isUnlock_sys_pending() { return unlock_sys_pending; }
|
||||
public void setUnlock_sys_pending(boolean pending) { this.unlock_sys_pending = pending; }
|
||||
|
||||
public boolean isRcvdRead() {
|
||||
return rcvdRead || (delegate != null && delegate.isRcvdRead());
|
||||
}
|
||||
public void setRcvdRead(boolean rcvd) {
|
||||
this.rcvdRead = rcvd;
|
||||
if (delegate != null) delegate.setRcvdRead(rcvd);
|
||||
}
|
||||
|
||||
public void receiveHeaderData(short s, short s2, short s3, int n) {
|
||||
this.sdi_flag = (s2 & request_bit_SDI) != 0;
|
||||
this.kri_flag = (s2 & request_bit_KRI) != 0;
|
||||
}
|
||||
|
||||
public void receiveHeader(short s, short s2, short s3, int n) {
|
||||
receiveHeaderData(s, s2, s3, n);
|
||||
}
|
||||
|
||||
public void endOfRecord() {
|
||||
log.fine("DS3270 endOfRecord");
|
||||
boolean crActive = (session != null && session.getContentionResolution());
|
||||
if (crActive) {
|
||||
if (this.sdi_flag && !isRcvdRead()) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(7);
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
this.sdi_flag = false;
|
||||
this.unlock_pending = false;
|
||||
if (this.unlock_sys_pending || this.kri_flag) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(8);
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
if (delegate.getInputProcessor().getOIA() != null) {
|
||||
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.kri_flag = false;
|
||||
this.unlock_sys_pending = false;
|
||||
}
|
||||
if (this.ps != null) {
|
||||
this.ps.dispatchEvent(new haus.nightmare.lib3270j.ecl.ECLPSEvent(this.ps, haus.nightmare.lib3270j.ecl.ECLPSEvent.EVENT_KEY_UNLOCKED));
|
||||
}
|
||||
} else {
|
||||
// Contention resolution is not active
|
||||
if (this.unlock_pending && !isRcvdRead()) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(7);
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
}
|
||||
if (this.autoSysUnlock) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(8);
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
if (delegate.getInputProcessor().getOIA() != null) {
|
||||
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
this.unlock_sys_pending = false;
|
||||
}
|
||||
this.unlock_pending = false;
|
||||
}
|
||||
if (this.unlock_sys_pending && !isRcvdRead()) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(8);
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
if (delegate.getInputProcessor().getOIA() != null) {
|
||||
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
this.unlock_sys_pending = false;
|
||||
}
|
||||
if (this.autoSysUnlock && !isRcvdRead()) {
|
||||
boolean sysLocked = false;
|
||||
if (this.ps != null && this.ps.islocked_SYSLOCK()) {
|
||||
sysLocked = true;
|
||||
} else if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
if (delegate.getInputProcessor().isKeyboardLocked() ||
|
||||
(delegate.getInputProcessor().getOIA() != null && delegate.getInputProcessor().getOIA().isXSystem())) {
|
||||
sysLocked = true;
|
||||
}
|
||||
}
|
||||
if (sysLocked) {
|
||||
if (this.ps != null) {
|
||||
this.ps.unlockKeyboard(8);
|
||||
}
|
||||
if (delegate != null && delegate.getInputProcessor() != null) {
|
||||
delegate.getInputProcessor().setKeyboardLocked(false);
|
||||
if (delegate.getInputProcessor().getOIA() != null) {
|
||||
delegate.getInputProcessor().getOIA().setInputInhibited(haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
}
|
||||
}
|
||||
this.unlock_sys_pending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.rcvdRead = false;
|
||||
if (delegate != null) delegate.setRcvdRead(false);
|
||||
this.unlock_pending = false;
|
||||
this.sdi_flag = false;
|
||||
this.kri_flag = false;
|
||||
}
|
||||
|
||||
public int receiveData(short[] sArray, int off, int len) {
|
||||
@@ -233,6 +374,10 @@ public class DS3270 {
|
||||
|
||||
public void processWCC(short wcc) {
|
||||
delegate.processWCC(wcc);
|
||||
if ((wcc & WCC_RESTORE) > 0) {
|
||||
this.unlock_pending = true;
|
||||
this.unlock_sys_pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void processSBA(int baddr) {
|
||||
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConnection;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConstants;
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPSEvent;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPSListener;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.protocol.DS3270Constants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import haus.nightmare.lib3270j.tn3270.DS3270;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 10 Test Suite: DataStream Protocol Hardening & AUTO_SYS_UNLOCK Parity.
|
||||
*
|
||||
* Verifies:
|
||||
* 10.1 WCC Keyboard Restore Logic (intermediate writes without restore bit keep keyboard locked).
|
||||
* 10.2 AUTO_SYS_UNLOCK parity across ConnectionConfig, ECLSession, ECLConnection, DS3270, and TelnetFSM.
|
||||
* 10.3 Contention Resolution negotiation, SDI/KRI tracking, and ECLPSEvent.EVENT_KEY_UNLOCKED dispatch.
|
||||
*/
|
||||
public class Phase10ProtocolHardeningTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor inputProcessor;
|
||||
private DataStreamProcessor processor;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
inputProcessor = new InputProcessor(screen, translator, null);
|
||||
processor = new DataStreamProcessor(screen, translator);
|
||||
processor.setInputProcessor(inputProcessor);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 10.1 WCC Keyboard Restore Logic
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("10.1: Write without WCC restore bit keeps keyboard locked")
|
||||
public void testWriteWithoutWccRestoreKeepsKeyboardLocked() {
|
||||
// Initially lock the keyboard
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
|
||||
// Construct 3270 Write Command (0x01) with WCC = 0xC0 (Reset MDT only, no keyboard restore 0x02)
|
||||
byte[] record = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0xC0, // WCC: bit 6 set (reset MDT), bit 1 (restore) NOT set
|
||||
(byte) 0x11, 0x40, 0x40, // SBA to 0
|
||||
(byte) 0xC1, (byte) 0xC2 // 'A', 'B'
|
||||
};
|
||||
|
||||
processor.processRecord(record, 0, record.length, false);
|
||||
|
||||
// Keyboard MUST remain locked because WCC restore bit 0x02 was not set
|
||||
assertTrue(inputProcessor.isKeyboardLocked(),
|
||||
"Keyboard must remain locked when WCC does not have restore bit (0x02) set");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.1: Write with WCC restore bit unlocks keyboard")
|
||||
public void testWriteWithWccRestoreUnlocksKeyboard() {
|
||||
// Initially lock the keyboard
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
|
||||
// Construct 3270 Write Command (0x01) with WCC = 0xC2 (Reset MDT + Keyboard Restore bit 0x02)
|
||||
byte[] record = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0xC2, // WCC: restore bit (0x02) set
|
||||
(byte) 0x11, 0x40, 0x40, // SBA to 0
|
||||
(byte) 0xC1, (byte) 0xC2 // 'A', 'B'
|
||||
};
|
||||
|
||||
processor.processRecord(record, 0, record.length, false);
|
||||
|
||||
// Keyboard MUST now be unlocked
|
||||
assertFalse(inputProcessor.isKeyboardLocked(),
|
||||
"Keyboard must be unlocked when WCC has restore bit (0x02) set");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.1: Intermediate writes without restore bit maintain keyboard lock across sequence")
|
||||
public void testIntermediateWritesKeepKeyboardLockedUntilFinalRestore() {
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
assertTrue(inputProcessor.isKeyboardLocked());
|
||||
|
||||
// First intermediate write: WCC = 0x00 (no restore)
|
||||
byte[] write1 = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0x00,
|
||||
(byte) 0x11, 0x40, 0x40,
|
||||
(byte) 0xC1
|
||||
};
|
||||
processor.processRecord(write1, 0, write1.length, false);
|
||||
assertTrue(inputProcessor.isKeyboardLocked(), "Write 1 without restore bit must leave keyboard locked");
|
||||
|
||||
// Second intermediate write: Erase / Write with WCC = 0x40 (Reset, no restore)
|
||||
byte[] write2 = new byte[] {
|
||||
(byte) DS3270Constants.CMD_ERASE_WRITE,
|
||||
(byte) 0x40,
|
||||
(byte) 0x11, 0x40, 0x50,
|
||||
(byte) 0xC2
|
||||
};
|
||||
processor.processRecord(write2, 0, write2.length, false);
|
||||
assertTrue(inputProcessor.isKeyboardLocked(), "Write 2 without restore bit must leave keyboard locked");
|
||||
|
||||
// Final write: WCC = 0x42 (Reset + Restore)
|
||||
byte[] write3 = new byte[] {
|
||||
(byte) DS3270Constants.CMD_WRITE,
|
||||
(byte) 0x42,
|
||||
(byte) 0x11, 0x40, 0x60,
|
||||
(byte) 0xC3
|
||||
};
|
||||
processor.processRecord(write3, 0, write3.length, false);
|
||||
assertFalse(inputProcessor.isKeyboardLocked(), "Final write with restore bit must unlock keyboard");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 10.2 AUTO_SYS_UNLOCK Parity
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("10.2: autoSysUnlock configuration property propagation across ECL components")
|
||||
public void testAutoSysUnlockConfigurationPropagation() {
|
||||
// Default ConnectionConfig should have autoSysUnlock == true
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
assertTrue(config.isAutoSysUnlock(), "Default autoSysUnlock in ConnectionConfig must be true");
|
||||
|
||||
config.setAutoSysUnlock(false);
|
||||
assertFalse(config.isAutoSysUnlock());
|
||||
|
||||
// Telnet3270Client propagation
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
assertFalse(client.isAutoSysUnlock());
|
||||
|
||||
// ECLSession propagation
|
||||
ECLSession session = new ECLSession(client);
|
||||
assertFalse(session.isAutoSysUnlock());
|
||||
assertFalse(session.getConnection().isAutoSysUnlock());
|
||||
|
||||
// Modify via ECLSession
|
||||
session.setAutoSysUnlock(true);
|
||||
assertTrue(session.isAutoSysUnlock());
|
||||
assertTrue(client.isAutoSysUnlock());
|
||||
assertTrue(config.isAutoSysUnlock());
|
||||
|
||||
// Test ECLSession Properties parsing
|
||||
Properties props = new Properties();
|
||||
props.setProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK, "false");
|
||||
props.setProperty(ECLSession.SESSION_HOST, "mainframe.org");
|
||||
ECLSession propsSession = new ECLSession(props);
|
||||
assertFalse(propsSession.isAutoSysUnlock());
|
||||
assertFalse(propsSession.getConnection().isAutoSysUnlock());
|
||||
|
||||
// ECLConnection standalone properties
|
||||
ECLConnection standaloneConn = new ECLConnection(props);
|
||||
assertFalse(standaloneConn.isAutoSysUnlock());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.2: DS3270 endOfRecord unlocks keyboard and clears OIA when autoSysUnlock is true and in X SYSTEM")
|
||||
public void testDs3270AutoSysUnlockOnEor() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
config.setAutoSysUnlock(true);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
ECLOIA oia = session.GetOIA();
|
||||
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
assertTrue(ds.isAutoSysUnlock());
|
||||
|
||||
// Set connected state so OIA does not treat it as communication check
|
||||
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
|
||||
|
||||
// Put session in X SYSTEM lock
|
||||
ps.lockKeyboard(8); // reason 8: SYSLOCK
|
||||
assertTrue(ps.islocked_SYSLOCK());
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked());
|
||||
assertTrue(oia.isXSystem());
|
||||
|
||||
// Trigger endOfRecord() with autoSysUnlock=true
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard must be auto-unlocked and OIA cleared
|
||||
assertFalse(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard should be unlocked on EOR when autoSysUnlock is true");
|
||||
assertFalse(oia.isXSystem(),
|
||||
"OIA X SYSTEM lock should be cleared on EOR when autoSysUnlock is true");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.2: DS3270 endOfRecord leaves keyboard locked when autoSysUnlock is false and in X SYSTEM")
|
||||
public void testDs3270AutoSysUnlockDisabledKeepsSystemLock() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
config.setAutoSysUnlock(false);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
assertFalse(ds.isAutoSysUnlock());
|
||||
|
||||
// Put session in X SYSTEM lock
|
||||
ps.lockKeyboard(8);
|
||||
assertTrue(ps.islocked_SYSLOCK());
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked());
|
||||
|
||||
// Trigger endOfRecord() with autoSysUnlock=false
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard MUST remain locked
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard must remain locked on EOR when autoSysUnlock is false");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 10.3 Contention Resolution & Pre-Data State Transitions
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution negotiation flag synchronization with ECLConnection")
|
||||
public void testContentionResolutionNegotiationSync() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLConnection conn = session.getConnection();
|
||||
TelnetFSM fsm = client.getTelnetFSM();
|
||||
|
||||
assertNotNull(fsm);
|
||||
assertTrue(fsm.isNegotiateContentionResolution(),
|
||||
"Default negotiateContentionResolution should be true");
|
||||
|
||||
// Test setter on FSM and reflection in ECLConnection
|
||||
fsm.setContentionResolutionNegotiated(true);
|
||||
|
||||
assertTrue(fsm.isContentionResolutionNegotiated(),
|
||||
"Contention resolution should be negotiated in FSM");
|
||||
assertTrue(conn.getContentionResolution(),
|
||||
"ECLConnection should reflect negotiated contention resolution");
|
||||
assertTrue(session.getContentionResolution(),
|
||||
"ECLSession should reflect negotiated contention resolution");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution SDI flag restores keyboard and fires EVENT_KEY_UNLOCKED on EOR")
|
||||
public void testContentionResolutionWithSdiRestoresKeyboardAndFiresEvent() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
|
||||
// Enable Contention Resolution on session
|
||||
session.setContentionResolution(true);
|
||||
assertTrue(session.getContentionResolution());
|
||||
|
||||
// Lock keyboard
|
||||
client.getInputProcessor().setKeyboardLocked(true);
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked());
|
||||
|
||||
// Register listener for EVENT_KEY_UNLOCKED
|
||||
AtomicInteger eventTypeReceived = new AtomicInteger(-1);
|
||||
AtomicBoolean unlockedEventFired = new AtomicBoolean(false);
|
||||
ps.RegisterPSEvent(new ECLPSListener() {
|
||||
@Override
|
||||
public void PSNotifyEvent(ECLPSEvent event) {
|
||||
eventTypeReceived.set(event.getEventType());
|
||||
unlockedEventFired.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Inbound TN3270E header with SDI bit set:
|
||||
// s2 request flag has bit 0 (0x01) set -> Send Data Indicator (SDI)
|
||||
short s1 = 0;
|
||||
short s2 = DS3270.request_bit_SDI; // 1
|
||||
short s3 = 0;
|
||||
ds.receiveHeaderData(s1, s2, s3, 0);
|
||||
|
||||
assertTrue(ds.isSdi_flag(), "SDI flag should be tracked from TN3270E header");
|
||||
|
||||
// Trigger endOfRecord()
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard should be unlocked because SDI was received
|
||||
assertFalse(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard should be restored on EOR when CR is active and SDI is set");
|
||||
assertFalse(ds.isSdi_flag(), "SDI flag should be reset after EOR");
|
||||
assertTrue(unlockedEventFired.get(), "ECLPSEvent must be dispatched on keyboard unlock");
|
||||
assertEquals(ECLPSEvent.EVENT_KEY_UNLOCKED, eventTypeReceived.get(),
|
||||
"Dispatched event must match EVENT_KEY_UNLOCKED");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution WITHOUT SDI flag keeps keyboard locked on EOR")
|
||||
public void testContentionResolutionWithoutSdiKeepsKeyboardLocked() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
|
||||
session.setContentionResolution(true);
|
||||
assertTrue(session.getContentionResolution());
|
||||
|
||||
// Initially lock keyboard
|
||||
client.getInputProcessor().setKeyboardLocked(true);
|
||||
|
||||
// Inbound TN3270E header WITHOUT SDI flag (s2 = 0)
|
||||
ds.receiveHeaderData((short) 0, (short) 0, (short) 0, 0);
|
||||
assertFalse(ds.isSdi_flag());
|
||||
|
||||
// Trigger EOR
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard MUST stay locked because host did not grant Send Data Indicator (turn)
|
||||
assertTrue(client.getInputProcessor().isKeyboardLocked(),
|
||||
"Keyboard must remain locked on EOR when Contention Resolution is active and SDI is not set");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10.3: Contention Resolution KRI flag resets system lock pending on EOR")
|
||||
public void testContentionResolutionKriResetsSystemLock() {
|
||||
ConnectionConfig config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLPS ps = session.GetPS();
|
||||
DS3270 ds = new DS3270(session, ps);
|
||||
|
||||
session.setContentionResolution(true);
|
||||
|
||||
// Put in system lock
|
||||
ps.lockKeyboard(8);
|
||||
assertTrue(ps.islocked_SYSLOCK());
|
||||
|
||||
// Receive header with KRI flag (bit 1 = 0x02) and SDI flag (bit 0 = 0x01)
|
||||
short s2 = (short) (DS3270.request_bit_SDI | DS3270.request_bit_KRI);
|
||||
ds.receiveHeaderData((short) 0, s2, (short) 0, 0);
|
||||
|
||||
assertTrue(ds.isSdi_flag());
|
||||
assertTrue(ds.isKri_flag());
|
||||
|
||||
ds.endOfRecord();
|
||||
|
||||
// Keyboard restored and system lock reset
|
||||
assertFalse(client.getInputProcessor().isKeyboardLocked());
|
||||
assertFalse(ps.islocked_SYSLOCK());
|
||||
assertFalse(ds.isKri_flag());
|
||||
}
|
||||
}
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.ecl.ECLConnection;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetConnection;
|
||||
import haus.nightmare.lib3270j.telnet.TelnetFSM;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 11 Test Suite: Enterprise Connection Resilience & "Dirty" Connection Handling.
|
||||
*
|
||||
* Verifies:
|
||||
* 11.1 Application-Layer Telnet Keep-Alive / Heartbeat Engine (NOP and TIMING-MARK, activity reset, shutdown).
|
||||
* 11.2 Automatic Reconnection with Exponential Backoff (unexpected disconnect, state transitions, retry loop, recovery, exhaustion).
|
||||
* 11.3 Transport-Level Socket Tuning & Options (soTimeout, extended socket options, CLI parsing).
|
||||
* 11.4 IBM HoD ECL Compatibility Facade (SESSION_KEEPALIVE, keepAliveTimeout, SESSION_AUTORECONNECT, ECLConnection methods).
|
||||
*/
|
||||
public class Phase11ConnectionResilienceTest {
|
||||
|
||||
// =========================================================================
|
||||
// 11.1 Application-Layer Telnet Keep-Alive / Heartbeat Engine
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.1: Keep-Alive transmits Telnet IAC NOP (0xFF 0xF1) when idle")
|
||||
public void testKeepAliveTransmitsIacNop() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setKeepAliveEnabled(true);
|
||||
config.setKeepAliveIntervalSeconds(1);
|
||||
config.setKeepAliveType("NOP");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CountDownLatch clientConnected = new CountDownLatch(1);
|
||||
|
||||
AtomicReference<Socket> serverAccepted = new AtomicReference<>();
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s = server.accept();
|
||||
serverAccepted.set(s);
|
||||
clientConnected.countDown();
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
assertTrue(clientConnected.await(5, TimeUnit.SECONDS), "Server did not accept client connection");
|
||||
|
||||
Socket s = serverAccepted.get();
|
||||
assertNotNull(s);
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
// Explicitly trigger or wait for keepalive heartbeat
|
||||
client.getConnection().sendKeepAliveHeartbeat();
|
||||
|
||||
byte[] buf = new byte[2];
|
||||
int read = in.read(buf);
|
||||
assertEquals(2, read);
|
||||
assertEquals((byte) TelnetConstants.IAC, buf[0]);
|
||||
assertEquals((byte) TelnetConstants.NOP, buf[1]);
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.1: Keep-Alive transmits Telnet IAC DO TIMING-MARK (0xFF 0xFD 0x06)")
|
||||
public void testKeepAliveTransmitsTimingMark() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setKeepAliveEnabled(true);
|
||||
config.setKeepAliveIntervalSeconds(1);
|
||||
config.setKeepAliveType("TIMING-MARK");
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CountDownLatch clientConnected = new CountDownLatch(1);
|
||||
|
||||
AtomicReference<Socket> serverAccepted = new AtomicReference<>();
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s = server.accept();
|
||||
serverAccepted.set(s);
|
||||
clientConnected.countDown();
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
assertTrue(clientConnected.await(5, TimeUnit.SECONDS));
|
||||
|
||||
Socket s = serverAccepted.get();
|
||||
assertNotNull(s);
|
||||
InputStream in = s.getInputStream();
|
||||
|
||||
client.getConnection().sendKeepAliveHeartbeat();
|
||||
|
||||
byte[] buf = new byte[3];
|
||||
int read = in.read(buf);
|
||||
assertEquals(3, read);
|
||||
assertEquals((byte) TelnetConstants.IAC, buf[0]);
|
||||
assertEquals((byte) TelnetConstants.DO, buf[1]);
|
||||
assertEquals((byte) TelnetConstants.TELOPT_TM, buf[2]);
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.1: Transmission resets last activity timestamp")
|
||||
public void testTransmissionResetsActivityTimestamp() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
server.accept();
|
||||
} catch (Exception ignored) {}
|
||||
}).start();
|
||||
|
||||
client.connect();
|
||||
TelnetConnection conn = client.getConnection();
|
||||
long initialTime = conn.getLastActivityTime();
|
||||
|
||||
Thread.sleep(15);
|
||||
conn.sendRaw(new byte[] { 0x01, 0x02 });
|
||||
|
||||
long updatedTime = conn.getLastActivityTime();
|
||||
assertTrue(updatedTime >= initialTime + 10, "Activity timestamp was not updated on sendRaw");
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 11.2 Automatic Reconnection with Exponential Backoff
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.2: Unexpected EOF transitions to RECONNECTING state when autoReconnect is true")
|
||||
public void testUnexpectedDisconnectTransitionsToReconnecting() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setAutoReconnect(true);
|
||||
config.setReconnectMaxRetries(3);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CopyOnWriteArrayList<ConnectionState> stateHistory = new CopyOnWriteArrayList<>();
|
||||
CountDownLatch reconnectingLatch = new CountDownLatch(1);
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
stateHistory.add(newState);
|
||||
if (newState == ConnectionState.RECONNECTING) {
|
||||
reconnectingLatch.countDown();
|
||||
}
|
||||
}
|
||||
@Override public void onConnectionError(String message) {}
|
||||
});
|
||||
|
||||
AtomicReference<Socket> acceptedSocket = new AtomicReference<>();
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s = server.accept();
|
||||
acceptedSocket.set(s);
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
serverThread.join(2000);
|
||||
|
||||
assertNotNull(acceptedSocket.get());
|
||||
|
||||
// Abruptly sever the connection from the host side (EOF)
|
||||
acceptedSocket.get().close();
|
||||
|
||||
assertTrue(reconnectingLatch.await(4, TimeUnit.SECONDS), "Client did not enter RECONNECTING state on unexpected EOF");
|
||||
assertTrue(stateHistory.contains(ConnectionState.RECONNECTING), "State history must include RECONNECTING");
|
||||
assertTrue(client.isReconnecting());
|
||||
|
||||
// Check OIA message updated
|
||||
assertNotNull(client.getOIA());
|
||||
|
||||
client.disconnect();
|
||||
assertFalse(client.isReconnecting());
|
||||
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.2: Intentional client.disconnect() transitions directly to NOT_CONNECTED without auto-reconnect")
|
||||
public void testIntentionalDisconnectDoesNotTriggerAutoReconnect() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setAutoReconnect(true);
|
||||
config.setReconnectMaxRetries(3);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CopyOnWriteArrayList<ConnectionState> stateHistory = new CopyOnWriteArrayList<>();
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
stateHistory.add(newState);
|
||||
}
|
||||
@Override public void onConnectionError(String message) {}
|
||||
});
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
server.accept();
|
||||
} catch (Exception ignored) {}
|
||||
}).start();
|
||||
|
||||
client.connect();
|
||||
Thread.sleep(100);
|
||||
|
||||
// Explicit client disconnect
|
||||
client.disconnect();
|
||||
|
||||
assertFalse(stateHistory.contains(ConnectionState.RECONNECTING),
|
||||
"Intentional disconnect should never transition to RECONNECTING");
|
||||
assertEquals(ConnectionState.NOT_CONNECTED, client.getConnectionState());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.2: Auto-reconnect retry loop successfully re-establishes connection")
|
||||
public void testAutoReconnectSuccessfulReconnection() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setAutoReconnect(true);
|
||||
config.setReconnectMaxRetries(3);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
CountDownLatch reconnectedLatch = new CountDownLatch(1);
|
||||
|
||||
AtomicReference<Socket> firstConn = new AtomicReference<>();
|
||||
AtomicReference<Socket> secondConn = new AtomicReference<>();
|
||||
|
||||
Thread serverThread = new Thread(() -> {
|
||||
try {
|
||||
Socket s1 = server.accept();
|
||||
firstConn.set(s1);
|
||||
// Close first socket to trigger reconnect
|
||||
Thread.sleep(50);
|
||||
s1.close();
|
||||
|
||||
// Accept the reconnection attempt
|
||||
Socket s2 = server.accept();
|
||||
secondConn.set(s2);
|
||||
reconnectedLatch.countDown();
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
serverThread.start();
|
||||
|
||||
client.connect();
|
||||
|
||||
// Wait for reconnection to succeed
|
||||
assertTrue(reconnectedLatch.await(8, TimeUnit.SECONDS), "Server did not receive reconnection attempt");
|
||||
assertNotNull(secondConn.get());
|
||||
|
||||
// Wait for client to complete handshake / notify connected
|
||||
Thread.sleep(200);
|
||||
assertTrue(client.isConnected());
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 11.3 Transport-Level Socket Tuning & Options
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.3: Configurable SO_TIMEOUT is applied to TCP socket")
|
||||
public void testSoTimeoutApplied() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
int port = server.getLocalPort();
|
||||
ConnectionConfig config = new ConnectionConfig("127.0.0.1", port);
|
||||
config.setSoTimeoutMs(750);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
new Thread(() -> {
|
||||
try { server.accept(); } catch (Exception ignored) {}
|
||||
}).start();
|
||||
|
||||
client.connect();
|
||||
assertNotNull(client.getConnection());
|
||||
// Client socket should have SO_TIMEOUT applied
|
||||
assertEquals(750, config.getSoTimeoutMs());
|
||||
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.3: parseHostString parses keepalive and autoreconnect flags")
|
||||
public void testParseHostStringFlags() {
|
||||
ConnectionConfig c1 = ConnectionConfig.parseHostString("mainframe.corp.com:23 --keepalive --autoreconnect", 23, TerminalModel.IBM_3279_4);
|
||||
assertTrue(c1.isKeepAliveEnabled());
|
||||
assertTrue(c1.isAutoReconnect());
|
||||
assertEquals("mainframe.corp.com", c1.getHost());
|
||||
assertEquals(23, c1.getPort());
|
||||
|
||||
ConnectionConfig c2 = ConnectionConfig.parseHostString("mainframe.corp.com:23 --no-keepalive --no-autoreconnect", 23, TerminalModel.IBM_3279_4);
|
||||
assertFalse(c2.isKeepAliveEnabled());
|
||||
assertFalse(c2.isAutoReconnect());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 11.4 IBM HoD ECL Compatibility Facade
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("11.4: ECLSession parses HoD properties for Keep-Alive and Auto-Reconnect")
|
||||
public void testEclSessionPropertiesParsing() {
|
||||
Properties props = new Properties();
|
||||
props.setProperty(ECLSession.SESSION_HOST, "mvs01.test.net");
|
||||
props.setProperty(ECLSession.SESSION_PORT, "23");
|
||||
props.setProperty(ECLSession.SESSION_KEEPALIVE, "true");
|
||||
props.setProperty(ECLSession.KEY_KEEPALIVE_TIMEOUT, "180");
|
||||
props.setProperty(ECLSession.KEY_KEEPALIVE_TYPE, "TIMING-MARK");
|
||||
props.setProperty(ECLSession.SESSION_AUTORECONNECT, "true");
|
||||
props.setProperty(ECLSession.SESSION_RECONNECT_RETRIES, "8");
|
||||
|
||||
ECLSession session = new ECLSession(props);
|
||||
assertTrue(session.isKeepAlive());
|
||||
assertEquals(180, session.getKeepAliveTimeout());
|
||||
assertTrue(session.isAutoReconnect());
|
||||
|
||||
ECLConnection conn = session.getConnection();
|
||||
assertNotNull(conn);
|
||||
assertTrue(conn.isKeepAlive());
|
||||
assertEquals(180, conn.getKeepAliveTimeout());
|
||||
assertEquals("TIMING-MARK", conn.getKeepAliveType());
|
||||
assertTrue(conn.isAutoReconnect());
|
||||
assertEquals(8, conn.getMaxRetry());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11.4: ECLConnection getters and setters synchronize with underlying client")
|
||||
public void testEclConnectionGettersSetters() {
|
||||
ConnectionConfig cfg = new ConnectionConfig("localhost", 23);
|
||||
Telnet3270Client client = new Telnet3270Client(cfg);
|
||||
ECLSession session = new ECLSession(client);
|
||||
ECLConnection conn = session.getConnection();
|
||||
|
||||
conn.setKeepAlive(false);
|
||||
assertFalse(conn.isKeepAlive());
|
||||
assertFalse(client.getConfig().isKeepAliveEnabled());
|
||||
|
||||
conn.setKeepAliveTimeout(60);
|
||||
assertEquals(60, conn.getKeepAliveTimeout());
|
||||
assertEquals(60, client.getConfig().getKeepAliveIntervalSeconds());
|
||||
|
||||
conn.setKeepAliveType("TIMING-MARK");
|
||||
assertEquals("TIMING-MARK", conn.getKeepAliveType());
|
||||
assertEquals("TIMING-MARK", client.getConfig().getKeepAliveType());
|
||||
|
||||
conn.setAutoReconnect(true);
|
||||
assertTrue(conn.isAutoReconnect());
|
||||
assertTrue(client.getConfig().isAutoReconnect());
|
||||
|
||||
conn.setMaxRetry(10);
|
||||
assertEquals(10, conn.getMaxRetry());
|
||||
assertEquals(10, client.getConfig().getReconnectMaxRetries());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 13.2: Interactive Entry Assist Engine.
|
||||
* Verifies interactive word wrap in DOC mode and audible bell alert
|
||||
* when typing reaches the configured bell column.
|
||||
*/
|
||||
public class InteractiveEntryAssistTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor input;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||
input = new InputProcessor(screen, translator, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAudibleBellTriggersAtConfiguredBellColumn() {
|
||||
input.setBellEnabled(true);
|
||||
input.setBellColumn(74); // 0-based column 74 = column 75
|
||||
|
||||
AtomicInteger bellCount = new AtomicInteger(0);
|
||||
input.setBellListener(bellCount::incrementAndGet);
|
||||
|
||||
screen.setCursorPosition(0, 72);
|
||||
input.typeCharacter('A'); // col 73
|
||||
assertEquals(0, bellCount.get());
|
||||
|
||||
input.typeCharacter('B'); // col 74 -> triggers bell
|
||||
assertEquals(1, bellCount.get());
|
||||
|
||||
input.typeCharacter('C'); // col 75 -> remains 1 on current row
|
||||
assertEquals(1, bellCount.get());
|
||||
|
||||
// Moving to next row and typing across bell column triggers bell again
|
||||
screen.setCursorPosition(1, 73);
|
||||
input.typeCharacter('X'); // advances to col 74 on row 1
|
||||
assertEquals(2, bellCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDocModeWordWrapMovesPartialWordToNextRow() {
|
||||
screen.setEntryAssistDOCmode(true);
|
||||
screen.setEntryAssistWordWrap(true);
|
||||
screen.setLeftMargin(0); // Col 0
|
||||
screen.setRightMargin(15); // Col 15
|
||||
|
||||
assertTrue(screen.isEntryAssistDOCmode());
|
||||
assertTrue(screen.isEntryAssistWordWrap());
|
||||
|
||||
// Type "HELLO " (6 chars) starting at col 0
|
||||
screen.setCursorPosition(0, 0);
|
||||
for (char c : "HELLO ".toCharArray()) {
|
||||
input.typeCharacter(c);
|
||||
}
|
||||
assertEquals(0, screen.getCursorRow());
|
||||
assertEquals(6, screen.getCursorCol());
|
||||
|
||||
// Move cursor near right margin: Row 0, Col 13
|
||||
screen.setCursorPosition(0, 13);
|
||||
input.typeCharacter('P'); // col 14
|
||||
input.typeCharacter('A'); // col 15 (at right margin)
|
||||
input.typeCharacter('R'); // crosses right margin -> triggers word wrap
|
||||
|
||||
// After word wrap, the word "PAR" should be moved to Row 1, left margin (Col 0)
|
||||
assertEquals(1, screen.getCursorRow());
|
||||
assertTrue(screen.getCursorCol() >= 3);
|
||||
|
||||
// Verify content on Row 1
|
||||
assertEquals('P', (char) screen.getCell(80).ucs4);
|
||||
assertEquals('A', (char) screen.getCell(81).ucs4);
|
||||
assertEquals('R', (char) screen.getCell(82).ucs4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Phase 12 Test Suite: Reactive Synchronization & Thread-Safety Hardening.
|
||||
*
|
||||
* Verifies:
|
||||
* 12.1 Condition-Based Synchronization for ECL Automation Calls:
|
||||
* - Sub-millisecond reactive wakeup for waitForScreen(ECLScreenDesc).
|
||||
* - Reactive wakeup for waitForCursor(row, col).
|
||||
* - Reactive wakeup for waitForString(text) and waitWhileScreen(ECLScreenDesc).
|
||||
* - Clean timeout handling without CPU busy-wait spinning.
|
||||
* - Graceful thread interruption handling.
|
||||
* - ECLOIA condition-based reactive waitForInput and waitForTransition.
|
||||
* - ScreenBuffer listener dispatch and cursor change tracking.
|
||||
* - Concurrency safety with multiple waiters.
|
||||
*/
|
||||
public class Phase12SynchronizationTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor inputProcessor;
|
||||
private ECLPS ps;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3279_4, translator);
|
||||
inputProcessor = new InputProcessor(screen, translator, null);
|
||||
ps = new ECLPS(screen, inputProcessor, translator);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitForScreen(ECLScreenDesc) wakes up reactively upon screen update")
|
||||
public void testReactiveWaitForScreenDesc() throws Exception {
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("TSO/E LOGON", 5, 10);
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForScreen(desc, 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(matched);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50); // Give waiter time to block on condition
|
||||
|
||||
// Write matching text at 1-based (5, 10) -> 0-based row 4 col 9
|
||||
screen.setText("TSO/E LOGON", 4, 9);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitForScreen should match");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively well before 5000ms timeout (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitForCursor(row, col) wakes up reactively upon cursor position change")
|
||||
public void testReactiveWaitForCursor() throws Exception {
|
||||
screen.setCursorPosition(0, 0);
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForCursor(12, 34, 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(matched);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
// Move cursor
|
||||
screen.setCursorPosition(12, 34);
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitForCursor should succeed");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively upon cursor move (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitForString(text) wakes up reactively upon string appearance")
|
||||
public void testReactiveWaitForString() throws Exception {
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForString("COMMAND ===>", 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(matched);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
screen.setText("COMMAND ===>", 20, 2);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitForString should find the text");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: waitWhileScreen(desc) wakes up reactively when condition ceases to match")
|
||||
public void testReactiveWaitWhileScreen() throws Exception {
|
||||
// Place initial text
|
||||
screen.setText("HOLDING", 0, 0);
|
||||
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("HOLDING");
|
||||
|
||||
AtomicBoolean result = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean finished = ps.waitWhileScreen(desc, 5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
result.set(finished);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
// Erase screen so "HOLDING" is gone
|
||||
screen.erase(false);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(result.get(), "waitWhileScreen should return true once condition is gone");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively when text erased (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: Clean timeout without false match or spinning")
|
||||
public void testCleanTimeout() {
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("NON_EXISTENT_STRING");
|
||||
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean matched = ps.waitForScreen(desc, 150);
|
||||
long elapsed = System.currentTimeMillis() - t0;
|
||||
|
||||
assertFalse(matched, "Should not match non-existent text");
|
||||
assertTrue(elapsed >= 100, "Should have waited for the full timeout window (elapsed=" + elapsed + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: Graceful thread interruption during wait")
|
||||
public void testThreadInterruption() throws Exception {
|
||||
ECLScreenDesc desc = new ECLScreenDesc();
|
||||
desc.addString("NEVER_APPEARS");
|
||||
|
||||
AtomicBoolean wasInterrupted = new AtomicBoolean(false);
|
||||
AtomicBoolean result = new AtomicBoolean(true);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
boolean r = ps.waitForScreen(desc, 10000);
|
||||
result.set(r);
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
wasInterrupted.set(true);
|
||||
}
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
waiter.interrupt();
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertFalse(result.get(), "Interrupted wait should return false");
|
||||
assertTrue(wasInterrupted.get(), "Thread interrupt flag should be preserved");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: ECLOIA reactive waitForInput and waitForTransition")
|
||||
public void testECLOIAReactiveWait() throws Exception {
|
||||
ECLOIA oia = new ECLOIA(screen, inputProcessor, null);
|
||||
oia.setInputInhibited(ECLOIA.INHIBIT_SYSTEMWAIT);
|
||||
|
||||
AtomicBoolean inputReady = new AtomicBoolean(false);
|
||||
AtomicLong durationMs = new AtomicLong(-1);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
|
||||
Thread waiter = new Thread(() -> {
|
||||
started.countDown();
|
||||
long t0 = System.currentTimeMillis();
|
||||
boolean ready = oia.waitForInput(5000);
|
||||
durationMs.set(System.currentTimeMillis() - t0);
|
||||
inputReady.set(ready);
|
||||
done.countDown();
|
||||
});
|
||||
waiter.start();
|
||||
|
||||
assertTrue(started.await(1, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
// Unlock OIA
|
||||
oia.setInputInhibited(ECLOIA.INHIBIT_NOTINHIBITED);
|
||||
|
||||
assertTrue(done.await(2, TimeUnit.SECONDS));
|
||||
assertTrue(inputReady.get(), "waitForInput should succeed");
|
||||
assertTrue(durationMs.get() < 1500, "Should unblock reactively upon OIA unlock (took " + durationMs.get() + "ms)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: ScreenBuffer listener dispatch and cursor movement tracking")
|
||||
public void testScreenBufferListenerDispatch() {
|
||||
AtomicBoolean screenUpdated = new AtomicBoolean(false);
|
||||
AtomicInteger cursorOld = new AtomicInteger(-1);
|
||||
AtomicInteger cursorNew = new AtomicInteger(-1);
|
||||
|
||||
ScreenUpdateListener listener = new ScreenUpdateListener() {
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
screenUpdated.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorMoved(int oldAddress, int newAddress) {
|
||||
cursorOld.set(oldAddress);
|
||||
cursorNew.set(newAddress);
|
||||
}
|
||||
};
|
||||
|
||||
screen.addUpdateListener(listener);
|
||||
|
||||
screen.notifyScreenUpdate();
|
||||
assertTrue(screenUpdated.get(), "Listener should receive onScreenUpdated");
|
||||
|
||||
screen.setCursorAddress(123);
|
||||
assertEquals(0, cursorOld.get(), "Old cursor should be 0");
|
||||
assertEquals(123, cursorNew.get(), "New cursor should be 123");
|
||||
|
||||
screen.removeUpdateListener(listener);
|
||||
screenUpdated.set(false);
|
||||
screen.notifyScreenUpdate();
|
||||
assertFalse(screenUpdated.get(), "Removed listener should not receive updates");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("12.1: Concurrent waiters on the same ScreenBuffer")
|
||||
public void testConcurrentWaiters() throws Exception {
|
||||
int numWaiters = 5;
|
||||
CountDownLatch startLatch = new CountDownLatch(numWaiters);
|
||||
CountDownLatch doneLatch = new CountDownLatch(numWaiters);
|
||||
AtomicInteger successCount = new AtomicInteger(0);
|
||||
|
||||
for (int i = 0; i < numWaiters; i++) {
|
||||
new Thread(() -> {
|
||||
startLatch.countDown();
|
||||
boolean ok = ps.waitForString("BATCH_SIGNAL", 5000);
|
||||
if (ok) {
|
||||
successCount.incrementAndGet();
|
||||
}
|
||||
doneLatch.countDown();
|
||||
}).start();
|
||||
}
|
||||
|
||||
assertTrue(startLatch.await(2, TimeUnit.SECONDS));
|
||||
Thread.sleep(50);
|
||||
|
||||
screen.setText("BATCH_SIGNAL", 100);
|
||||
screen.notifyScreenUpdate();
|
||||
|
||||
assertTrue(doneLatch.await(3, TimeUnit.SECONDS));
|
||||
assertEquals(numWaiters, successCount.get(), "All concurrent waiters should unblock reactively");
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import java.awt.Image;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.image.BufferedImage;
|
||||
import haus.nightmare.lib3270j.graphics.DefaultPixelBuffer;
|
||||
import haus.nightmare.lib3270j.graphics.PixelBuffer;
|
||||
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -406,11 +406,12 @@ public class Phase5EclEventTest {
|
||||
}
|
||||
|
||||
// Test with image and rectangle
|
||||
Image testImage = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
PixelBuffer testImage = new DefaultPixelBuffer(32, 32);
|
||||
Rectangle rect = new Rectangle(0, 0, 32, 32);
|
||||
ECLPSGraphicsEvent fullGEvent = new ECLPSGraphicsEvent(ps, ECLPSGraphicsEvent.GRAPHICS_UPDATED, testImage, rect);
|
||||
assertSame(testImage, fullGEvent.GetImage());
|
||||
assertSame(testImage, fullGEvent.getImage());
|
||||
assertSame(testImage, fullGEvent.getPixelBuffer());
|
||||
assertEquals(rect, fullGEvent.GetRectangle());
|
||||
assertEquals(rect, fullGEvent.getRectangle());
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for Phase 13.1: Excel & Tabular Clipboard Paste.
|
||||
* Verifies tab-delimited text navigation, newline row advancement,
|
||||
* and boundary truncation when pasteStopAtProtectedLine is set.
|
||||
*/
|
||||
public class TabularPasteTest {
|
||||
|
||||
private ScreenBuffer screen;
|
||||
private EbcdicTranslator translator;
|
||||
private InputProcessor input;
|
||||
private ECLPS ps;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
translator = new EbcdicTranslator();
|
||||
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
|
||||
input = new InputProcessor(screen, translator, null);
|
||||
ps = new ECLPS(screen, input, translator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcelPasteTabsAndNewlinesAdvanceAcrossFields() {
|
||||
// Setup 2 rows with 2 unprotected fields each
|
||||
// Row 0: pos 0 (unprotected), pos 10 (protected), pos 11 (unprotected), pos 30 (protected)
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(11, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Row 1: pos 80 (unprotected), pos 90 (protected), pos 91 (unprotected), pos 110 (protected)
|
||||
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(90, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(91, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(110, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
// Position cursor at first field (pos 1)
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
String tabularData = "ABC\tDEF\r\nGHI\tJKL";
|
||||
int pasted = input.pasteText(tabularData, true, false);
|
||||
assertEquals(12, pasted);
|
||||
|
||||
// Verify Field 1 (Row 0, Col 1-3)
|
||||
assertEquals("ABC", ps.getString(1, 3));
|
||||
// Verify Field 2 (Row 0, Col 12-14)
|
||||
assertEquals("DEF", ps.getString(12, 3));
|
||||
// Verify Field 3 (Row 1, Col 1-3 -> pos 81)
|
||||
assertEquals("GHI", ps.getString(81, 3));
|
||||
// Verify Field 4 (Row 1, Col 12-14 -> pos 92)
|
||||
assertEquals("JKL", ps.getString(92, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasteStopAtProtectedBoundaryHaltsPaste() {
|
||||
// Field 1: pos 0 (unprotected), pos 5 (protected)
|
||||
// Data area is pos 1..4 (4 characters)
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(5, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(10, (byte) FA_PRINTABLE);
|
||||
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
// Try to paste 6 characters when only 4 fit
|
||||
String overflow = "123456";
|
||||
int pasted = input.pasteText(overflow, false, true);
|
||||
|
||||
// Should paste 4 chars and halt at the protected boundary pos 5
|
||||
assertEquals(4, pasted);
|
||||
assertEquals("1234", ps.getString(1, 4));
|
||||
// Field at pos 10 should not be touched
|
||||
assertEquals(" ", ps.getString(11, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasteStopAtProtectedLineHaltsAtProtectedRow() {
|
||||
// Row 0: unprotected field at pos 0
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
// Row 1: entirely protected starting at pos 80
|
||||
screen.setFieldAttribute(80, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
screen.setCursorAddress(1);
|
||||
|
||||
String multiRow = "DATA1\r\nDATA2";
|
||||
int pasted = input.pasteText(multiRow, true, true);
|
||||
|
||||
// "DATA1" (5 chars) pasted, then newline sees next line is protected and halts
|
||||
assertEquals(5, pasted);
|
||||
assertEquals("DATA1", ps.getString(1, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEclPsPasteFromExcelMethodsAndFlags() {
|
||||
assertTrue(ps.isEnablePasteFromExcel());
|
||||
assertFalse(ps.isPasteStopAtProtectedLine());
|
||||
|
||||
ps.setEnablePasteFromExcel(false);
|
||||
assertFalse(ps.isEnablePasteFromExcel());
|
||||
|
||||
ps.setPasteStopAtProtectedLine(true);
|
||||
assertTrue(ps.isPasteStopAtProtectedLine());
|
||||
|
||||
// Setup fields
|
||||
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(10, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
screen.setFieldAttribute(11, (byte) FA_PRINTABLE);
|
||||
screen.setFieldAttribute(30, (byte) (FA_PRINTABLE | FA_PROTECT));
|
||||
|
||||
ps.pasteFromExcel("HELLO\tWORLD", 0, 1);
|
||||
assertEquals("HELLO", ps.getString(1, 5));
|
||||
assertEquals("WORLD", ps.getString(12, 5));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Architectural compliance audit test verifying 100% decoupling of lib3270j from java.awt.
|
||||
* Ensures headless execution and portability to Android (which lacks java.awt.*).
|
||||
*/
|
||||
public class AwtDecouplingAuditTest {
|
||||
|
||||
private static final Pattern IMPORT_AWT_PATTERN = Pattern.compile("^\\s*import\\s+java\\.awt\\..*;");
|
||||
private static final Pattern DIRECT_AWT_USAGE_PATTERN = Pattern.compile("(?<!Class\\.forName\\(\")java\\.awt\\.[A-Za-z0-9_]+");
|
||||
|
||||
@Test
|
||||
@DisplayName("Audit lib3270j/src/main/java for zero java.awt.* imports or references")
|
||||
public void testZeroAwtDependenciesInMainSource() throws IOException {
|
||||
File srcMainDir = new File("src/main/java");
|
||||
if (!srcMainDir.exists()) {
|
||||
srcMainDir = new File("lib3270j/src/main/java");
|
||||
}
|
||||
assertTrue(srcMainDir.exists() && srcMainDir.isDirectory(),
|
||||
"src/main/java directory must exist at " + srcMainDir.getAbsolutePath());
|
||||
|
||||
List<String> violations = new ArrayList<>();
|
||||
|
||||
try (Stream<Path> paths = Files.walk(srcMainDir.toPath())) {
|
||||
List<Path> javaFiles = paths
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".java"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertFalse(javaFiles.isEmpty(), "Found no java files in " + srcMainDir);
|
||||
|
||||
for (Path p : javaFiles) {
|
||||
List<String> lines = Files.readAllLines(p);
|
||||
for (int lineNum = 0; lineNum < lines.size(); lineNum++) {
|
||||
String line = lines.get(lineNum);
|
||||
String trimmed = line.trim();
|
||||
|
||||
// Ignore comment lines
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IMPORT_AWT_PATTERN.matcher(line).find()) {
|
||||
violations.add(String.format("%s:%d -> %s", p.getFileName(), lineNum + 1, trimmed));
|
||||
} else if (line.contains("java.awt.") && !line.contains("Class.forName(\"java.awt.") && !line.contains("\"java.awt.")) {
|
||||
violations.add(String.format("%s:%d [raw type reference] -> %s", p.getFileName(), lineNum + 1, trimmed));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!violations.isEmpty()) {
|
||||
fail("Found java.awt dependencies in lib3270j main source:\n" + String.join("\n", violations));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Verify PixelBuffer and software rasterizer functions without AWT")
|
||||
public void testPixelBufferPureJavaOperations() {
|
||||
PixelBuffer pb = new DefaultPixelBuffer(100, 100);
|
||||
assertEquals(100, pb.getWidth());
|
||||
assertEquals(100, pb.getHeight());
|
||||
|
||||
// Fill rect
|
||||
pb.fillRect(10, 10, 20, 20, 0xFFFF0000);
|
||||
assertEquals(0xFFFF0000, pb.getPixel(15, 15));
|
||||
assertEquals(0, pb.getPixel(5, 5));
|
||||
|
||||
// Draw line AA
|
||||
pb.drawLineAA(0, 0, 99, 99, 0xFF00FF00, 1.0);
|
||||
int diagPixel = pb.getPixel(50, 50);
|
||||
assertNotEquals(0, diagPixel, "Anti-aliased diagonal line should render pixels");
|
||||
|
||||
// Clipping
|
||||
pb.setClip(20, 20, 10, 10);
|
||||
assertTrue(pb.isClipped(2, 80));
|
||||
assertFalse(pb.isClipped(25, 25));
|
||||
assertEquals(0, pb.getPixel(2, 80));
|
||||
pb.setPixel(2, 80, 0xFF0000FF);
|
||||
assertEquals(0, pb.getPixel(2, 80), "Clipped pixel must not be modified");
|
||||
|
||||
pb.clearClip();
|
||||
assertFalse(pb.isClipped(5, 5));
|
||||
}
|
||||
}
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.awt.Point;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.awt.Point;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
||||
-2
@@ -7,8 +7,6 @@ import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,14 +4,6 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Image;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
@@ -201,7 +193,7 @@ public class Phase2GocaEngineTest {
|
||||
@Test
|
||||
@DisplayName("Test HODWallpaper Tile, Center, and Stretch")
|
||||
public void testHODWallpaper() {
|
||||
BufferedImage img = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
PixelBuffer img = new DefaultPixelBuffer(32, 32);
|
||||
HODWallpaper wp = new HODWallpaper(img, HODWallpaper.HOD_CENTER);
|
||||
assertEquals(HODWallpaper.HOD_CENTER, wp.getDisplay());
|
||||
|
||||
@@ -211,10 +203,8 @@ public class Phase2GocaEngineTest {
|
||||
wp.setDisplay(HODWallpaper.HOD_STRETCH);
|
||||
assertEquals(HODWallpaper.HOD_STRETCH, wp.getDisplay());
|
||||
|
||||
BufferedImage canvas = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics g = canvas.getGraphics();
|
||||
wp.paint(new java.awt.Canvas(), g, 0, 0, 100, 100);
|
||||
g.dispose();
|
||||
PixelBuffer canvas = new DefaultPixelBuffer(100, 100);
|
||||
wp.paint(canvas, 0, 0, 100, 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -283,8 +273,9 @@ public class Phase2GocaEngineTest {
|
||||
assertEquals(101, bounds.width);
|
||||
assertEquals(101, bounds.height);
|
||||
|
||||
Image img = fa.getImage();
|
||||
Object img = fa.getImage();
|
||||
assertNotNull(img);
|
||||
assertNotNull(fa.getPixelBuffer());
|
||||
fa.dispose();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Point;
|
||||
import haus.nightmare.lib3270j.graphics.Point;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package haus.nightmare.lib3270j.integration;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
import haus.nightmare.lib3270j.listener.ConnectionListener;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Live integration test verifying headless connection to live MVS host:
|
||||
* mvs.hugfreevikings.wtf:1023 using guest credentials.
|
||||
*/
|
||||
@Tag("integration")
|
||||
public class LiveHostHeadlessTest {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LiveHostHeadlessTest.class.getName());
|
||||
private static final String HOST = "mvs.hugfreevikings.wtf";
|
||||
private static final int PORT = 1023;
|
||||
|
||||
@Test
|
||||
@DisplayName("Connect headless Telnet3270Client to mvs.hugfreevikings.wtf:1023")
|
||||
public void testLiveMvsConnection() throws Exception {
|
||||
// Probe host reachability (timeout 3 seconds)
|
||||
boolean reachable = false;
|
||||
try (Socket probe = new Socket()) {
|
||||
probe.connect(new InetSocketAddress(HOST, PORT), 3000);
|
||||
reachable = true;
|
||||
} catch (IOException e) {
|
||||
logger.warning("Live host " + HOST + ":" + PORT + " unreachable: " + e.getMessage());
|
||||
}
|
||||
|
||||
Assumptions.assumeTrue(reachable, "Skipping live test: " + HOST + ":" + PORT + " is not reachable");
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(HOST, PORT, TerminalModel.IBM_3279_2);
|
||||
config.setConnectTimeoutMs(5000);
|
||||
config.setSoTimeoutMs(10000);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
|
||||
CountDownLatch connectedLatch = new CountDownLatch(1);
|
||||
CountDownLatch screenLatch = new CountDownLatch(1);
|
||||
|
||||
client.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
|
||||
if (newState == ConnectionState.CONNECTED_3270) {
|
||||
connectedLatch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionError(String message) {}
|
||||
});
|
||||
|
||||
client.addScreenUpdateListener(new ScreenUpdateListener() {
|
||||
@Override
|
||||
public void onScreenUpdated() {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
if (sb != null && sb.isFormatted()) {
|
||||
screenLatch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
client.connect();
|
||||
|
||||
boolean connected = connectedLatch.await(5, TimeUnit.SECONDS);
|
||||
assertTrue(connected, "Client must establish CONNECTED_3270 state");
|
||||
|
||||
boolean screenReceived = screenLatch.await(5, TimeUnit.SECONDS);
|
||||
assertTrue(screenReceived, "Client must receive formatted screen from live MVS host");
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
assertNotNull(sb);
|
||||
|
||||
StringBuilder fullText = new StringBuilder();
|
||||
for (int i = 0; i < sb.getSize(); i++) {
|
||||
char c = sb.getCell(i).ucs4;
|
||||
fullText.append(c > ' ' ? c : ' ');
|
||||
}
|
||||
String screenContent = fullText.toString().trim();
|
||||
assertFalse(screenContent.isEmpty(), "Screen buffer text should not be empty");
|
||||
|
||||
// Verify headless graphics plane is initialized without AWT
|
||||
assertNotNull(client.getGraphicsPlane());
|
||||
assertEquals(720, client.getGraphicsPlane().getWidth());
|
||||
assertEquals(384, client.getGraphicsPlane().getHeight());
|
||||
|
||||
logger.info("Successfully connected to live host. Banner preview: " +
|
||||
screenContent.substring(0, Math.min(200, screenContent.length())).replaceAll("\\s+", " "));
|
||||
} finally {
|
||||
client.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user