Clear cruft
This commit is contained in:
@@ -70,15 +70,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
setLocationRelativeTo(null);
|
setLocationRelativeTo(null);
|
||||||
setMinimumSize(new Dimension(640, 400));
|
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 -> {
|
refreshTimer = new Timer(100, e -> {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
statusBar.updateStatus();
|
statusBar.updateStatus();
|
||||||
syncModeMenuItems();
|
syncModeMenuItems();
|
||||||
if (isActive() && !terminalPanel.hasFocus()) {
|
if (!isAnyChildDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) {
|
||||||
|
if (!terminalPanel.hasFocus()) {
|
||||||
terminalPanel.requestFocusInWindow();
|
terminalPanel.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
refreshTimer.start();
|
refreshTimer.start();
|
||||||
|
|
||||||
@@ -96,11 +98,30 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void windowActivated(WindowEvent e) {
|
public void windowActivated(WindowEvent e) {
|
||||||
|
if (!isAnyChildDialogActive()) {
|
||||||
terminalPanel.requestFocusInWindow();
|
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() {
|
private void buildUI() {
|
||||||
terminalPanel = new TerminalPanel();
|
terminalPanel = new TerminalPanel();
|
||||||
statusBar = new StatusBar();
|
statusBar = new StatusBar();
|
||||||
@@ -743,13 +764,26 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
fileTransfer.cancel();
|
fileTransfer.cancel();
|
||||||
fileTransfer = null;
|
fileTransfer = null;
|
||||||
}
|
}
|
||||||
client.disconnect();
|
Telnet3270Client c = client;
|
||||||
client = null;
|
client = null;
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
c.disconnect();
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}, "Disconnect-Thread").start();
|
||||||
|
|
||||||
|
Runnable uiReset = () -> {
|
||||||
terminalPanel.setClient(null);
|
terminalPanel.setClient(null);
|
||||||
statusBar.setClient(null, terminalPanel);
|
statusBar.setClient(null, terminalPanel);
|
||||||
syncModeMenuItems();
|
syncModeMenuItems();
|
||||||
terminalPanel.repaint();
|
terminalPanel.repaint();
|
||||||
setTitle("j3270 — Java TN3270 Terminal Emulator");
|
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();
|
statusBar.updateStatus();
|
||||||
terminalPanel.repaint();
|
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.guardedPack();
|
||||||
terminalPanel.requestFocusInWindow();
|
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 ==========
|
// ========== Help dialogs ==========
|
||||||
|
|
||||||
private void showKeyMappings() {
|
private void showKeyMappings() {
|
||||||
@@ -949,6 +1005,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
boolean cliTls = false;
|
boolean cliTls = false;
|
||||||
boolean cliNoVerifyCert = false;
|
boolean cliNoVerifyCert = false;
|
||||||
Boolean cliTn3270e = null;
|
Boolean cliTn3270e = null;
|
||||||
|
Boolean cliAutoSysUnlock = null;
|
||||||
GraphicsMode cliGraphicsMode = null;
|
GraphicsMode cliGraphicsMode = null;
|
||||||
String configFile = null;
|
String configFile = null;
|
||||||
|
|
||||||
@@ -965,6 +1022,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
cliTn3270e = false;
|
cliTn3270e = false;
|
||||||
} else if ("--tn3270e".equals(arg)) {
|
} else if ("--tn3270e".equals(arg)) {
|
||||||
cliTn3270e = true;
|
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=")) {
|
} else if (arg.startsWith("--graphics=")) {
|
||||||
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
|
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
|
||||||
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
} 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 finalTls = cliTls;
|
||||||
final boolean finalNoVerify = cliNoVerifyCert;
|
final boolean finalNoVerify = cliNoVerifyCert;
|
||||||
final Boolean finalTn3270e = cliTn3270e;
|
final Boolean finalTn3270e = cliTn3270e;
|
||||||
|
final Boolean finalAutoSysUnlock = cliAutoSysUnlock;
|
||||||
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
|
||||||
|
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
@@ -1077,6 +1139,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
if (finalTn3270e != null) {
|
if (finalTn3270e != null) {
|
||||||
config.setTn3270eEnabled(finalTn3270e);
|
config.setTn3270eEnabled(finalTn3270e);
|
||||||
}
|
}
|
||||||
|
if (finalAutoSysUnlock != null) {
|
||||||
|
config.setAutoSysUnlock(finalAutoSysUnlock);
|
||||||
|
} else {
|
||||||
|
config.setAutoSysUnlock(haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
|
||||||
|
}
|
||||||
if (finalGraphicsMode != null) {
|
if (finalGraphicsMode != null) {
|
||||||
config.setGraphicsMode(finalGraphicsMode);
|
config.setGraphicsMode(finalGraphicsMode);
|
||||||
} else {
|
} else {
|
||||||
@@ -1100,6 +1167,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
|||||||
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
|
||||||
config.setTlsVerifyCert(verify);
|
config.setTlsVerifyCert(verify);
|
||||||
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
|
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
|
||||||
|
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
|
||||||
if (finalGraphicsMode != null) {
|
if (finalGraphicsMode != null) {
|
||||||
config.setGraphicsMode(finalGraphicsMode);
|
config.setGraphicsMode(finalGraphicsMode);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -105,6 +105,51 @@ public class Settings {
|
|||||||
prefs.putBoolean("autoConnectTn3270e", tn3270e);
|
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() {
|
public static haus.nightmare.lib3270j.graphics.GraphicsMode getGraphicsMode() {
|
||||||
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
|
String modeStr = prefs.get("graphicsMode", haus.nightmare.lib3270j.graphics.GraphicsMode.BOTH.name());
|
||||||
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
return haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(modeStr);
|
||||||
@@ -311,6 +356,26 @@ public class Settings {
|
|||||||
prefs.putBoolean("blockSelectMode", block);
|
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 ==========
|
// ========== Crosshair Ruler ==========
|
||||||
|
|
||||||
public static boolean getCrosshairRuler() {
|
public static boolean getCrosshairRuler() {
|
||||||
@@ -509,11 +574,42 @@ public class Settings {
|
|||||||
setDynamicCols(Integer.parseInt(value));
|
setDynamicCols(Integer.parseInt(value));
|
||||||
break;
|
break;
|
||||||
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(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:
|
default:
|
||||||
log.warning("Unknown behavior/connection key: " + key);
|
log.warning("Unknown behavior/connection key: " + key);
|
||||||
}
|
}
|
||||||
break;
|
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":
|
case "colors":
|
||||||
if (key.startsWith("color_")) {
|
if (key.startsWith("color_")) {
|
||||||
int index = Integer.parseInt(key.substring(6));
|
int index = Integer.parseInt(key.substring(6));
|
||||||
@@ -633,6 +729,9 @@ public class Settings {
|
|||||||
w.println("dynamicRows = " + getDynamicRows());
|
w.println("dynamicRows = " + getDynamicRows());
|
||||||
w.println("dynamicCols = " + getDynamicCols());
|
w.println("dynamicCols = " + getDynamicCols());
|
||||||
w.println("blockSelectMode = " + getBlockSelectMode());
|
w.println("blockSelectMode = " + getBlockSelectMode());
|
||||||
|
w.println("autoSysUnlock = " + getAutoSysUnlock());
|
||||||
|
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
|
||||||
|
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
|
||||||
w.println();
|
w.println();
|
||||||
|
|
||||||
// [entryassist]
|
// [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 tlsCheckBox;
|
||||||
private JCheckBox verifyCertCheckBox;
|
private JCheckBox verifyCertCheckBox;
|
||||||
private JCheckBox tn3270eCheckBox;
|
private JCheckBox tn3270eCheckBox;
|
||||||
|
private JCheckBox keepAliveCheckBox;
|
||||||
|
private JCheckBox autoReconnectCheckBox;
|
||||||
private boolean confirmed;
|
private boolean confirmed;
|
||||||
private ConnectionConfig result;
|
private ConnectionConfig result;
|
||||||
|
|
||||||
@@ -246,6 +248,24 @@ public class ConnectDialog extends JDialog {
|
|||||||
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
|
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
|
||||||
mainPanel.add(tn3270eCheckBox, gbc);
|
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
|
// Buttons
|
||||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
|
||||||
|
|
||||||
@@ -266,7 +286,7 @@ public class ConnectDialog extends JDialog {
|
|||||||
buttonPanel.add(connectBtn);
|
buttonPanel.add(connectBtn);
|
||||||
|
|
||||||
gbc.gridx = 0;
|
gbc.gridx = 0;
|
||||||
gbc.gridy = 10;
|
gbc.gridy = 12;
|
||||||
gbc.gridwidth = 2;
|
gbc.gridwidth = 2;
|
||||||
mainPanel.add(buttonPanel, gbc);
|
mainPanel.add(buttonPanel, gbc);
|
||||||
|
|
||||||
@@ -328,6 +348,18 @@ public class ConnectDialog extends JDialog {
|
|||||||
result.setUseTls(tlsCheckBox.isSelected());
|
result.setUseTls(tlsCheckBox.isSelected());
|
||||||
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
result.setTlsVerifyCert(verifyCertCheckBox.isSelected());
|
||||||
result.setTn3270eEnabled(tn3270eCheckBox.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;
|
confirmed = true;
|
||||||
dispose();
|
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.*;
|
||||||
import java.awt.event.*;
|
import java.awt.event.*;
|
||||||
|
import java.io.File;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.swing.table.DefaultTableModel;
|
import javax.swing.table.DefaultTableModel;
|
||||||
@@ -33,6 +34,8 @@ public class SettingsDialog extends JDialog {
|
|||||||
private JCheckBox blockSelectCheck;
|
private JCheckBox blockSelectCheck;
|
||||||
private JSpinner dynamicRowsSpinner;
|
private JSpinner dynamicRowsSpinner;
|
||||||
private JSpinner dynamicColsSpinner;
|
private JSpinner dynamicColsSpinner;
|
||||||
|
private JCheckBox enablePasteFromExcelCheck;
|
||||||
|
private JCheckBox pasteStopAtProtectedCheck;
|
||||||
|
|
||||||
// Entry Assist & Modes tab
|
// Entry Assist & Modes tab
|
||||||
private JCheckBox docModeCheck;
|
private JCheckBox docModeCheck;
|
||||||
@@ -291,10 +294,18 @@ public class SettingsDialog extends JDialog {
|
|||||||
gbc.gridx = 1;
|
gbc.gridx = 1;
|
||||||
panel.add(dynDimPanel, gbc);
|
panel.add(dynDimPanel, gbc);
|
||||||
|
|
||||||
// Placeholder for potentially more behavior options below
|
// Clipboard & Tabular Paste options
|
||||||
gbc.gridx = 0;
|
gbc.gridx = 0;
|
||||||
gbc.gridy = 4;
|
gbc.gridy = 4;
|
||||||
gbc.gridwidth = 2;
|
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;
|
gbc.weighty = 1.0;
|
||||||
panel.add(Box.createGlue(), gbc);
|
panel.add(Box.createGlue(), gbc);
|
||||||
|
|
||||||
@@ -653,6 +664,45 @@ public class SettingsDialog extends JDialog {
|
|||||||
});
|
});
|
||||||
btnPanel.add(btnReset);
|
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(new JScrollPane(table), BorderLayout.CENTER);
|
||||||
main.add(btnPanel, BorderLayout.SOUTH);
|
main.add(btnPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
@@ -705,6 +755,14 @@ public class SettingsDialog extends JDialog {
|
|||||||
// Block select mode
|
// Block select mode
|
||||||
Settings.setBlockSelectMode(blockSelectCheck.isSelected());
|
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
|
// Default Dynamic screen dimensions
|
||||||
if (dynamicRowsSpinner != null && dynamicColsSpinner != null) {
|
if (dynamicRowsSpinner != null && dynamicColsSpinner != null) {
|
||||||
Settings.setDynamicRows((Integer) dynamicRowsSpinner.getValue());
|
Settings.setDynamicRows((Integer) dynamicRowsSpinner.getValue());
|
||||||
|
|||||||
@@ -137,12 +137,20 @@ public class StatusBar extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void applyTheme(UITheme theme) {
|
public void applyTheme(UITheme theme) {
|
||||||
|
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||||
|
SwingUtilities.invokeLater(() -> applyTheme(theme));
|
||||||
|
return;
|
||||||
|
}
|
||||||
setBackground(ThemeManager.getStatusBarBg(theme));
|
setBackground(ThemeManager.getStatusBarBg(theme));
|
||||||
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
|
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme)));
|
||||||
updateStatus();
|
updateStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateStatus() {
|
public void updateStatus() {
|
||||||
|
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||||
|
SwingUtilities.invokeLater(this::updateStatus);
|
||||||
|
return;
|
||||||
|
}
|
||||||
UITheme theme = ThemeManager.getTheme();
|
UITheme theme = ThemeManager.getTheme();
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
connectionStatus.setText("Not Connected");
|
connectionStatus.setText("Not Connected");
|
||||||
@@ -170,6 +178,10 @@ public class StatusBar extends JPanel {
|
|||||||
connectionStatus.setText("Not Connected");
|
connectionStatus.setText("Not Connected");
|
||||||
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
connectionStatus.setForeground(ThemeManager.getOiaFgDim(theme));
|
||||||
break;
|
break;
|
||||||
|
case RECONNECTING:
|
||||||
|
connectionStatus.setText("Reconnecting...");
|
||||||
|
connectionStatus.setForeground(ThemeManager.getOiaFgAlert(theme));
|
||||||
|
break;
|
||||||
case TCP_PENDING:
|
case TCP_PENDING:
|
||||||
case TELNET_PENDING:
|
case TELNET_PENDING:
|
||||||
connectionStatus.setText("Connecting...");
|
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 cols = sb != null ? sb.getDisplayCols() : 80;
|
||||||
int curRow = cols > 0 ? curPos / cols : 0;
|
int curRow = cols > 0 ? curPos / cols : 0;
|
||||||
int curCol = 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);
|
client.getPS().pasteString(text, curRow, curCol);
|
||||||
} else {
|
} else {
|
||||||
for (char ch : text.toCharArray()) {
|
for (char ch : text.toCharArray()) {
|
||||||
@@ -603,6 +607,10 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
|
|
||||||
|
|
||||||
public void guardedPack() {
|
public void guardedPack() {
|
||||||
|
if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) {
|
||||||
|
SwingUtilities.invokeLater(this::guardedPack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
resizeGuard = true;
|
resizeGuard = true;
|
||||||
revalidate();
|
revalidate();
|
||||||
Container top = getTopLevelAncestor();
|
Container top = getTopLevelAncestor();
|
||||||
@@ -1163,10 +1171,19 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
if (ip != null) {
|
if (ip != null) {
|
||||||
ip.setBellEnabled(haus.nightmare.j3270.config.Settings.getEntryAssistBell());
|
ip.setBellEnabled(haus.nightmare.j3270.config.Settings.getEntryAssistBell());
|
||||||
ip.setBellColumn(Math.max(0, haus.nightmare.j3270.config.Settings.getEntryAssistBellCol() - 1));
|
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.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid());
|
||||||
ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
|
ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock());
|
||||||
ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled());
|
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) {
|
if (statusBar != null) {
|
||||||
statusBar.updateStatus();
|
statusBar.updateStatus();
|
||||||
@@ -1432,8 +1449,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
|||||||
if (rgb != null && gWidth > 0 && gHeight > 0) {
|
if (rgb != null && gWidth > 0 && gHeight > 0) {
|
||||||
long currentUpdateCount = client.getGraphicsPlane().getUpdateCount();
|
long currentUpdateCount = client.getGraphicsPlane().getUpdateCount();
|
||||||
if (cachedGraphicsImage == null || currentUpdateCount != lastGraphicsUpdateCount || cachedGraphicsImage.getWidth() != gWidth || cachedGraphicsImage.getHeight() != gHeight) {
|
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 = AwtPixelBufferBridge.toBufferedImage(client.getGraphicsPlane());
|
||||||
cachedGraphicsImage.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth);
|
|
||||||
lastGraphicsUpdateCount = currentUpdateCount;
|
lastGraphicsUpdateCount = currentUpdateCount;
|
||||||
}
|
}
|
||||||
if (gWidth == gridW && gHeight == gridH) {
|
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);
|
haus.nightmare.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
|
||||||
if (slot != null) {
|
if (slot != null) {
|
||||||
int symBg = (bgIsExplicit || reverse) ? bgColor.getRGB() : 0;
|
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) {
|
if (img != null) {
|
||||||
g2.drawImage(img, x, y, null);
|
g2.drawImage(img, x, y, null);
|
||||||
drawnAsPs = true;
|
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) {
|
public void setHodWallpaper(haus.nightmare.lib3270j.graphics.HODWallpaper wallpaper) {
|
||||||
this.hodWallpaper = wallpaper;
|
this.hodWallpaper = wallpaper;
|
||||||
if (wallpaper != null) {
|
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();
|
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> environmentVariables = new java.util.LinkedHashMap<>();
|
||||||
private java.util.Map<String, String> userVariables = 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() {}
|
||||||
|
|
||||||
public ConnectionConfig(String host, int port) {
|
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"),
|
* 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"),
|
* 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 pUser = null;
|
||||||
String pPass = null;
|
String pPass = null;
|
||||||
|
|
||||||
|
boolean keepAlive = true;
|
||||||
|
boolean autoReconnect = false;
|
||||||
|
|
||||||
String[] tokens = s.split("\\s+");
|
String[] tokens = s.split("\\s+");
|
||||||
StringBuilder remaining = new StringBuilder();
|
StringBuilder remaining = new StringBuilder();
|
||||||
for (String tok : tokens) {
|
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();
|
String proxyUrl = tok.substring(tok.indexOf('=') + 1).trim();
|
||||||
try {
|
try {
|
||||||
java.net.URI uri = new java.net.URI(proxyUrl);
|
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);
|
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||||
config.setUseTls(tls);
|
config.setUseTls(tls);
|
||||||
config.setTn3270eEnabled(tn3270e);
|
config.setTn3270eEnabled(tn3270e);
|
||||||
|
config.setKeepAliveEnabled(keepAlive);
|
||||||
|
config.setAutoReconnect(autoReconnect);
|
||||||
if (dynamic) {
|
if (dynamic) {
|
||||||
config.setDynamicDimensions(dynRows, dynCols);
|
config.setDynamicDimensions(dynRows, dynCols);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ public class Telnet3270Client {
|
|||||||
private final haus.nightmare.lib3270j.ecl.ECLXfer xfer;
|
private final haus.nightmare.lib3270j.ecl.ECLXfer xfer;
|
||||||
private TelnetConnection connection;
|
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) {
|
public Telnet3270Client(ConnectionConfig config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.translator = new EbcdicTranslator(config.getCodePage());
|
this.translator = new EbcdicTranslator(config.getCodePage());
|
||||||
@@ -58,6 +61,7 @@ public class Telnet3270Client {
|
|||||||
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
|
||||||
}
|
}
|
||||||
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
|
||||||
|
this.dsProcessor.setAutoSysUnlock(config.isAutoSysUnlock());
|
||||||
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
this.dsProcessor.getQueryReplyBuilder().setGraphicsMode(config.getGraphicsMode());
|
||||||
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||||
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
|
||||||
@@ -87,6 +91,33 @@ public class Telnet3270Client {
|
|||||||
@Override public void onSoundAlarm() {
|
@Override public void onSoundAlarm() {
|
||||||
ps.notifyAlarm();
|
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.
|
* Disconnect from the host.
|
||||||
*/
|
*/
|
||||||
public void disconnect() {
|
public void disconnect() {
|
||||||
|
cancelAutoReconnect();
|
||||||
if (connection != null) {
|
if (connection != null) {
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
connection = null;
|
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;
|
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. */
|
/** Send an NVT ASCII character in NVT mode. */
|
||||||
public void sendNVTChar(char c) throws IOException {
|
public void sendNVTChar(char c) throws IOException {
|
||||||
fsm.sendNVTChar(c);
|
fsm.sendNVTChar(c);
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ public class DataStreamProcessor {
|
|||||||
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
|
||||||
private int currentGocaSubtype = 0;
|
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. */
|
/** Functional interface for sending output back through the telnet stack. */
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface OutputSender {
|
public interface OutputSender {
|
||||||
@@ -131,6 +138,21 @@ public class DataStreamProcessor {
|
|||||||
screenListeners.remove(l);
|
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.
|
* Process a 3270 data stream record.
|
||||||
*
|
*
|
||||||
@@ -197,16 +219,19 @@ public class DataStreamProcessor {
|
|||||||
break;
|
break;
|
||||||
case CMD_RB:
|
case CMD_RB:
|
||||||
case SNA_CMD_RB:
|
case SNA_CMD_RB:
|
||||||
|
rcvdRead = true;
|
||||||
programSymbolManager.commitStagedSymbols();
|
programSymbolManager.commitStagedSymbols();
|
||||||
processReadBuffer();
|
processReadBuffer();
|
||||||
break;
|
break;
|
||||||
case CMD_RM:
|
case CMD_RM:
|
||||||
case SNA_CMD_RM:
|
case SNA_CMD_RM:
|
||||||
|
rcvdRead = true;
|
||||||
programSymbolManager.commitStagedSymbols();
|
programSymbolManager.commitStagedSymbols();
|
||||||
processReadModified(false);
|
processReadModified(false);
|
||||||
break;
|
break;
|
||||||
case CMD_RMA:
|
case CMD_RMA:
|
||||||
case SNA_CMD_RMA:
|
case SNA_CMD_RMA:
|
||||||
|
rcvdRead = true;
|
||||||
programSymbolManager.commitStagedSymbols();
|
programSymbolManager.commitStagedSymbols();
|
||||||
processReadModified(true);
|
processReadModified(true);
|
||||||
break;
|
break;
|
||||||
@@ -235,7 +260,10 @@ public class DataStreamProcessor {
|
|||||||
screen.updateDisplaySnapshot();
|
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);
|
inputProcessor.setKeyboardLocked(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,10 +361,13 @@ public class DataStreamProcessor {
|
|||||||
log.fine("WCC: " + String.format("0x%02x", wcc) +
|
log.fine("WCC: " + String.format("0x%02x", wcc) +
|
||||||
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
|
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
|
||||||
|
|
||||||
if (kbdRestore || inputProcessor != null) {
|
if (kbdRestore) {
|
||||||
if (inputProcessor != null) {
|
unlockPending = true;
|
||||||
inputProcessor.setKeyboardLocked(false);
|
unlockSysPending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!contentionResolution && kbdRestore && inputProcessor != null) {
|
||||||
|
inputProcessor.setKeyboardLocked(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resetMdt) {
|
if (resetMdt) {
|
||||||
@@ -1101,6 +1132,9 @@ public class DataStreamProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void notifyScreenUpdated() {
|
private void notifyScreenUpdated() {
|
||||||
|
if (screen != null) {
|
||||||
|
screen.notifyScreenUpdate();
|
||||||
|
}
|
||||||
for (ScreenUpdateListener l : screenListeners) {
|
for (ScreenUpdateListener l : screenListeners) {
|
||||||
l.onScreenUpdated();
|
l.onScreenUpdated();
|
||||||
}
|
}
|
||||||
@@ -1549,7 +1583,11 @@ public class DataStreamProcessor {
|
|||||||
log.fine("processWCC: " + String.format("0x%02x", wcc) +
|
log.fine("processWCC: " + String.format("0x%02x", wcc) +
|
||||||
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
|
" 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);
|
inputProcessor.setKeyboardLocked(false);
|
||||||
}
|
}
|
||||||
if (resetMdt) {
|
if (resetMdt) {
|
||||||
|
|||||||
+3
-4
@@ -1,7 +1,6 @@
|
|||||||
package haus.nightmare.lib3270j.eNetwork.ECL.event;
|
package haus.nightmare.lib3270j.eNetwork.ECL.event;
|
||||||
|
|
||||||
import java.awt.Image;
|
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||||
import java.awt.Rectangle;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop-in IBM Host On-Demand compatible facade for ECLPSGraphicsEvent.
|
* 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);
|
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);
|
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);
|
super(source, id, image, rectangle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
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.
|
* Drop-in IBM Host On-Demand compatible facade for FillArea.
|
||||||
|
|||||||
+1
-3
@@ -1,12 +1,10 @@
|
|||||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||||
|
|
||||||
import java.awt.Component;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop-in IBM Host On-Demand compatible facade for HODBitImage.
|
* Drop-in IBM Host On-Demand compatible facade for HODBitImage.
|
||||||
*/
|
*/
|
||||||
public class HODBitImage extends haus.nightmare.lib3270j.graphics.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);
|
super(comp, width, height, data, baseColor, depth, useGraphicColors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-6
@@ -1,8 +1,7 @@
|
|||||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||||
|
|
||||||
import java.awt.Component;
|
import haus.nightmare.lib3270j.graphics.Dimension;
|
||||||
import java.awt.Dimension;
|
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||||
import java.awt.Rectangle;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop-in IBM Host On-Demand compatible facade for HODPart.
|
* Drop-in IBM Host On-Demand compatible facade for HODPart.
|
||||||
@@ -12,15 +11,15 @@ public class HODPart extends haus.nightmare.lib3270j.graphics.HODPart {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public HODPart(Component component) {
|
public HODPart(Object component) {
|
||||||
super(component);
|
super(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HODPart(Component component, Dimension dimension) {
|
public HODPart(Object component, Dimension dimension) {
|
||||||
super(component, dimension);
|
super(component, dimension);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HODPart(Component component, Rectangle rectangle) {
|
public HODPart(Object component, Rectangle rectangle) {
|
||||||
super(component, rectangle);
|
super(component, rectangle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-3
@@ -1,7 +1,5 @@
|
|||||||
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
package haus.nightmare.lib3270j.eNetwork.ECL.hostgraphics;
|
||||||
|
|
||||||
import java.awt.Image;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop-in IBM Host On-Demand compatible facade for HODWallpaper.
|
* 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);
|
super(displayMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HODWallpaper(Image image, int displayMode) {
|
public HODWallpaper(Object image, int displayMode) {
|
||||||
super(image, displayMode);
|
super(image, displayMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package haus.nightmare.lib3270j.ecl;
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
import java.awt.Color;
|
import haus.nightmare.lib3270j.graphics.Color;
|
||||||
import java.awt.Component;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard implementation of ECLPSGraphicsServices conforming to IBM Host On-Demand ECL.
|
* Standard implementation of ECLPSGraphicsServices conforming to IBM Host On-Demand ECL.
|
||||||
|
* Completely decoupled from java.awt.
|
||||||
*/
|
*/
|
||||||
public class DefaultPSGraphicsServices implements ECLPSGraphicsServices {
|
public class DefaultPSGraphicsServices implements ECLPSGraphicsServices {
|
||||||
|
|
||||||
private final ECLPS ps;
|
private final ECLPS ps;
|
||||||
private Component visualComponent;
|
private Object visualComponent;
|
||||||
private Color[] colors;
|
private Color[] colors;
|
||||||
private final List<ECLPSGraphicsListener> listeners = new CopyOnWriteArrayList<>();
|
private final List<ECLPSGraphicsListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
@@ -20,11 +20,11 @@ public class DefaultPSGraphicsServices implements ECLPSGraphicsServices {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setVisualComponent(Component comp) {
|
public void setVisualComponent(Object comp) {
|
||||||
this.visualComponent = comp;
|
this.visualComponent = comp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Component getVisualComponent() {
|
public Object getVisualComponent() {
|
||||||
return visualComponent;
|
return visualComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ public class ECLConnection {
|
|||||||
private String luName;
|
private String luName;
|
||||||
private String workstationId = "";
|
private String workstationId = "";
|
||||||
private boolean ssl = false;
|
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 contentionResolution = false;
|
||||||
private boolean luluSession = false;
|
private boolean luluSession = false;
|
||||||
private boolean isNegCR = false;
|
private boolean isNegCR = false;
|
||||||
@@ -71,12 +77,49 @@ public class ECLConnection {
|
|||||||
if (props != null) {
|
if (props != null) {
|
||||||
this.properties.putAll(props);
|
this.properties.putAll(props);
|
||||||
convertData(this.properties);
|
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) {
|
public ECLConnection(ECLSession session, Telnet3270Client client) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.client = client;
|
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) {
|
if (client != null) {
|
||||||
client.addConnectionListener(new ConnectionListener() {
|
client.addConnectionListener(new ConnectionListener() {
|
||||||
@@ -114,6 +157,13 @@ public class ECLConnection {
|
|||||||
state, state, "TN3270E Negotiated", deviceType, deviceName);
|
state, state, "TN3270E Negotiated", deviceType, deviceName);
|
||||||
notifyCommEvent(event);
|
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 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 void SetContentionResolution(boolean bl) { setContentionResolution(bl); }
|
||||||
public boolean getContentionResolution() { return contentionResolution; }
|
public boolean getContentionResolution() {
|
||||||
public boolean isContentionResolution() { return contentionResolution; }
|
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 void set_LULU_Session(boolean bl) { this.luluSession = bl; }
|
||||||
public boolean is_LULU_Session() { return luluSession; }
|
public boolean is_LULU_Session() { return luluSession; }
|
||||||
public boolean get_LULU_Session() { return luluSession; }
|
public boolean get_LULU_Session() { return luluSession; }
|
||||||
|
|
||||||
public boolean isNegotiateCResolution() { return isNegCR; }
|
public boolean isNegotiateCResolution() {
|
||||||
public void setNegotiatedCResolution(boolean bl) { this.isNegCR = bl; }
|
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 boolean isBIND7FArchitectureViolation() { return isBIND7FArchitectureViolation; }
|
||||||
public void setBIND7FArchitectureViolation(boolean bl) { this.isBIND7FArchitectureViolation = bl; }
|
public void setBIND7FArchitectureViolation(boolean bl) { this.isBIND7FArchitectureViolation = bl; }
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package haus.nightmare.lib3270j.ecl;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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.input.InputProcessor;
|
||||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
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() {
|
public synchronized void notifyOIAChanged() {
|
||||||
|
signalWaiters();
|
||||||
ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(),
|
ECLOIAEvent event = new ECLOIAEvent(this, ECLOIAEvent.OIA_UPDATE, getInputInhibited(),
|
||||||
getAlphanumericType(), isInsertMode(), getStatusString());
|
getAlphanumericType(), isInsertMode(), getStatusString());
|
||||||
for (haus.nightmare.lib3270j.ecl.ECLOIANotify l : listeners) {
|
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.
|
* @return true if keyboard unlocked, false if timeout occurred.
|
||||||
*/
|
*/
|
||||||
public boolean waitForInput(long timeoutMs) {
|
public boolean waitForInput(long timeoutMs) {
|
||||||
long start = System.currentTimeMillis();
|
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
|
||||||
if (getInputInhibited() == INHIBIT_NOT_INHIBITED) {
|
if (getInputInhibited() == INHIBIT_NOT_INHIBITED) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
oiaLock.lock();
|
||||||
try {
|
try {
|
||||||
Thread.sleep(20);
|
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||||
|
while (getInputInhibited() != INHIBIT_NOT_INHIBITED) {
|
||||||
|
if (remainingNanos <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
remainingNanos = oiaCondition.awaitNanos(remainingNanos);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getInputInhibited() == INHIBIT_NOT_INHIBITED;
|
return true;
|
||||||
|
} finally {
|
||||||
|
oiaLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -458,20 +483,26 @@ public class ECLOIA implements ECLConstants {
|
|||||||
* Block until any OIA transition occurs.
|
* Block until any OIA transition occurs.
|
||||||
*/
|
*/
|
||||||
public boolean waitForTransition(long timeoutMs) {
|
public boolean waitForTransition(long timeoutMs) {
|
||||||
|
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||||
int initialInhibit = getInputInhibited();
|
int initialInhibit = getInputInhibited();
|
||||||
long start = System.currentTimeMillis();
|
oiaLock.lock();
|
||||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
try {
|
||||||
if (getInputInhibited() != initialInhibit) {
|
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||||
return true;
|
while (getInputInhibited() == initialInhibit) {
|
||||||
|
if (remainingNanos <= 0) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Thread.sleep(20);
|
remainingNanos = oiaCondition.awaitNanos(remainingNanos);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getInputInhibited() != initialInhibit;
|
return true;
|
||||||
|
} finally {
|
||||||
|
oiaLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean WaitForTransition(long timeoutMs) {
|
public boolean WaitForTransition(long timeoutMs) {
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ package haus.nightmare.lib3270j.ecl;
|
|||||||
|
|
||||||
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
||||||
import haus.nightmare.lib3270j.input.InputProcessor;
|
import haus.nightmare.lib3270j.input.InputProcessor;
|
||||||
|
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
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.*;
|
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, ECLScreenDesc> descriptorListeners = new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
private final java.util.Map<ECLPSListener, Integer> listenerEventTypes = 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) {
|
public ECLPS(ScreenBuffer screen, InputProcessor inputProcessor, EbcdicTranslator translator) {
|
||||||
this.screen = screen;
|
this.screen = screen;
|
||||||
this.inputProcessor = inputProcessor;
|
this.inputProcessor = inputProcessor;
|
||||||
@@ -46,6 +75,27 @@ public class ECLPS implements ECLConstants {
|
|||||||
this.bidiServices = new DefaultPSBIDIServices(this);
|
this.bidiServices = new DefaultPSBIDIServices(this);
|
||||||
this.hindiServices = new DefaultPSHindiServices(this);
|
this.hindiServices = new DefaultPSHindiServices(this);
|
||||||
this.thaiServices = new DefaultPSTHAIServices(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) {
|
public ECLPS(ECLSession session) {
|
||||||
@@ -494,11 +544,71 @@ public class ECLPS implements ECLConstants {
|
|||||||
return copyString(sRow, sCol, eRow, eCol);
|
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).
|
* Paste a multi-line rectangular block of text starting at (row, col).
|
||||||
*/
|
*/
|
||||||
public synchronized int pasteString(String text, int row, int col) {
|
public synchronized int pasteString(String text, int row, int col) {
|
||||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
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 rows = screen.getRows();
|
||||||
int cols = screen.getCols();
|
int cols = screen.getCols();
|
||||||
if (rows <= 0 || cols <= 0) return 0;
|
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) {
|
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) {
|
public int PasteRectangular(String text, int row, int col) {
|
||||||
@@ -848,6 +984,20 @@ public class ECLPS implements ECLConstants {
|
|||||||
UnregisterPSEvent(listener);
|
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) {
|
public void notifyPSEvent(ECLPSEvent event) {
|
||||||
for (ECLPSListener l : psListeners) {
|
for (ECLPSListener l : psListeners) {
|
||||||
ECLScreenDesc desc = descriptorListeners.get(l);
|
ECLScreenDesc desc = descriptorListeners.get(l);
|
||||||
@@ -899,6 +1049,7 @@ public class ECLPS implements ECLConstants {
|
|||||||
int c = (screen != null) ? screen.getCols() : 0;
|
int c = (screen != null) ? screen.getCols() : 0;
|
||||||
int cur = (screen != null) ? screen.getCursorAddress() : 0;
|
int cur = (screen != null) ? screen.getCursorAddress() : 0;
|
||||||
int ring = ringCounter.incrementAndGet();
|
int ring = ringCounter.incrementAndGet();
|
||||||
|
signalWaiters();
|
||||||
ECLPSEvent evt = new ECLPSEvent(this, ECLPSEvent.PS_UPDATE, type, startRow, startCol, endRow, endCol,
|
ECLPSEvent evt = new ECLPSEvent(this, ECLPSEvent.PS_UPDATE, type, startRow, startCol, endRow, endCol,
|
||||||
cur, cur, r, c, full, cursorVisible, ring, startPrinter, null);
|
cur, cur, r, c, full, cursorVisible, ring, startPrinter, null);
|
||||||
notifyPSEvent(evt);
|
notifyPSEvent(evt);
|
||||||
@@ -910,12 +1061,14 @@ public class ECLPS implements ECLConstants {
|
|||||||
int row = (c > 0) ? newAddress / c : 0;
|
int row = (c > 0) ? newAddress / c : 0;
|
||||||
int col = (c > 0) ? newAddress % c : 0;
|
int col = (c > 0) ? newAddress % c : 0;
|
||||||
int ring = ringCounter.incrementAndGet();
|
int ring = ringCounter.incrementAndGet();
|
||||||
|
signalWaiters();
|
||||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_CURSOR, USER_EVENTS, row, col, row, col,
|
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_CURSOR, USER_EVENTS, row, col, row, col,
|
||||||
oldAddress, newAddress, r, c, false, cursorVisible, ring, false, null));
|
oldAddress, newAddress, r, c, false, cursorVisible, ring, false, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyAlarm() {
|
public void notifyAlarm() {
|
||||||
int ring = ringCounter.incrementAndGet();
|
int ring = ringCounter.incrementAndGet();
|
||||||
|
signalWaiters();
|
||||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_ALARM, HOST_EVENTS, 0, 0, 0, 0,
|
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_ALARM, HOST_EVENTS, 0, 0, 0, 0,
|
||||||
0, 0, 0, 0, false, cursorVisible, ring, false, null));
|
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) {
|
public void notifyScreenResized(int rows, int cols) {
|
||||||
int cur = (screen != null) ? screen.getCursorAddress() : 0;
|
int cur = (screen != null) ? screen.getCursorAddress() : 0;
|
||||||
int ring = ringCounter.incrementAndGet();
|
int ring = ringCounter.incrementAndGet();
|
||||||
|
signalWaiters();
|
||||||
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_RESIZE, HOST_EVENTS, 0, 0, rows - 1, cols - 1,
|
notifyPSEvent(new ECLPSEvent(this, ECLPSEvent.PS_RESIZE, HOST_EVENTS, 0, 0, rows - 1, cols - 1,
|
||||||
cur, cur, rows, cols, true, cursorVisible, ring, false, null));
|
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) {
|
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||||
if (desc == null) return true;
|
if (desc == null) return true;
|
||||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
ECLOIA oia = (session != null) ? session.GetOIA() : null;
|
ECLOIA oia = (session != null) ? session.GetOIA() : null;
|
||||||
while (System.currentTimeMillis() - start < limit) {
|
|
||||||
if (desc.Matches(this, oia)) {
|
if (desc.Matches(this, oia)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
ReentrantLock lock = getSyncLock();
|
||||||
|
Condition cond = getSyncCondition();
|
||||||
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
Thread.sleep(25);
|
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||||
|
while (!desc.Matches(this, oia)) {
|
||||||
|
if (remainingNanos <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return desc.Matches(this, oia);
|
return true;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
|
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||||
@@ -1097,20 +1261,30 @@ public class ECLPS implements ECLConstants {
|
|||||||
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
|
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||||
if (desc == null) return true;
|
if (desc == null) return true;
|
||||||
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
ECLOIA oia = (session != null) ? session.GetOIA() : null;
|
ECLOIA oia = (session != null) ? session.GetOIA() : null;
|
||||||
while (System.currentTimeMillis() - start < limit) {
|
|
||||||
if (!desc.Matches(this, oia)) {
|
if (!desc.Matches(this, oia)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
ReentrantLock lock = getSyncLock();
|
||||||
|
Condition cond = getSyncCondition();
|
||||||
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
Thread.sleep(25);
|
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||||
|
while (desc.Matches(this, oia)) {
|
||||||
|
if (remainingNanos <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return !desc.Matches(this, oia);
|
return true;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
|
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.
|
* Block until the specified text appears anywhere on the presentation space.
|
||||||
*/
|
*/
|
||||||
public boolean waitForScreen(String text, long timeoutMs) {
|
public boolean waitForScreen(String text, long timeoutMs) {
|
||||||
long start = System.currentTimeMillis();
|
if (text == null || text.isEmpty()) return true;
|
||||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||||
if (searchString(text) >= 0) {
|
if (searchString(text) >= 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
ReentrantLock lock = getSyncLock();
|
||||||
|
Condition cond = getSyncCondition();
|
||||||
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
Thread.sleep(25);
|
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||||
|
while (searchString(text) < 0) {
|
||||||
|
if (remainingNanos <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return searchString(text) >= 0;
|
return true;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean WaitForScreen(String text, long timeoutMs) {
|
public boolean WaitForScreen(String text, long timeoutMs) {
|
||||||
return waitForScreen(text, 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.
|
* Block until the specified text appears at the given (row, col) coordinate.
|
||||||
*/
|
*/
|
||||||
public boolean waitForScreen(String text, int row, int col, long timeoutMs) {
|
public boolean waitForScreen(String text, int row, int col, long timeoutMs) {
|
||||||
long start = System.currentTimeMillis();
|
if (text == null) return true;
|
||||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||||
String onScreen = getString(row, col, text.length());
|
if (text.equals(getString(row, col, text.length()))) {
|
||||||
if (text.equals(onScreen)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
ReentrantLock lock = getSyncLock();
|
||||||
|
Condition cond = getSyncCondition();
|
||||||
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
Thread.sleep(25);
|
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) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return text.equals(getString(row, col, text.length()));
|
return true;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean WaitForScreen(String text, int row, int col, long timeoutMs) {
|
public boolean WaitForScreen(String text, int row, int col, long timeoutMs) {
|
||||||
return waitForScreen(text, row, col, 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).
|
* Block until the cursor moves to (row, col).
|
||||||
*/
|
*/
|
||||||
public boolean waitForCursor(int row, int col, long timeoutMs) {
|
public boolean waitForCursor(int row, int col, long timeoutMs) {
|
||||||
long start = System.currentTimeMillis();
|
long limit = (timeoutMs <= 0) ? 120000L : timeoutMs;
|
||||||
while (System.currentTimeMillis() - start < timeoutMs) {
|
|
||||||
if (getCursorRow() == row && getCursorCol() == col) {
|
if (getCursorRow() == row && getCursorCol() == col) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
ReentrantLock lock = getSyncLock();
|
||||||
|
Condition cond = getSyncCondition();
|
||||||
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
Thread.sleep(25);
|
long remainingNanos = TimeUnit.MILLISECONDS.toNanos(limit);
|
||||||
|
while (getCursorRow() != row || getCursorCol() != col) {
|
||||||
|
if (remainingNanos <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
remainingNanos = cond.awaitNanos(remainingNanos);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getCursorRow() == row && getCursorCol() == col;
|
return true;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean WaitForCursor(int row, int col, long timeoutMs) {
|
public boolean WaitForCursor(int row, int col, long timeoutMs) {
|
||||||
return waitForCursor(row, col, 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_ALARM = PS_ALARM;
|
||||||
public static final int EVENT_RESIZE = PS_RESIZE;
|
public static final int EVENT_RESIZE = PS_RESIZE;
|
||||||
public static final int EVENT_CLOSE = PS_CLOSE;
|
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 eventType;
|
||||||
private final int type;
|
private final int type;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package haus.nightmare.lib3270j.ecl;
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
import java.awt.Image;
|
import haus.nightmare.lib3270j.graphics.PixelBuffer;
|
||||||
import java.awt.Rectangle;
|
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conforms to IBM Host On-Demand ECLPSGraphicsEvent.
|
* Conforms to IBM Host On-Demand ECLPSGraphicsEvent.
|
||||||
|
* Decoupled from java.awt.
|
||||||
*/
|
*/
|
||||||
public class ECLPSGraphicsEvent {
|
public class ECLPSGraphicsEvent {
|
||||||
public static final int GRAPHICS_CURSOR_ON = 1;
|
public static final int GRAPHICS_CURSOR_ON = 1;
|
||||||
@@ -14,7 +15,7 @@ public class ECLPSGraphicsEvent {
|
|||||||
public static final int GRAPHICS_UPDATED = 5;
|
public static final int GRAPHICS_UPDATED = 5;
|
||||||
|
|
||||||
private int id;
|
private int id;
|
||||||
private Image image;
|
private Object image;
|
||||||
private Rectangle rect;
|
private Rectangle rect;
|
||||||
private ECLPS source;
|
private ECLPS source;
|
||||||
|
|
||||||
@@ -23,13 +24,13 @@ public class ECLPSGraphicsEvent {
|
|||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ECLPSGraphicsEvent(ECLPS source, int id, Image image) {
|
public ECLPSGraphicsEvent(ECLPS source, int id, Object image) {
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.image = image;
|
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.source = source;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.image = image;
|
this.image = image;
|
||||||
@@ -46,9 +47,13 @@ public class ECLPSGraphicsEvent {
|
|||||||
public int getID() { return this.id; }
|
public int getID() { return this.id; }
|
||||||
public int GetID() { return this.id; }
|
public int GetID() { return this.id; }
|
||||||
|
|
||||||
public void setImage(Image image) { this.image = image; }
|
public void setImage(Object image) { this.image = image; }
|
||||||
public Image getImage() { return this.image; }
|
public Object getImage() { return this.image; }
|
||||||
public Image 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 void setRectangle(Rectangle rect) { this.rect = rect; }
|
||||||
public Rectangle getRectangle() { return this.rect; }
|
public Rectangle getRectangle() { return this.rect; }
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package haus.nightmare.lib3270j.ecl;
|
package haus.nightmare.lib3270j.ecl;
|
||||||
|
|
||||||
import java.awt.Color;
|
import haus.nightmare.lib3270j.graphics.Color;
|
||||||
import java.awt.Component;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presentation Space graphics services interface conforming to IBM Host On-Demand ECL.
|
* Presentation Space graphics services interface conforming to IBM Host On-Demand ECL.
|
||||||
|
* Completely decoupled from java.awt.
|
||||||
*/
|
*/
|
||||||
public interface ECLPSGraphicsServices {
|
public interface ECLPSGraphicsServices {
|
||||||
void setVisualComponent(Component comp);
|
void setVisualComponent(Object comp);
|
||||||
void setGraphicColor(Color[] colors, boolean b);
|
void setGraphicColor(Color[] colors, boolean b);
|
||||||
void mousePressed(int x, int y, int button);
|
void mousePressed(int x, int y, int button);
|
||||||
void addGraphicsListener(ECLPSGraphicsListener listener);
|
void addGraphicsListener(ECLPSGraphicsListener listener);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class ECLSession {
|
|||||||
|
|
||||||
private static final Logger log = Logger.getLogger(ECLSession.class.getName());
|
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_HOST = "SESSION_HOST";
|
||||||
public static final String SESSION_PORT = "SESSION_PORT";
|
public static final String SESSION_PORT = "SESSION_PORT";
|
||||||
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
|
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_TN3270E = "SESSION_TN3270E";
|
||||||
public static final String SESSION_WIN_TITLE = "SESSION_WIN_TITLE";
|
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_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 Telnet3270Client client;
|
||||||
private final ECLConnection connection;
|
private final ECLConnection connection;
|
||||||
@@ -98,6 +116,9 @@ public class ECLSession {
|
|||||||
String tn3270eStr = getProp(props, SESSION_TN3270E, "tn3270e", "TN3270E", "true");
|
String tn3270eStr = getProp(props, SESSION_TN3270E, "tn3270e", "TN3270E", "true");
|
||||||
config.setTn3270eEnabled("true".equalsIgnoreCase(tn3270eStr) || "yes".equalsIgnoreCase(tn3270eStr) || "1".equals(tn3270eStr));
|
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);
|
String certUrl = getProp(props, "certificateURL", "CERTIFICATE_URL", "certificate_url", null);
|
||||||
if (certUrl != null) config.setKeyStorePath(certUrl);
|
if (certUrl != null) config.setKeyStorePath(certUrl);
|
||||||
String certPwd = getProp(props, "certificatePassword", "CERTIFICATE_PASSWORD", "certificate_password", null);
|
String certPwd = getProp(props, "certificatePassword", "CERTIFICATE_PASSWORD", "certificate_password", null);
|
||||||
@@ -118,6 +139,25 @@ public class ECLSession {
|
|||||||
config.setEnabledProtocols(tlsVer);
|
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;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +201,7 @@ public class ECLSession {
|
|||||||
properties.setProperty(SESSION_SSL, String.valueOf(cfg.isUseTls()));
|
properties.setProperty(SESSION_SSL, String.valueOf(cfg.isUseTls()));
|
||||||
if (cfg.getLuName() != null) properties.setProperty(SESSION_LU_NAME, cfg.getLuName());
|
if (cfg.getLuName() != null) properties.setProperty(SESSION_LU_NAME, cfg.getLuName());
|
||||||
properties.setProperty(SESSION_TN3270E, String.valueOf(cfg.isTn3270eEnabled()));
|
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 ==========
|
// ========== Automation Keystrokes & Waits ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -455,6 +532,41 @@ public class ECLSession {
|
|||||||
dispose();
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("ECLSession[host=%s, port=%d, connected=%b, state=%s]",
|
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.
|
* 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();
|
this();
|
||||||
if (color != null) {
|
if (color != null) {
|
||||||
this.fillColor = color.getRGB();
|
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()) {
|
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 minX = Double.MAX_VALUE, minY = Double.MAX_VALUE;
|
||||||
double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE;
|
double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE;
|
||||||
@@ -88,7 +107,7 @@ public class FillArea {
|
|||||||
int y = (int) Math.floor(minY);
|
int y = (int) Math.floor(minY);
|
||||||
int w = (int) Math.ceil(maxX) - x + 1;
|
int w = (int) Math.ceil(maxX) - x + 1;
|
||||||
int h = (int) Math.ceil(maxY) - y + 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() {
|
public synchronized void setFillModeOR() {
|
||||||
@@ -123,18 +142,20 @@ public class FillArea {
|
|||||||
this.pixelPattern = pat;
|
this.pixelPattern = pat;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized java.awt.Image getImage() {
|
public synchronized PixelBuffer getPixelBuffer() {
|
||||||
java.awt.Rectangle b = getBounds();
|
Rectangle b = getBounds();
|
||||||
if (b.width <= 0 || b.height <= 0) {
|
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);
|
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);
|
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);
|
DefaultPixelBuffer cropped = new DefaultPixelBuffer(b.width, b.height);
|
||||||
java.awt.Graphics g = img.getGraphics();
|
cropped.blit(tempPlane.getRgbBuffer(), b.x, b.y, b.width, b.height, 0, 0);
|
||||||
g.drawImage(tempPlane.getImage(), -b.x, -b.y, null);
|
return cropped;
|
||||||
g.dispose();
|
}
|
||||||
return img;
|
|
||||||
|
public synchronized Object getImage() {
|
||||||
|
return getPixelBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void dispose() {
|
public synchronized void dispose() {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package haus.nightmare.lib3270j.graphics;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
import java.awt.Point;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dedicated scaling layer for IBM 3179G / GDDM GOCA graphics presentation space.
|
* Dedicated scaling layer for IBM 3179G / GDDM GOCA graphics presentation space.
|
||||||
|
|||||||
@@ -1,19 +1,5 @@
|
|||||||
package haus.nightmare.lib3270j.graphics;
|
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.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -23,9 +9,9 @@ import java.util.logging.Logger;
|
|||||||
/**
|
/**
|
||||||
* Offscreen rendering surface for GOCA vector graphics.
|
* Offscreen rendering surface for GOCA vector graphics.
|
||||||
* Maintained as an ARGB 32-bit integer pixel buffer that overlays the 3270 character cell matrix.
|
* 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());
|
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);
|
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) {
|
||||||
public synchronized BufferedImage toBufferedImage() {
|
drawLine((double) x1, (double) y1, (double) x2, (double) y2, argb, currentLineType, currentLineWidth);
|
||||||
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);
|
|
||||||
}
|
|
||||||
return canvasImage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized Image getImage() {
|
@Override
|
||||||
return toBufferedImage();
|
public synchronized int getWidth() {
|
||||||
|
return canvasWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized Graphics getGraphics() {
|
@Override
|
||||||
return toBufferedImage().getGraphics();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasContent = true;
|
||||||
|
updateCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 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) {
|
public void setProgramSymbolManager(ProgramSymbolManager psm) {
|
||||||
@@ -230,16 +306,21 @@ public class GraphicsPlane {
|
|||||||
this.canvasWidth = w;
|
this.canvasWidth = w;
|
||||||
this.canvasHeight = h;
|
this.canvasHeight = h;
|
||||||
this.rgbBuffer = newBuffer;
|
this.rgbBuffer = newBuffer;
|
||||||
this.canvasImage = null;
|
|
||||||
updateViewingWindowPixels();
|
updateViewingWindowPixels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public synchronized void clear() {
|
public synchronized void clear() {
|
||||||
|
clear(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void clear(int argb) {
|
||||||
if (rgbBuffer != null) {
|
if (rgbBuffer != null) {
|
||||||
Arrays.fill(rgbBuffer, 0);
|
Arrays.fill(rgbBuffer, argb);
|
||||||
}
|
}
|
||||||
this.currentMixMode = 0;
|
this.currentMixMode = 0;
|
||||||
hasContent = false;
|
hasContent = (argb != 0);
|
||||||
updateCount++;
|
updateCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,6 +617,7 @@ public class GraphicsPlane {
|
|||||||
/**
|
/**
|
||||||
* Standard integer Bresenham line algorithm matching IBM 3179G / Host On-Demand 1-pixel rasterization.
|
* 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) {
|
public synchronized void drawLineBresenham(int x0, int y0, int x1, int y1, int color) {
|
||||||
int dx = Math.abs(x1 - x0);
|
int dx = Math.abs(x1 - x0);
|
||||||
int dy = Math.abs(y1 - y0);
|
int dy = Math.abs(y1 - y0);
|
||||||
@@ -598,6 +680,7 @@ public class GraphicsPlane {
|
|||||||
/**
|
/**
|
||||||
* Draws a line matching IBM 3179G / Host On-Demand rasterization.
|
* 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) {
|
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 color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : (GocaConstants.GOCA_COLORS[0] & 0x00FFFFFF);
|
||||||
int ix0 = (int) Math.round(x0);
|
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);
|
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) {
|
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 color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
|
||||||
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
|
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
|
||||||
@@ -1231,15 +1419,6 @@ public class GraphicsPlane {
|
|||||||
double tanShear = Math.tan(Math.toRadians(shearAngle));
|
double tanShear = Math.tan(Math.toRadians(shearAngle));
|
||||||
|
|
||||||
if (ch < 6.0) {
|
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;
|
int ptr = offset;
|
||||||
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
||||||
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
|
||||||
@@ -1249,7 +1428,8 @@ public class GraphicsPlane {
|
|||||||
int dataPtr = ptr + 2;
|
int dataPtr = ptr + 2;
|
||||||
|
|
||||||
if (numPoints >= 2) {
|
if (numPoints >= 2) {
|
||||||
Path2D.Double path = new Path2D.Double();
|
double[] px = new double[numPoints];
|
||||||
|
double[] py = new double[numPoints];
|
||||||
for (int p = 0; p < numPoints; p++) {
|
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 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);
|
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
|
||||||
@@ -1263,25 +1443,23 @@ public class GraphicsPlane {
|
|||||||
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
|
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
|
||||||
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
|
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
|
||||||
|
|
||||||
double px = x + rx;
|
px[p] = x + rx;
|
||||||
double py = y + ry;
|
py[p] = y + ry;
|
||||||
|
}
|
||||||
|
|
||||||
if (p == 0) path.moveTo(px, py);
|
for (int p = 0; p < numPoints - 1; p++) {
|
||||||
else path.lineTo(px, py);
|
drawLineAA(px[p], py[p], px[p + 1], py[p + 1], color);
|
||||||
}
|
}
|
||||||
g.draw(path);
|
|
||||||
}
|
}
|
||||||
ptr += 2 + byteLen;
|
ptr += 2 + byteLen;
|
||||||
} else {
|
} else {
|
||||||
ptr++;
|
ptr++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
g.dispose();
|
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
updateCount++;
|
updateCount++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
int ptr = offset;
|
int ptr = offset;
|
||||||
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
|
||||||
|
|||||||
@@ -1,30 +1,24 @@
|
|||||||
package haus.nightmare.lib3270j.graphics;
|
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).
|
* 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 {
|
public class HODBitImage {
|
||||||
protected Component vComponent;
|
protected Object vComponent;
|
||||||
protected Dimension iSize = new Dimension();
|
protected Dimension iSize = new Dimension();
|
||||||
protected Dimension iScaledSize = new Dimension();
|
protected Dimension iScaledSize = new Dimension();
|
||||||
protected int iDepth;
|
protected int iDepth;
|
||||||
protected int iScanLength;
|
protected int iScanLength;
|
||||||
protected boolean _iUseGraphicColors;
|
protected boolean _iUseGraphicColors;
|
||||||
protected byte[] iScaledImageData;
|
protected byte[] iScaledImageData;
|
||||||
protected Image[] hImage;
|
protected PixelBuffer[] hImage;
|
||||||
protected Image[] iScaledImage;
|
protected PixelBuffer[] iScaledImage;
|
||||||
protected int transparentBG = 0;
|
protected int transparentBG = 0;
|
||||||
protected byte[] hImageData;
|
protected byte[] hImageData;
|
||||||
protected int iBaseColor;
|
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.vComponent = comp;
|
||||||
this.iSize.width = width;
|
this.iSize.width = width;
|
||||||
this.iSize.height = height;
|
this.iSize.height = height;
|
||||||
@@ -32,8 +26,8 @@ public class HODBitImage {
|
|||||||
this.iBaseColor = baseColor;
|
this.iBaseColor = baseColor;
|
||||||
this.iDepth = depth;
|
this.iDepth = depth;
|
||||||
this._iUseGraphicColors = useGraphicColors;
|
this._iUseGraphicColors = useGraphicColors;
|
||||||
this.hImage = new Image[depth == 1 ? 17 : 1];
|
this.hImage = new PixelBuffer[depth == 1 ? 17 : 1];
|
||||||
this.iScaledImage = new Image[depth == 1 ? 17 : 1];
|
this.iScaledImage = new PixelBuffer[depth == 1 ? 17 : 1];
|
||||||
this.buildHODImage();
|
this.buildHODImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,11 +66,19 @@ public class HODBitImage {
|
|||||||
return nArray;
|
return nArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Image getHODImage(int colorIdx) {
|
public Object getHODImage(int colorIdx) {
|
||||||
return this.getHODImage(this.iSize.width, this.iSize.height, 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;
|
if (w <= 0 || h <= 0) return null;
|
||||||
boolean diffColor = this.iBaseColor != colorIdx && this.iDepth == 1;
|
boolean diffColor = this.iBaseColor != colorIdx && this.iDepth == 1;
|
||||||
boolean matchesBase = this.iSize.width == w && this.iSize.height == h;
|
boolean matchesBase = this.iSize.width == w && this.iSize.height == h;
|
||||||
@@ -88,13 +90,18 @@ public class HODBitImage {
|
|||||||
this.iScaledSize.height = h;
|
this.iScaledSize.height = h;
|
||||||
this.scaleHODImage();
|
this.scaleHODImage();
|
||||||
}
|
}
|
||||||
Image image = matchesBase ? this.hImage[colorIdx] : this.iScaledImage[colorIdx];
|
PixelBuffer image = matchesBase ? this.hImage[colorIdx] : this.iScaledImage[colorIdx];
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
HODColorChangeFilter filter = new HODColorChangeFilter(this.getHODColor(colorIdx));
|
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) {
|
if (base != null) {
|
||||||
FilteredImageSource source = new FilteredImageSource(base.getSource(), filter);
|
int bw = base.getWidth();
|
||||||
image = Toolkit.getDefaultToolkit().createImage(source);
|
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) {
|
if (matchesBase) {
|
||||||
this.hImage[colorIdx] = image;
|
this.hImage[colorIdx] = image;
|
||||||
} else {
|
} else {
|
||||||
@@ -121,27 +128,47 @@ public class HODBitImage {
|
|||||||
private void buildHODImage() {
|
private void buildHODImage() {
|
||||||
if (this.iSize.width <= 0 || this.iSize.height <= 0) return;
|
if (this.iSize.width <= 0 || this.iSize.height <= 0) return;
|
||||||
int[] pixels = getHODImageData(this.iBaseColor);
|
int[] pixels = getHODImageData(this.iBaseColor);
|
||||||
MemoryImageSource mis = new MemoryImageSource(this.iSize.width, this.iSize.height, pixels, 0, this.iSize.width);
|
PixelBuffer buf = new DefaultPixelBuffer(this.iSize.width, this.iSize.height, pixels);
|
||||||
Image img = Toolkit.getDefaultToolkit().createImage(mis);
|
|
||||||
if (this.iDepth == 1) {
|
if (this.iDepth == 1) {
|
||||||
this.hImage[this.iBaseColor] = img;
|
this.hImage[this.iBaseColor] = buf;
|
||||||
} else {
|
} else {
|
||||||
this.hImage[0] = img;
|
this.hImage[0] = buf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scaleHODImage() {
|
private void scaleHODImage() {
|
||||||
if (this.iScaledSize.width <= 0 || this.iScaledSize.height <= 0) return;
|
int sw = this.iScaledSize.width;
|
||||||
Image base = this.hImage[this.iDepth == 1 ? this.iBaseColor : 0];
|
int sh = this.iScaledSize.height;
|
||||||
|
if (sw <= 0 || sh <= 0) return;
|
||||||
|
PixelBuffer base = this.hImage[this.iDepth == 1 ? this.iBaseColor : 0];
|
||||||
if (base != null) {
|
if (base != null) {
|
||||||
this.iScaledImage[this.iDepth == 1 ? this.iBaseColor : 0] =
|
int bw = base.getWidth();
|
||||||
base.getScaledInstance(this.iScaledSize.width, this.iScaledSize.height, Image.SCALE_FAST);
|
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) {
|
private int getHODColor(int idx) {
|
||||||
if (idx == 0 && this.vComponent != null) {
|
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);
|
return GocaConstants.getGocaColorArgb(idx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package haus.nightmare.lib3270j.graphics;
|
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).
|
* Bounding box encapsulation matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODBounds).
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
package haus.nightmare.lib3270j.graphics;
|
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.
|
* Image filter that replaces occurrences of one color with another color.
|
||||||
* Conforms to IBM Host On-Demand HODColorChangeFilter.
|
* 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 oldRgb;
|
||||||
private int newRgb;
|
private int newRgb;
|
||||||
|
|
||||||
public HODColorChangeFilter(int newRgb) {
|
public HODColorChangeFilter(int newRgb) {
|
||||||
this.canFilterIndexColorModel = true;
|
|
||||||
this.oldRgb = -1;
|
this.oldRgb = -1;
|
||||||
this.newRgb = newRgb | 0xFF000000;
|
this.newRgb = newRgb | 0xFF000000;
|
||||||
}
|
}
|
||||||
@@ -23,7 +21,6 @@ public class HODColorChangeFilter extends RGBImageFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public HODColorChangeFilter(int oldRgb, int newRgb) {
|
public HODColorChangeFilter(int oldRgb, int newRgb) {
|
||||||
this.canFilterIndexColorModel = true;
|
|
||||||
this.oldRgb = oldRgb & 0x00FFFFFF;
|
this.oldRgb = oldRgb & 0x00FFFFFF;
|
||||||
this.newRgb = newRgb;
|
this.newRgb = newRgb;
|
||||||
}
|
}
|
||||||
@@ -48,7 +45,6 @@ public class HODColorChangeFilter extends RGBImageFilter {
|
|||||||
this.newRgb = newRgb;
|
this.newRgb = newRgb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int filterRGB(int x, int y, int rgb) {
|
public int filterRGB(int x, int y, int rgb) {
|
||||||
if (oldRgb == -1) {
|
if (oldRgb == -1) {
|
||||||
if ((rgb & 0xFF000000) != 0) {
|
if ((rgb & 0xFF000000) != 0) {
|
||||||
@@ -61,4 +57,20 @@ public class HODColorChangeFilter extends RGBImageFilter {
|
|||||||
}
|
}
|
||||||
return rgb;
|
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;
|
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).
|
* 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 {
|
public class HODGraphicsPlane {
|
||||||
private final GraphicsPlane delegate;
|
private final GraphicsPlane delegate;
|
||||||
@@ -40,6 +32,10 @@ public class HODGraphicsPlane {
|
|||||||
return delegate;
|
return delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PixelBuffer getPixelBuffer() {
|
||||||
|
return delegate;
|
||||||
|
}
|
||||||
|
|
||||||
public void resize(Dimension dim, boolean keepContent) {
|
public void resize(Dimension dim, boolean keepContent) {
|
||||||
if (dim != null) {
|
if (dim != null) {
|
||||||
delegate.setDimensions(dim.width, dim.height);
|
delegate.setDimensions(dim.width, dim.height);
|
||||||
@@ -52,15 +48,19 @@ public class HODGraphicsPlane {
|
|||||||
this.bounds.set(0, 0, delegate.getCanvasWidth(), delegate.getCanvasHeight());
|
this.bounds.set(0, 0, delegate.getCanvasWidth(), delegate.getCanvasHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Graphics getHODGraphics() {
|
public Object getHODGraphics() {
|
||||||
return delegate.getGraphics();
|
return delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Image getHODImage() {
|
public Object getHODImage() {
|
||||||
return delegate.getImage();
|
return delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHODTemporaryGraphics(Graphics g) {
|
public PixelBuffer getHODPixelBuffer() {
|
||||||
|
return delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHODTemporaryGraphics(Object g) {
|
||||||
// No-op or temporary override
|
// No-op or temporary override
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,38 +116,35 @@ public class HODGraphicsPlane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void drawHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
public void drawHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
||||||
Graphics g = delegate.getGraphics();
|
double rx = width / 2.0;
|
||||||
if (g != null) {
|
double ry = height / 2.0;
|
||||||
g.setColor(currentColor);
|
double cx = x + rx;
|
||||||
g.drawArc(x, y, width, height, startAngle, arcAngle);
|
double cy = y + ry;
|
||||||
|
delegate.drawArc(cx, cy, rx, ry, startAngle, arcAngle, currentColor.getRGB(), currentLineType, currentLineWidth, false);
|
||||||
updateHODBounds(x, y);
|
updateHODBounds(x, y);
|
||||||
updateHODBounds(x + width, y + height);
|
updateHODBounds(x + width, y + height);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void fillHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
public void fillHODArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
|
||||||
Graphics g = delegate.getGraphics();
|
double rx = width / 2.0;
|
||||||
if (g != null) {
|
double ry = height / 2.0;
|
||||||
g.setColor(currentColor);
|
double cx = x + rx;
|
||||||
g.fillArc(x, y, width, height, startAngle, arcAngle);
|
double cy = y + ry;
|
||||||
|
delegate.drawArc(cx, cy, rx, ry, startAngle, arcAngle, currentColor.getRGB(), currentLineType, currentLineWidth, true);
|
||||||
updateHODBounds(x, y);
|
updateHODBounds(x, y);
|
||||||
updateHODBounds(x + width, y + height);
|
updateHODBounds(x + width, y + height);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void drawHODImage(HODBitImage bitImage, int x, int y, int w, int h) {
|
public void drawHODImage(HODBitImage bitImage, int x, int y, int w, int h) {
|
||||||
if (bitImage != null) {
|
if (bitImage != null) {
|
||||||
Image img = bitImage.getHODImage(w, h, currentColorIndex);
|
PixelBuffer buf = bitImage.getHODPixelBuffer(w, h, currentColorIndex);
|
||||||
if (img != null) {
|
if (buf != null) {
|
||||||
Graphics g = delegate.getGraphics();
|
delegate.blit(buf.getPixels(), 0, 0, buf.getWidth(), buf.getHeight(), x, y);
|
||||||
if (g != null) {
|
|
||||||
g.drawImage(img, x, y, null);
|
|
||||||
updateHODBounds(x, y);
|
updateHODBounds(x, y);
|
||||||
updateHODBounds(x + w, y + h);
|
updateHODBounds(x + w, y + h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void fillHODArea(FillArea area) {
|
public void fillHODArea(FillArea area) {
|
||||||
if (area != null) {
|
if (area != null) {
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
package haus.nightmare.lib3270j.graphics;
|
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.io.Serializable;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Visual part container matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODPart).
|
* 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 {
|
public class HODPart extends Rectangle implements Serializable {
|
||||||
protected Component hodParent;
|
protected Object hodParent;
|
||||||
protected Font _hodFont;
|
protected Object _hodFont;
|
||||||
protected Color foregroundColor;
|
protected Color foregroundColor;
|
||||||
protected Color backgroundColor;
|
protected Color backgroundColor;
|
||||||
protected Boolean isTransparent;
|
protected Boolean isTransparent;
|
||||||
@@ -22,33 +17,36 @@ public class HODPart extends Rectangle implements Serializable {
|
|||||||
|
|
||||||
protected HODPart() {}
|
protected HODPart() {}
|
||||||
|
|
||||||
public HODPart(Component component) {
|
public HODPart(Object component) {
|
||||||
this();
|
this();
|
||||||
this.setHODParent(component);
|
this.setHODParent(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HODPart(Component component, Dimension dimension) {
|
public HODPart(Object component, Dimension dimension) {
|
||||||
super(dimension);
|
super(0, 0, dimension != null ? dimension.width : 0, dimension != null ? dimension.height : 0);
|
||||||
this.setHODParent(component);
|
this.setHODParent(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HODPart(Component component, Rectangle rectangle) {
|
public HODPart(Object component, Rectangle rectangle) {
|
||||||
super(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);
|
this.setHODParent(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HODPart(HODPart hODPart) {
|
public HODPart(HODPart hODPart) {
|
||||||
this(hODPart.getHODParent(), hODPart.getSize());
|
this(hODPart != null ? hODPart.getHODParent() : null, hODPart != null ? hODPart.getSize() : null);
|
||||||
|
if (hODPart != null) {
|
||||||
this.setHODBackground(hODPart.getHODBackground());
|
this.setHODBackground(hODPart.getHODBackground());
|
||||||
this.setHODForeground(hODPart.getHODForeground());
|
this.setHODForeground(hODPart.getHODForeground());
|
||||||
this.setHODFont(hODPart.getHODFont());
|
this.setHODFont(hODPart.getHODFont());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Component getHODParent() {
|
public Object getHODParent() {
|
||||||
return this.hodParent;
|
return this.hodParent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHODParent(Component component) {
|
public void setHODParent(Object component) {
|
||||||
if (component != null && !component.equals(this.hodParent)) {
|
if (component != null && !component.equals(this.hodParent)) {
|
||||||
this.hodParent = component;
|
this.hodParent = component;
|
||||||
}
|
}
|
||||||
@@ -56,18 +54,28 @@ public class HODPart extends Rectangle implements Serializable {
|
|||||||
|
|
||||||
public void repaint() {
|
public void repaint() {
|
||||||
if (this.hodParent != null) {
|
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);
|
this.setBounds(x, y, w, h);
|
||||||
if (Boolean.TRUE.equals(this._visible)) {
|
if (Boolean.TRUE.equals(this._visible)) {
|
||||||
this.paintHODView(graphics);
|
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() {
|
public Color getHODBackground() {
|
||||||
return this.backgroundColor;
|
return this.backgroundColor;
|
||||||
@@ -85,11 +93,11 @@ public class HODPart extends Rectangle implements Serializable {
|
|||||||
this.foregroundColor = color;
|
this.foregroundColor = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Font getHODFont() {
|
public Object getHODFont() {
|
||||||
return this._hodFont;
|
return this._hodFont;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHODFont(Font font) {
|
public void setHODFont(Object font) {
|
||||||
this._hodFont = font;
|
this._hodFont = font;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
-9
@@ -1,12 +1,10 @@
|
|||||||
package haus.nightmare.lib3270j.graphics;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
import java.awt.Dimension;
|
import java.lang.reflect.Method;
|
||||||
import java.awt.Graphics;
|
|
||||||
import java.awt.Point;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Programmed Symbol Set manager facade matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODProgramSymbolManager).
|
* 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 class HODProgramSymbolManager {
|
||||||
public static final int MAX_HOD_SLOT = 254;
|
public static final int MAX_HOD_SLOT = 254;
|
||||||
@@ -51,14 +49,57 @@ public class HODProgramSymbolManager {
|
|||||||
delegate.loadProgrammedSymbolSet(bytes, 0, bytes.length);
|
delegate.loadProgrammedSymbolSet(bytes, 0, bytes.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void drawHODImageCharacter(Graphics g, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) {
|
public void drawHODImageCharacter(PixelBuffer pb, int lcid, int codepoint, Point pt, int colorIdx, int cellW, int cellH) {
|
||||||
if (g == null || pt == null) return;
|
if (pb == null || pt == null) return;
|
||||||
ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint);
|
ProgramSymbolSet.SymbolSlot slot = delegate.getSymbol(lcid, codepoint);
|
||||||
if (slot != null) {
|
if (slot != null) {
|
||||||
int fg = GocaConstants.getGocaColorArgb(colorIdx);
|
int fg = GocaConstants.getGocaColorArgb(colorIdx);
|
||||||
BufferedImage img = slot.getScaledImage(cellW, cellH, fg, 0);
|
PixelBuffer glyph = slot.getScaledPixelBuffer(cellW, cellH, fg, 0);
|
||||||
if (img != null) {
|
if (glyph != null) {
|
||||||
g.drawImage(img, pt.x, pt.y, 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;
|
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).
|
* 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;
|
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.
|
* Image filter that keys out a specific background color by setting its alpha to 0x00.
|
||||||
* Conforms to IBM Host On-Demand HODTransparentColorFilter.
|
* 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;
|
private int transparentRgb;
|
||||||
|
|
||||||
public HODTransparentColorFilter(int rgb) {
|
public HODTransparentColorFilter(int rgb) {
|
||||||
this.canFilterIndexColorModel = true;
|
|
||||||
this.transparentRgb = rgb & 0x00FFFFFF;
|
this.transparentRgb = rgb & 0x00FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,11 +26,26 @@ public class HODTransparentColorFilter extends RGBImageFilter {
|
|||||||
this.transparentRgb = rgb & 0x00FFFFFF;
|
this.transparentRgb = rgb & 0x00FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int filterRGB(int x, int y, int rgb) {
|
public int filterRGB(int x, int y, int rgb) {
|
||||||
if ((rgb & 0x00FFFFFF) == transparentRgb) {
|
if ((rgb & 0x00FFFFFF) == transparentRgb) {
|
||||||
return 0x00000000;
|
return 0x00000000;
|
||||||
}
|
}
|
||||||
return rgb;
|
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;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.lang.reflect.Method;
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wallpaper background manager matching IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODWallpaper).
|
* 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 class HODWallpaper extends HODPart {
|
||||||
public static final int HOD_TILE = 0;
|
public static final int HOD_TILE = 0;
|
||||||
@@ -17,8 +12,8 @@ public class HODWallpaper extends HODPart {
|
|||||||
public static final int HOD_STRETCH = 2;
|
public static final int HOD_STRETCH = 2;
|
||||||
|
|
||||||
private int _display = HOD_CENTER;
|
private int _display = HOD_CENTER;
|
||||||
private Image rawImage;
|
private Object rawImage;
|
||||||
private Image backgroundImage;
|
private Object backgroundImage;
|
||||||
|
|
||||||
public HODWallpaper() {
|
public HODWallpaper() {
|
||||||
this(HOD_CENTER);
|
this(HOD_CENTER);
|
||||||
@@ -28,7 +23,7 @@ public class HODWallpaper extends HODPart {
|
|||||||
this.setDisplay(displayMode);
|
this.setDisplay(displayMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HODWallpaper(Image image, int displayMode) {
|
public HODWallpaper(Object image, int displayMode) {
|
||||||
this(displayMode);
|
this(displayMode);
|
||||||
this.setImage(image);
|
this.setImage(image);
|
||||||
}
|
}
|
||||||
@@ -46,30 +41,40 @@ public class HODWallpaper extends HODPart {
|
|||||||
return this._display;
|
return this._display;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setImage(Image image) {
|
public void setImage(Object image) {
|
||||||
this.rawImage = image;
|
this.rawImage = image;
|
||||||
this.backgroundImage = null;
|
this.backgroundImage = null;
|
||||||
this.repaint();
|
this.repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Image getHODImage() {
|
public Object getHODImage() {
|
||||||
return this.rawImage;
|
return this.rawImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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.setBounds(x, y, w, h);
|
||||||
this.setHODParent(component);
|
this.setHODParent(component);
|
||||||
super.paint(component, graphics, x, y, w, h);
|
super.paint(component, graphics, x, y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void paintHODView(Graphics graphics) {
|
public void paint(PixelBuffer buffer, int x, int y, int w, int h) {
|
||||||
Image image = this.getHODImage();
|
this.setBounds(x, y, w, h);
|
||||||
int display = this.getDisplay();
|
super.paint(buffer, x, y, w, h);
|
||||||
Component component = this.getHODParent();
|
}
|
||||||
|
|
||||||
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;
|
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);
|
Dimension imgSize = getImageSize(image);
|
||||||
if (imgSize.width <= 0 || imgSize.height <= 0) return;
|
if (imgSize.width <= 0 || imgSize.height <= 0) return;
|
||||||
|
|
||||||
@@ -99,40 +180,86 @@ public class HODWallpaper extends HODPart {
|
|||||||
for (int i = 0; i < cols; i++) {
|
for (int i = 0; i < cols; i++) {
|
||||||
int curY = startY;
|
int curY = startY;
|
||||||
for (int j = 0; j < rows; j++) {
|
for (int j = 0; j < rows; j++) {
|
||||||
graphics.drawImage(image, curX, curY, this.getHODParent());
|
invokeDrawImage(graphics, image, curX, curY);
|
||||||
curY += imgSize.height;
|
curY += imgSize.height;
|
||||||
}
|
}
|
||||||
curX += imgSize.width;
|
curX += imgSize.width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void centerHODImage(Graphics graphics, Image image) {
|
protected void centerHODImage(Object graphics, Object image) {
|
||||||
Dimension imgSize = getImageSize(image);
|
Dimension imgSize = getImageSize(image);
|
||||||
if (imgSize.width <= 0 || imgSize.height <= 0) return;
|
if (imgSize.width <= 0 || imgSize.height <= 0) return;
|
||||||
|
|
||||||
Insets insets = this.getInsets();
|
Insets insets = this.getInsets();
|
||||||
int cx = this.x + insets.left + (this.width - imgSize.width) / 2;
|
int cx = this.x + insets.left + (this.width - imgSize.width) / 2;
|
||||||
int cy = this.y + insets.top + (this.height - imgSize.height) / 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();
|
Insets insets = this.getInsets();
|
||||||
int sx = this.x + insets.left;
|
int sx = this.x + insets.left;
|
||||||
int sy = this.y + insets.top;
|
int sy = this.y + insets.top;
|
||||||
int sw = this.width - (insets.left + insets.right);
|
int sw = this.width - (insets.left + insets.right);
|
||||||
int sh = this.height - (insets.top + insets.bottom);
|
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);
|
if (image == null) return new Dimension(0, 0);
|
||||||
Component c = this.getHODParent();
|
if (image instanceof PixelBuffer) {
|
||||||
int w = image.getWidth(c);
|
PixelBuffer pb = (PixelBuffer) image;
|
||||||
int h = image.getHeight(c);
|
return new Dimension(pb.getWidth(), pb.getHeight());
|
||||||
if (w <= 0 && image instanceof BufferedImage) {
|
}
|
||||||
w = ((BufferedImage) image).getWidth();
|
int w = 0;
|
||||||
h = ((BufferedImage) image).getHeight();
|
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));
|
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).
|
* 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).
|
* Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
|
||||||
|
* Decoupled from java.awt using PixelBuffer.
|
||||||
*/
|
*/
|
||||||
public class ProgramSymbolSet {
|
public class ProgramSymbolSet {
|
||||||
|
|
||||||
@@ -64,8 +65,8 @@ public class ProgramSymbolSet {
|
|||||||
private int[] cachedRgbArray;
|
private int[] cachedRgbArray;
|
||||||
private int cachedFgRgb = -1;
|
private int cachedFgRgb = -1;
|
||||||
private int cachedBgRgb = -1;
|
private int cachedBgRgb = -1;
|
||||||
private java.awt.image.BufferedImage cachedImage;
|
private PixelBuffer cachedPixelBuffer;
|
||||||
private java.awt.image.BufferedImage cachedScaledImage;
|
private PixelBuffer cachedScaledPixelBuffer;
|
||||||
private int cachedTargetW = 0;
|
private int cachedTargetW = 0;
|
||||||
private int cachedTargetH = 0;
|
private int cachedTargetH = 0;
|
||||||
private int cachedScaledFgRgb = -1;
|
private int cachedScaledFgRgb = -1;
|
||||||
@@ -103,23 +104,21 @@ public class ProgramSymbolSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
|
* Returns a PixelBuffer scaled directly to target dimensions (cellWidth x cellHeight).
|
||||||
* Enables unscaled 1:1 hardware blitting in Java2D.
|
|
||||||
*/
|
*/
|
||||||
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) {
|
if (targetW <= 0 || targetH <= 0) {
|
||||||
return getImage(fgArgb, bgArgb);
|
return getPixelBuffer(fgArgb, bgArgb);
|
||||||
}
|
}
|
||||||
if (targetW == width && targetH == height) {
|
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) {
|
&& cachedScaledFgRgb == fgArgb && cachedScaledBgRgb == bgArgb) {
|
||||||
return cachedScaledImage;
|
return cachedScaledPixelBuffer;
|
||||||
}
|
}
|
||||||
int[] srcRgb = getRgbPixels(fgArgb, bgArgb);
|
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 = new int[targetW * targetH];
|
||||||
int[] dstRgb = ((java.awt.image.DataBufferInt) scaled.getRaster().getDataBuffer()).getData();
|
|
||||||
|
|
||||||
for (int dy = 0; dy < targetH; dy++) {
|
for (int dy = 0; dy < targetH; dy++) {
|
||||||
int sy = dy * height / targetH;
|
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.cachedTargetW = targetW;
|
||||||
this.cachedTargetH = targetH;
|
this.cachedTargetH = targetH;
|
||||||
this.cachedScaledFgRgb = fgArgb;
|
this.cachedScaledFgRgb = fgArgb;
|
||||||
this.cachedScaledBgRgb = bgArgb;
|
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.
|
* Computes and returns the cached PixelBuffer for this symbol glyph.
|
||||||
* Eliminates per-cell heap allocations during high frame rate rendering.
|
|
||||||
*/
|
*/
|
||||||
public synchronized java.awt.image.BufferedImage getImage(int fgArgb, int bgArgb) {
|
public synchronized PixelBuffer getPixelBuffer(int fgArgb, int bgArgb) {
|
||||||
if (cachedImage != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
if (cachedPixelBuffer != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
||||||
return cachedImage;
|
return cachedPixelBuffer;
|
||||||
}
|
}
|
||||||
int[] rgb = getRgbPixels(fgArgb, bgArgb);
|
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 = new int[rgb.length];
|
||||||
int[] imgData = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData();
|
|
||||||
System.arraycopy(rgb, 0, imgData, 0, rgb.length);
|
System.arraycopy(rgb, 0, imgData, 0, rgb.length);
|
||||||
this.cachedImage = img;
|
this.cachedPixelBuffer = new DefaultPixelBuffer(width, height, imgData);
|
||||||
this.cachedFgRgb = fgArgb;
|
this.cachedFgRgb = fgArgb;
|
||||||
this.cachedBgRgb = bgArgb;
|
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).
|
* Build inbound 3270 Read Modified data stream (AID + Cursor + SBA + Modified fields).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,4 +14,7 @@ public interface ConnectionListener {
|
|||||||
|
|
||||||
/** Called when TN3270E negotiation completes. */
|
/** Called when TN3270E negotiation completes. */
|
||||||
default void onTN3270ENegotiated(String deviceType, String deviceName) {}
|
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). */
|
/** Called when the screen size changes (erase/write vs erase/write alternate). */
|
||||||
default void onScreenSizeChanged(int rows, int cols) {}
|
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() {
|
private void notifyScreenUpdated() {
|
||||||
|
if (screenBuffer != null) {
|
||||||
|
screenBuffer.notifyScreenUpdate();
|
||||||
|
}
|
||||||
for (ScreenUpdateListener l : screenListeners) {
|
for (ScreenUpdateListener l : screenListeners) {
|
||||||
l.onScreenUpdated();
|
l.onScreenUpdated();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import haus.nightmare.lib3270j.charset.EbcdicTranslator;
|
|||||||
import haus.nightmare.lib3270j.ecl.ECLField;
|
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||||
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
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.*;
|
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,6 +65,61 @@ public class ScreenBuffer {
|
|||||||
private final EbcdicTranslator translator;
|
private final EbcdicTranslator translator;
|
||||||
private final Object renderLock = new Object();
|
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() {
|
public Object getRenderLock() {
|
||||||
return renderLock;
|
return renderLock;
|
||||||
}
|
}
|
||||||
@@ -272,10 +332,17 @@ public class ScreenBuffer {
|
|||||||
|
|
||||||
// ========== Cursor ==========
|
// ========== Cursor ==========
|
||||||
public int getCursorAddress() { return cursorAddress; }
|
public int getCursorAddress() { return cursorAddress; }
|
||||||
public synchronized void setCursorAddress(int addr) {
|
public void setCursorAddress(int addr) {
|
||||||
|
int oldAddr;
|
||||||
|
synchronized (this) {
|
||||||
|
oldAddr = this.cursorAddress;
|
||||||
this.cursorAddress = addr;
|
this.cursorAddress = addr;
|
||||||
this.displayCursorAddress = addr;
|
this.displayCursorAddress = addr;
|
||||||
}
|
}
|
||||||
|
if (oldAddr != addr) {
|
||||||
|
notifyCursorMoved(oldAddr, addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
public synchronized void setCursorPosition(int row, int col) {
|
public synchronized void setCursorPosition(int row, int col) {
|
||||||
int r = Math.max(0, Math.min(row, rows - 1));
|
int r = Math.max(0, Math.min(row, rows - 1));
|
||||||
int c = Math.max(0, Math.min(col, cols - 1));
|
int c = Math.max(0, Math.min(col, cols - 1));
|
||||||
@@ -831,19 +898,28 @@ public class ScreenBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void setText(String text) {
|
public synchronized void setText(String text) {
|
||||||
|
setText(text, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setText(String text, int pos) {
|
||||||
if (text == null) return;
|
if (text == null) return;
|
||||||
int size = rows * cols;
|
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++) {
|
for (int i = 0; i < len; i++) {
|
||||||
char ch = text.charAt(i);
|
char ch = text.charAt(i);
|
||||||
int ebc = translator.unicodeToEbcdic(ch);
|
int ebc = translator.unicodeToEbcdic(ch);
|
||||||
buffer[i].ec = (byte) (ebc >= 0 ? ebc : 0);
|
buffer[pos + i].ec = (byte) (ebc >= 0 ? ebc : 0);
|
||||||
buffer[i].ucs4 = ch;
|
buffer[pos + i].ucs4 = ch;
|
||||||
}
|
}
|
||||||
screenChanged = true;
|
screenChanged = true;
|
||||||
updateDisplaySnapshot();
|
updateDisplaySnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setText(String text, int row, int col) {
|
||||||
|
setText(text, row * cols + col);
|
||||||
|
}
|
||||||
|
|
||||||
public int searchString(String target) {
|
public int searchString(String target) {
|
||||||
if (target == null || target.isEmpty()) return -1;
|
if (target == null || target.isEmpty()) return -1;
|
||||||
String full = getText();
|
String full = getText();
|
||||||
@@ -1119,8 +1195,10 @@ public class ScreenBuffer {
|
|||||||
ExtendedAttribute[] wordCells = new ExtendedAttribute[wordLen];
|
ExtendedAttribute[] wordCells = new ExtendedAttribute[wordLen];
|
||||||
for (int i = 0; i < wordLen; i++) {
|
for (int i = 0; i < wordLen; i++) {
|
||||||
wordCells[i] = new ExtendedAttribute();
|
wordCells[i] = new ExtendedAttribute();
|
||||||
wordCells[i].copyFrom(getCell(wordStartAddr + i));
|
ExtendedAttribute srcCell = getCell(wordStartAddr + i);
|
||||||
getCell(wordStartAddr + i).clear();
|
wordCells[i].copyFrom(srcCell);
|
||||||
|
srcCell.ec = 0;
|
||||||
|
srcCell.ucs4 = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int nextRow = (curRow + 1) % rows;
|
int nextRow = (curRow + 1) % rows;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import haus.nightmare.lib3270j.Telnet3270Client;
|
|||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.*;
|
import java.net.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
@@ -28,6 +30,10 @@ public class TelnetConnection {
|
|||||||
private final TelnetFSM fsm;
|
private final TelnetFSM fsm;
|
||||||
private final ConnectionConfig config;
|
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;
|
private javax.net.ssl.SSLSession sslSession;
|
||||||
|
|
||||||
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
|
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
|
||||||
@@ -91,6 +97,7 @@ public class TelnetConnection {
|
|||||||
if (config.getSoTimeoutMs() > 0) {
|
if (config.getSoTimeoutMs() > 0) {
|
||||||
rawSocket.setSoTimeout(config.getSoTimeoutMs());
|
rawSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||||
}
|
}
|
||||||
|
applyExtendedSocketOptions(rawSocket);
|
||||||
rawSocket.connect(new InetSocketAddress(connectHost, connectPort), config.getConnectTimeoutMs());
|
rawSocket.connect(new InetSocketAddress(connectHost, connectPort), config.getConnectTimeoutMs());
|
||||||
|
|
||||||
// Perform proxy handshake if configured
|
// Perform proxy handshake if configured
|
||||||
@@ -123,6 +130,7 @@ public class TelnetConnection {
|
|||||||
if (config.getSoTimeoutMs() > 0) {
|
if (config.getSoTimeoutMs() > 0) {
|
||||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||||
}
|
}
|
||||||
|
applyExtendedSocketOptions(sslSocket);
|
||||||
applyTlsSocketSettings(sslSocket);
|
applyTlsSocketSettings(sslSocket);
|
||||||
sslSocket.startHandshake();
|
sslSocket.startHandshake();
|
||||||
socket = sslSocket;
|
socket = sslSocket;
|
||||||
@@ -143,10 +151,13 @@ public class TelnetConnection {
|
|||||||
|
|
||||||
log.info("Connected to " + socket.getRemoteSocketAddress());
|
log.info("Connected to " + socket.getRemoteSocketAddress());
|
||||||
|
|
||||||
|
intentionalDisconnect = false;
|
||||||
|
lastActivityTime.set(System.currentTimeMillis());
|
||||||
running = true;
|
running = true;
|
||||||
readerThread = new Thread(this::readLoop, "TN3270-Reader");
|
readerThread = new Thread(this::readLoop, "TN3270-Reader");
|
||||||
readerThread.setDaemon(true);
|
readerThread.setDaemon(true);
|
||||||
readerThread.start();
|
readerThread.start();
|
||||||
|
startKeepAlive();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,6 +178,7 @@ public class TelnetConnection {
|
|||||||
if (config.getSoTimeoutMs() > 0) {
|
if (config.getSoTimeoutMs() > 0) {
|
||||||
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
sslSocket.setSoTimeout(config.getSoTimeoutMs());
|
||||||
}
|
}
|
||||||
|
applyExtendedSocketOptions(sslSocket);
|
||||||
applyTlsSocketSettings(sslSocket);
|
applyTlsSocketSettings(sslSocket);
|
||||||
sslSocket.startHandshake();
|
sslSocket.startHandshake();
|
||||||
this.socket = sslSocket;
|
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 {
|
private void establishHttpProxy(Socket s, String targetHost, int targetPort, String user, String pass) throws IOException {
|
||||||
OutputStream out = s.getOutputStream();
|
OutputStream out = s.getOutputStream();
|
||||||
InputStream in = s.getInputStream();
|
InputStream in = s.getInputStream();
|
||||||
@@ -411,6 +523,7 @@ public class TelnetConnection {
|
|||||||
if (outputStream == null) return;
|
if (outputStream == null) return;
|
||||||
outputStream.write(data, offset, length);
|
outputStream.write(data, offset, length);
|
||||||
outputStream.flush();
|
outputStream.flush();
|
||||||
|
lastActivityTime.set(System.currentTimeMillis());
|
||||||
if (log.isLoggable(Level.FINE)) {
|
if (log.isLoggable(Level.FINE)) {
|
||||||
log.fine("SENT " + length + " bytes: " + formatHex(data, offset, length));
|
log.fine("SENT " + length + " bytes: " + formatHex(data, offset, length));
|
||||||
}
|
}
|
||||||
@@ -432,13 +545,16 @@ public class TelnetConnection {
|
|||||||
byte[] escaped = out.toByteArray();
|
byte[] escaped = out.toByteArray();
|
||||||
outputStream.write(escaped, 0, escaped.length);
|
outputStream.write(escaped, 0, escaped.length);
|
||||||
outputStream.flush();
|
outputStream.flush();
|
||||||
|
lastActivityTime.set(System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnect from the host.
|
* Disconnect from the host.
|
||||||
*/
|
*/
|
||||||
public void disconnect() {
|
public void disconnect() {
|
||||||
|
intentionalDisconnect = true;
|
||||||
running = false;
|
running = false;
|
||||||
|
stopKeepAlive();
|
||||||
try {
|
try {
|
||||||
if (socket != null && !socket.isClosed()) {
|
if (socket != null && !socket.isClosed()) {
|
||||||
socket.shutdownInput();
|
socket.shutdownInput();
|
||||||
@@ -471,10 +587,12 @@ public class TelnetConnection {
|
|||||||
int n = inputStream.read(buf);
|
int n = inputStream.read(buf);
|
||||||
if (n < 0) {
|
if (n < 0) {
|
||||||
log.info("Host disconnected (EOF)");
|
log.info("Host disconnected (EOF)");
|
||||||
fsm.onDisconnect();
|
boolean unexpected = !intentionalDisconnect;
|
||||||
|
fsm.onDisconnect(unexpected);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
|
lastActivityTime.set(System.currentTimeMillis());
|
||||||
if (log.isLoggable(Level.FINE)) {
|
if (log.isLoggable(Level.FINE)) {
|
||||||
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
|
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) {
|
} catch (SocketException e) {
|
||||||
if (running) {
|
if (running && !intentionalDisconnect) {
|
||||||
log.info("Socket closed: " + e.getMessage());
|
log.info("Socket closed unexpectedly: " + e.getMessage());
|
||||||
fsm.onDisconnect();
|
fsm.onDisconnect(true);
|
||||||
|
} else if (running) {
|
||||||
|
fsm.onDisconnect(false);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (running) {
|
if (running && !intentionalDisconnect) {
|
||||||
log.log(Level.WARNING, "Read error", e);
|
log.log(Level.WARNING, "Read error: " + e.getMessage());
|
||||||
|
fsm.onDisconnect(true);
|
||||||
|
} else if (running) {
|
||||||
fsm.onError("Read error: " + e.getMessage());
|
fsm.onError("Read error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
@@ -501,6 +628,8 @@ public class TelnetConnection {
|
|||||||
log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t);
|
log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t);
|
||||||
fsm.onError("Network loop error: " + t.getMessage());
|
fsm.onError("Network loop error: " + t.getMessage());
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
stopKeepAlive();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,11 @@ public class TelnetFSM {
|
|||||||
private String connectedLu;
|
private String connectedLu;
|
||||||
private String connectedType;
|
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 enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
|
||||||
|
|
||||||
public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) {
|
public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) {
|
||||||
@@ -190,7 +195,7 @@ public class TelnetFSM {
|
|||||||
eFuncs[FUNC_SYSREQ] = true;
|
eFuncs[FUNC_SYSREQ] = true;
|
||||||
eFuncs[FUNC_SNA_SENSE] = true;
|
eFuncs[FUNC_SNA_SENSE] = true;
|
||||||
eFuncs[FUNC_DATA_STREAM_CTL] = true;
|
eFuncs[FUNC_DATA_STREAM_CTL] = true;
|
||||||
eFuncs[FUNC_CONTENTION_RESOLUTION] = true;
|
eFuncs[FUNC_CONTENTION_RESOLUTION] = negotiateContentionResolution;
|
||||||
|
|
||||||
statusDisplay(STATUS_CONNECTING, "Connecting to host");
|
statusDisplay(STATUS_CONNECTING, "Connecting to host");
|
||||||
changeState(ConnectionState.TELNET_PENDING);
|
changeState(ConnectionState.TELNET_PENDING);
|
||||||
@@ -951,6 +956,10 @@ public class TelnetFSM {
|
|||||||
log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames());
|
log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames());
|
||||||
log.info("TN3270E negotiation complete");
|
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
|
// RFC 2355: If BIND-IMAGE function is not negotiated, the emulation session is considered
|
||||||
// to be bound immediately upon completion of the FUNCTIONS negotiation.
|
// to be bound immediately upon completion of the FUNCTIONS negotiation.
|
||||||
if (eFuncs[FUNC_BIND_IMAGE]) {
|
if (eFuncs[FUNC_BIND_IMAGE]) {
|
||||||
@@ -963,6 +972,7 @@ public class TelnetFSM {
|
|||||||
// Notify listeners
|
// Notify listeners
|
||||||
for (ConnectionListener l : connectionListeners) {
|
for (ConnectionListener l : connectionListeners) {
|
||||||
l.onTN3270ENegotiated(connectedType, connectedLu);
|
l.onTN3270ENegotiated(connectedType, connectedLu);
|
||||||
|
l.onTN3270EFunctionsNegotiated(eFuncs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,9 +1006,59 @@ public class TelnetFSM {
|
|||||||
processTN3270ERecord(data);
|
processTN3270ERecord(data);
|
||||||
} else {
|
} else {
|
||||||
// Plain TN3270 mode: data is raw 3270 data stream
|
// 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();
|
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) {
|
public void processTn3270eHeader(byte[] data) {
|
||||||
@@ -1016,6 +1076,12 @@ public class TelnetFSM {
|
|||||||
int responseFlag = data[2] & 0xFF;
|
int responseFlag = data[2] & 0xFF;
|
||||||
int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 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) +
|
log.fine("TN3270E header: type=" + TN3270EConstants.dataTypeName(dataType) +
|
||||||
" rqf=" + requestFlag + " rsf=" + responseFlag + " seq=" + seqNumber);
|
" rqf=" + requestFlag + " rsf=" + responseFlag + " seq=" + seqNumber);
|
||||||
|
|
||||||
@@ -1033,7 +1099,7 @@ public class TelnetFSM {
|
|||||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true);
|
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, false);
|
||||||
notifyScreenUpdate();
|
notifyScreenUpdate();
|
||||||
// Send positive response if required
|
// Send positive response if required
|
||||||
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
|
||||||
@@ -1423,9 +1489,50 @@ public class TelnetFSM {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setConnectionState(ConnectionState newState) {
|
||||||
|
changeState(newState);
|
||||||
|
}
|
||||||
|
|
||||||
public void onDisconnect() {
|
public void onDisconnect() {
|
||||||
|
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);
|
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) {
|
public void onError(String message) {
|
||||||
log.warning("Error: " + message);
|
log.warning("Error: " + message);
|
||||||
@@ -1434,7 +1541,10 @@ public class TelnetFSM {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyScreenUpdate() {
|
public void notifyScreenUpdate() {
|
||||||
|
if (screenBuffer != null) {
|
||||||
|
screenBuffer.notifyScreenUpdate();
|
||||||
|
}
|
||||||
for (ScreenUpdateListener l : screenListeners) {
|
for (ScreenUpdateListener l : screenListeners) {
|
||||||
l.onScreenUpdated();
|
l.onScreenUpdated();
|
||||||
}
|
}
|
||||||
@@ -1810,4 +1920,41 @@ public class TelnetFSM {
|
|||||||
public boolean isFunctionNegotiated(int func) {
|
public boolean isFunctionNegotiated(int func) {
|
||||||
return func >= 0 && func < eFuncs.length && eFuncs[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 suppressClearEvent;
|
||||||
public boolean wsfvalid = true;
|
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
|
// Underlying lib3270j data stream processor
|
||||||
private final DataStreamProcessor delegate;
|
private final DataStreamProcessor delegate;
|
||||||
private ECLSession session;
|
private ECLSession session;
|
||||||
@@ -172,9 +180,15 @@ public class DS3270 {
|
|||||||
public DS3270(ECLSession session, ECLPS ps) {
|
public DS3270(ECLSession session, ECLPS ps) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.ps = ps;
|
this.ps = ps;
|
||||||
|
if (session != null) {
|
||||||
|
this.autoSysUnlock = session.isAutoSysUnlock();
|
||||||
|
}
|
||||||
ScreenBuffer sb = (ps != null) ? ps.getScreenBuffer() : new ScreenBuffer();
|
ScreenBuffer sb = (ps != null) ? ps.getScreenBuffer() : new ScreenBuffer();
|
||||||
EbcdicTranslator trans = (ps != null) ? ps.getTranslator() : new EbcdicTranslator();
|
EbcdicTranslator trans = (ps != null) ? ps.getTranslator() : new EbcdicTranslator();
|
||||||
this.delegate = new DataStreamProcessor(sb, trans);
|
this.delegate = new DataStreamProcessor(sb, trans);
|
||||||
|
if (session != null) {
|
||||||
|
this.delegate.setAutoSysUnlock(session.isAutoSysUnlock());
|
||||||
|
}
|
||||||
if (ps != null && ps.getInputProcessor() != null) {
|
if (ps != null && ps.getInputProcessor() != null) {
|
||||||
this.delegate.setInputProcessor(ps.getInputProcessor());
|
this.delegate.setInputProcessor(ps.getInputProcessor());
|
||||||
}
|
}
|
||||||
@@ -214,8 +228,135 @@ public class DS3270 {
|
|||||||
return false;
|
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() {
|
public void endOfRecord() {
|
||||||
log.fine("DS3270 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) {
|
public int receiveData(short[] sArray, int off, int len) {
|
||||||
@@ -233,6 +374,10 @@ public class DS3270 {
|
|||||||
|
|
||||||
public void processWCC(short wcc) {
|
public void processWCC(short wcc) {
|
||||||
delegate.processWCC(wcc);
|
delegate.processWCC(wcc);
|
||||||
|
if ((wcc & WCC_RESTORE) > 0) {
|
||||||
|
this.unlock_pending = true;
|
||||||
|
this.unlock_sys_pending = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void processSBA(int baddr) {
|
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.input.InputProcessor;
|
||||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||||
|
|
||||||
import java.awt.Image;
|
import haus.nightmare.lib3270j.graphics.DefaultPixelBuffer;
|
||||||
import java.awt.Rectangle;
|
import haus.nightmare.lib3270j.graphics.PixelBuffer;
|
||||||
import java.awt.image.BufferedImage;
|
import haus.nightmare.lib3270j.graphics.Rectangle;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@@ -406,11 +406,12 @@ public class Phase5EclEventTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test with image and rectangle
|
// 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);
|
Rectangle rect = new Rectangle(0, 0, 32, 32);
|
||||||
ECLPSGraphicsEvent fullGEvent = new ECLPSGraphicsEvent(ps, ECLPSGraphicsEvent.GRAPHICS_UPDATED, testImage, rect);
|
ECLPSGraphicsEvent fullGEvent = new ECLPSGraphicsEvent(ps, ECLPSGraphicsEvent.GRAPHICS_UPDATED, testImage, rect);
|
||||||
assertSame(testImage, fullGEvent.GetImage());
|
assertSame(testImage, fullGEvent.GetImage());
|
||||||
assertSame(testImage, fullGEvent.getImage());
|
assertSame(testImage, fullGEvent.getImage());
|
||||||
|
assertSame(testImage, fullGEvent.getPixelBuffer());
|
||||||
assertEquals(rect, fullGEvent.GetRectangle());
|
assertEquals(rect, fullGEvent.GetRectangle());
|
||||||
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;
|
package haus.nightmare.lib3270j.graphics;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import java.awt.Point;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package haus.nightmare.lib3270j.graphics;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.awt.Point;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
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.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
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.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
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.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,7 +193,7 @@ public class Phase2GocaEngineTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("Test HODWallpaper Tile, Center, and Stretch")
|
@DisplayName("Test HODWallpaper Tile, Center, and Stretch")
|
||||||
public void testHODWallpaper() {
|
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);
|
HODWallpaper wp = new HODWallpaper(img, HODWallpaper.HOD_CENTER);
|
||||||
assertEquals(HODWallpaper.HOD_CENTER, wp.getDisplay());
|
assertEquals(HODWallpaper.HOD_CENTER, wp.getDisplay());
|
||||||
|
|
||||||
@@ -211,10 +203,8 @@ public class Phase2GocaEngineTest {
|
|||||||
wp.setDisplay(HODWallpaper.HOD_STRETCH);
|
wp.setDisplay(HODWallpaper.HOD_STRETCH);
|
||||||
assertEquals(HODWallpaper.HOD_STRETCH, wp.getDisplay());
|
assertEquals(HODWallpaper.HOD_STRETCH, wp.getDisplay());
|
||||||
|
|
||||||
BufferedImage canvas = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
|
PixelBuffer canvas = new DefaultPixelBuffer(100, 100);
|
||||||
Graphics g = canvas.getGraphics();
|
wp.paint(canvas, 0, 0, 100, 100);
|
||||||
wp.paint(new java.awt.Canvas(), g, 0, 0, 100, 100);
|
|
||||||
g.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -283,8 +273,9 @@ public class Phase2GocaEngineTest {
|
|||||||
assertEquals(101, bounds.width);
|
assertEquals(101, bounds.width);
|
||||||
assertEquals(101, bounds.height);
|
assertEquals(101, bounds.height);
|
||||||
|
|
||||||
Image img = fa.getImage();
|
Object img = fa.getImage();
|
||||||
assertNotNull(img);
|
assertNotNull(img);
|
||||||
|
assertNotNull(fa.getPixelBuffer());
|
||||||
fa.dispose();
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
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