7 Commits

Author SHA1 Message Date
rudi aa724fbcef Hotspot detection and screen buffer
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m20s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m28s
Release j3270 / Build & Publish Release (push) Successful in 1m39s
2026-09-18 18:18:53 +00:00
rudi 8a30efe9c0 Add bugfix for ATTN
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 35s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m36s
2026-09-18 17:33:58 +00:00
rudi 5049d8df74 Add facade classes for 3rd party programs
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m15s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m18s
2026-09-18 16:48:49 +00:00
rudi badd026055 Cleanup datastream and Graphics
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m13s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m34s
2026-09-18 00:55:54 +00:00
rudi ef70ea0f5a Bugfixes from zOS testing
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 37s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m16s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m52s
2026-09-17 23:09:43 +00:00
rudi 30b1a77b61 Datastream bug fixes w/ not recognizing bytes
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 33s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m9s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m10s
Release j3270 / Build & Publish Release (push) Successful in 1m11s
2026-09-16 21:08:22 +00:00
rudi 9bd1676ec1 Fix setting storage, and add proxy options like a3270
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 34s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m11s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m17s
2026-09-16 14:43:30 +00:00
146 changed files with 19786 additions and 850 deletions
@@ -42,8 +42,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// Dialog instances
private FindDialog findDialog;
private ScriptDialog scriptDialog;
private MacroDialog macroDialog;
private FieldInspectorDialog fieldInspectorDialog;
private PrinterSessionDialog printerSessionDialog;
private ScreenHistoryDialog screenHistoryDialog;
// Modes menu items
private JCheckBoxMenuItem docModeItem;
@@ -94,6 +96,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (printerSessionDialog != null) {
printerSessionDialog.dispose();
}
if (screenHistoryDialog != null) {
screenHistoryDialog.dispose();
}
terminalPanel.dispose();
}
@@ -104,6 +109,29 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}
}
});
// Ensure clicking anywhere inside the frame brings focus to window and terminalPanel
Toolkit.getDefaultToolkit().addAWTEventListener(event -> {
if (event instanceof MouseEvent && event.getID() == MouseEvent.MOUSE_PRESSED) {
MouseEvent me = (MouseEvent) event;
Object src = me.getSource();
if (src instanceof Component) {
Window win = SwingUtilities.getWindowAncestor((Component) src);
if (win == J3270App.this && !isAnyChildDialogActive()) {
if (!J3270App.this.isFocused() || !J3270App.this.isActive()) {
J3270App.this.toFront();
J3270App.this.requestFocus();
}
boolean isMenu = (src instanceof JMenuBar) || (src instanceof JMenuItem) || (src instanceof JPopupMenu)
|| SwingUtilities.getAncestorOfClass(JMenuBar.class, (Component) src) != null
|| SwingUtilities.getAncestorOfClass(JPopupMenu.class, (Component) src) != null;
if (!isMenu && terminalPanel != null && !terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow();
}
}
}
}
}, AWTEvent.MOUSE_EVENT_MASK);
}
private boolean isAnyChildDialogActive() {
@@ -113,8 +141,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}
if (findDialog != null && findDialog.isVisible()) return true;
if (scriptDialog != null && scriptDialog.isVisible()) return true;
if (macroDialog != null && macroDialog.isVisible()) return true;
if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
if (screenHistoryDialog != null && screenHistoryDialog.isVisible()) return true;
for (Window owned : getOwnedWindows()) {
if (owned != null && owned.isVisible()) {
return true;
@@ -181,6 +211,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// 2. Edit menu
JMenu editMenu = createMenu("Edit");
editMenu.add(createMenuItem("Cut", KeyEvent.VK_X, () -> terminalPanel.cutSelection()));
editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection()));
editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard()));
editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog));
@@ -312,6 +343,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
viewMenu.add(gfxMenu);
viewMenu.addSeparator();
viewMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
menuBar.add(viewMenu);
@@ -430,6 +462,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}, true));
actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
actionsMenu.add(createMenuItem("Macro Runtime (<HASCRIPT>)...", KeyEvent.VK_M, this::showMacroDialog, true));
actionsMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
menuBar.add(actionsMenu);
@@ -536,6 +570,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
scriptDialog.setVisible(true);
}
private void showMacroDialog() {
if (macroDialog == null) {
macroDialog = new MacroDialog(this, client, terminalPanel);
}
macroDialog.setVisible(true);
}
private void showFieldInspectorDialog() {
if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
@@ -559,6 +600,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
printerSessionDialog.setVisible(true);
}
private void showScreenHistoryDialog() {
if (screenHistoryDialog == null) {
screenHistoryDialog = new ScreenHistoryDialog(this, client);
} else {
if (client != null && client.getScreenHistoryBuffer() != null) {
screenHistoryDialog.refreshHistoryView(client.getScreenHistoryBuffer().size() - 1);
}
}
screenHistoryDialog.setVisible(true);
screenHistoryDialog.toFront();
}
private void showPasteLineWrapDialog() {
if (client == null || !client.getConnectionState().isFullSession()) {
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE);
@@ -694,6 +747,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
dialog.setVisible(true);
if (dialog.isConfirmed()) {
@@ -730,6 +784,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
if (config.getGraphicsMode() != null) {
haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode());
}
disconnect();
@@ -773,6 +830,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
fileTransfer.cancel();
fileTransfer = null;
}
if (macroDialog != null) {
macroDialog.stopMacro();
}
Telnet3270Client c = client;
client = null;
new Thread(() -> {
@@ -1070,8 +1130,47 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}
}
// ========== Main ==========
private static void applyProxyUrl(ConnectionConfig config, String proxyUrl) {
if (config == null || proxyUrl == null || proxyUrl.trim().isEmpty()) return;
try {
java.net.URI uri = new java.net.URI(proxyUrl.trim());
String scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase() : "http";
ConnectionConfig.ProxyType pType;
int pPort;
if (scheme.equals("http") || scheme.equals("https")) {
pType = ConnectionConfig.ProxyType.HTTP;
pPort = uri.getPort() > 0 ? uri.getPort() : 8080;
} else if (scheme.equals("socks4") || scheme.equals("socks4a")) {
pType = ConnectionConfig.ProxyType.SOCKS4;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else if (scheme.equals("socks5") || scheme.equals("socks")) {
pType = ConnectionConfig.ProxyType.SOCKS5;
pPort = uri.getPort() > 0 ? uri.getPort() : 1080;
} else {
return;
}
String pHost = uri.getHost();
String pUser = null;
String pPass = null;
String userInfo = uri.getUserInfo();
if (userInfo != null) {
int colon = userInfo.indexOf(':');
if (colon >= 0) {
pUser = userInfo.substring(0, colon);
pPass = userInfo.substring(colon + 1);
} else {
pUser = userInfo;
}
}
if (pHost != null && !pHost.isEmpty()) {
config.setProxy(pType, pHost, pPort, pUser, pPass);
}
} catch (Exception e) {
log.warning("Failed to parse CLI proxy URL: " + proxyUrl);
}
}
// ========== Main ==========
public static void main(String[] args) {
boolean debug = false;
@@ -1081,6 +1180,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
Boolean cliAutoSysUnlock = null;
GraphicsMode cliGraphicsMode = null;
String configFile = null;
String cliProxyUrl = null;
String cliProfileName = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) {
@@ -1107,6 +1208,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
cliGraphicsMode = GraphicsMode.NONE;
} else if (("-c".equals(arg) || "--config".equals(arg)) && i + 1 < args.length) {
configFile = args[++i];
} else if (arg.startsWith("--proxy=") || arg.startsWith("-proxy=")) {
cliProxyUrl = arg.substring(arg.indexOf('=') + 1).trim();
} else if (("--proxy".equals(arg) || "-proxy".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliProxyUrl = args[++i].trim();
} else if (arg.startsWith("--profile=")) {
cliProfileName = arg.substring(arg.indexOf('=') + 1).trim();
} else if ("--profile".equals(arg) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliProfileName = args[++i].trim();
} else if (arg.startsWith("-")) {
System.err.println("Unknown option: " + arg);
} else {
@@ -1143,11 +1252,33 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
final Boolean finalTn3270e = cliTn3270e;
final Boolean finalAutoSysUnlock = cliAutoSysUnlock;
final GraphicsMode finalGraphicsMode = cliGraphicsMode;
final String finalProxyUrl = cliProxyUrl;
final String finalProfileName = cliProfileName;
SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App();
app.setVisible(true);
if (finalProfileName != null) {
haus.nightmare.j3270.storage.SavedHost profile = haus.nightmare.j3270.storage.HostStorage.findByName(finalProfileName);
if (profile == null) {
profile = haus.nightmare.j3270.storage.HostStorage.findById(finalProfileName);
}
if (profile != null) {
ConnectionConfig config = profile.toConnectionConfig();
if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
if (finalAutoSysUnlock != null) config.setAutoSysUnlock(finalAutoSysUnlock);
if (finalGraphicsMode != null) config.setGraphicsMode(finalGraphicsMode);
if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl);
app.connect(config);
return;
} else {
System.err.println("Profile not found: " + finalProfileName);
}
}
if (!remainingArgs.isEmpty()) {
String hostArg = remainingArgs.get(0);
int port = finalTls ? 992 : 23;
@@ -1188,6 +1319,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
}
if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl);
}
app.connect(config);
} else {
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
@@ -1197,26 +1331,41 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
SwingUtilities.invokeLater(app::showConnectDialog);
break;
case AUTO_CONNECT:
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
boolean verify = haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert();
boolean tn3270e = haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e();
if (host != null && !host.isEmpty()) {
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
}
haus.nightmare.j3270.storage.SavedHost autoHost = haus.nightmare.j3270.storage.HostStorage.getAutoConnectHost();
if (autoHost != null) {
ConnectionConfig config = autoHost.toConnectionConfig();
if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
if (finalAutoSysUnlock != null) config.setAutoSysUnlock(finalAutoSysUnlock);
if (finalGraphicsMode != null) config.setGraphicsMode(finalGraphicsMode);
if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl);
app.connect(config);
} else {
SwingUtilities.invokeLater(app::showConnectDialog);
String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost();
int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort();
boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls();
boolean verify = haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert();
boolean tn3270e = haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e();
if (host != null && !host.isEmpty()) {
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls);
config.setTlsVerifyCert(verify);
config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e);
config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock());
if (finalGraphicsMode != null) {
config.setGraphicsMode(finalGraphicsMode);
} else {
config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
}
if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl);
}
app.connect(config);
} else {
SwingUtilities.invokeLater(app::showConnectDialog);
}
}
break;
case DO_NOTHING:
@@ -439,6 +439,33 @@ public class Settings {
flushPrefs();
}
public static boolean getTrimRectSizingHandles() {
return prefs.getBoolean("trimRectSizingHandles", false);
}
public static void setTrimRectSizingHandles(boolean val) {
prefs.putBoolean("trimRectSizingHandles", val);
flushPrefs();
}
public static boolean getTrimRectRemainAfterEdit() {
return prefs.getBoolean("trimRectRemainAfterEdit", false);
}
public static void setTrimRectRemainAfterEdit(boolean val) {
prefs.putBoolean("trimRectRemainAfterEdit", val);
flushPrefs();
}
public static boolean getPasteWordBreak() {
return prefs.getBoolean("pasteWordBreak", true);
}
public static void setPasteWordBreak(boolean val) {
prefs.putBoolean("pasteWordBreak", val);
flushPrefs();
}
// ========== Crosshair Ruler ==========
public static boolean getCrosshairRuler() {
@@ -449,6 +476,71 @@ public class Settings {
prefs.putBoolean("crosshairRuler", enabled);
}
// ========== Phase 25: Hotspots & Mouse Wheel Navigation ==========
public static boolean getHotspotsEnabled() {
return prefs.getBoolean("hotspotsEnabled", true);
}
public static void setHotspotsEnabled(boolean enabled) {
prefs.putBoolean("hotspotsEnabled", enabled);
flushPrefs();
}
public static boolean getHotspotsUrlEnabled() {
return prefs.getBoolean("hotspotsUrlEnabled", true);
}
public static void setHotspotsUrlEnabled(boolean enabled) {
prefs.putBoolean("hotspotsUrlEnabled", enabled);
flushPrefs();
}
public static boolean getHotspotsPfEnabled() {
return prefs.getBoolean("hotspotsPfEnabled", true);
}
public static void setHotspotsPfEnabled(boolean enabled) {
prefs.putBoolean("hotspotsPfEnabled", enabled);
flushPrefs();
}
public static boolean getHotspotsMenuChoicesEnabled() {
return prefs.getBoolean("hotspotsMenuChoicesEnabled", true);
}
public static void setHotspotsMenuChoicesEnabled(boolean enabled) {
prefs.putBoolean("hotspotsMenuChoicesEnabled", enabled);
flushPrefs();
}
public static boolean getMouseWheelEnabled() {
return prefs.getBoolean("mouseWheelEnabled", true);
}
public static void setMouseWheelEnabled(boolean enabled) {
prefs.putBoolean("mouseWheelEnabled", enabled);
flushPrefs();
}
public static String getMouseWheelUpKey() {
return prefs.get("mouseWheelUpKey", "[pf7]");
}
public static void setMouseWheelUpKey(String key) {
prefs.put("mouseWheelUpKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf7]");
flushPrefs();
}
public static String getMouseWheelDownKey() {
return prefs.get("mouseWheelDownKey", "[pf8]");
}
public static void setMouseWheelDownKey(String key) {
prefs.put("mouseWheelDownKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf8]");
flushPrefs();
}
// ========== Status Bar Visibility ==========
public static boolean getStatusBarVisible() {
@@ -685,6 +777,18 @@ public class Settings {
case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break;
case "trimRectSizingHandles":
case "sizingHandles":
setTrimRectSizingHandles(Boolean.parseBoolean(value));
break;
case "trimRectRemainAfterEdit":
case "remainAfterEdit":
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
break;
case "pasteWordBreak":
case "wordBreak":
setPasteWordBreak(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown behavior/connection key: " + key);
}
@@ -706,6 +810,18 @@ public class Settings {
case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break;
case "trimRectSizingHandles":
case "sizingHandles":
setTrimRectSizingHandles(Boolean.parseBoolean(value));
break;
case "trimRectRemainAfterEdit":
case "remainAfterEdit":
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
break;
case "pasteWordBreak":
case "wordBreak":
setPasteWordBreak(Boolean.parseBoolean(value));
break;
default:
log.warning("Unknown clipboard key: " + key);
}
@@ -840,6 +956,9 @@ public class Settings {
w.println("autoSysUnlock = " + getAutoSysUnlock());
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
w.println("trimRectSizingHandles = " + getTrimRectSizingHandles());
w.println("trimRectRemainAfterEdit = " + getTrimRectRemainAfterEdit());
w.println("pasteWordBreak = " + getPasteWordBreak());
w.println();
// [entryassist]
@@ -0,0 +1,433 @@
package haus.nightmare.j3270.storage;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
/**
* Storage manager for saved host profiles, maintaining 1:1 format compatibility with a3270.
* Persists profiles in Java user preferences with JSON serialization.
*/
public class HostStorage {
private static final Logger log = Logger.getLogger(HostStorage.class.getName());
private static final Preferences prefs = Preferences.userNodeForPackage(HostStorage.class);
public static final String KEY_SAVED_HOSTS = "saved_hosts";
public static synchronized List<SavedHost> getSavedHosts() {
String jsonStr = prefs.get(KEY_SAVED_HOSTS, null);
if (jsonStr == null || jsonStr.trim().isEmpty()) {
return new ArrayList<>();
}
try {
return parseHostsJson(jsonStr);
} catch (Exception e) {
log.log(Level.WARNING, "Failed to parse saved hosts JSON", e);
return new ArrayList<>();
}
}
public static synchronized void saveHost(SavedHost host) {
if (host == null) return;
List<SavedHost> current = new ArrayList<>(getSavedHosts());
int index = -1;
for (int i = 0; i < current.size(); i++) {
if (Objects.equals(current.get(i).getId(), host.getId())) {
index = i;
break;
}
}
// If this host is set to autoConnect, clear autoConnect on all others
SavedHost hostToSave = host.copy();
if (hostToSave.isAutoConnect()) {
for (int i = 0; i < current.size(); i++) {
current.get(i).setAutoConnect(false);
}
}
if (index >= 0) {
current.set(index, hostToSave);
} else {
current.add(hostToSave);
}
saveAll(current);
}
public static synchronized void deleteHost(String hostId) {
if (hostId == null) return;
List<SavedHost> current = new ArrayList<>(getSavedHosts());
current.removeIf(h -> Objects.equals(h.getId(), hostId));
saveAll(current);
}
public static synchronized SavedHost getAutoConnectHost() {
for (SavedHost h : getSavedHosts()) {
if (h.isAutoConnect()) {
return h;
}
}
return null;
}
public static synchronized SavedHost findById(String id) {
if (id == null) return null;
for (SavedHost h : getSavedHosts()) {
if (id.equals(h.getId())) {
return h;
}
}
return null;
}
public static synchronized SavedHost findByName(String name) {
if (name == null) return null;
for (SavedHost h : getSavedHosts()) {
if (name.equalsIgnoreCase(h.getName())) {
return h;
}
}
return null;
}
public static synchronized void saveAll(List<SavedHost> hosts) {
if (hosts == null) return;
String json = serializeHostsJson(hosts);
prefs.put(KEY_SAVED_HOSTS, json);
try {
prefs.flush();
} catch (Exception ignored) {
}
}
public static synchronized void clearAll() {
prefs.remove(KEY_SAVED_HOSTS);
try {
prefs.flush();
} catch (Exception ignored) {
}
}
// ================= JSON Serialization =================
public static String serializeHostsJson(List<SavedHost> hosts) {
StringBuilder sb = new StringBuilder();
sb.append("[\n");
for (int i = 0; i < hosts.size(); i++) {
SavedHost h = hosts.get(i);
sb.append(" ").append(serializeHostJson(h));
if (i < hosts.size() - 1) {
sb.append(",");
}
sb.append("\n");
}
sb.append("]");
return sb.toString();
}
public static String serializeHostJson(SavedHost h) {
StringBuilder sb = new StringBuilder();
sb.append("{");
appendField(sb, "id", h.getId(), true);
appendField(sb, "name", h.getName(), true);
appendField(sb, "host", h.getHost(), true);
appendField(sb, "port", h.getPort(), true);
appendField(sb, "model", h.getModel(), true);
appendField(sb, "dynamicRows", h.getDynamicRows(), true);
appendField(sb, "dynamicCols", h.getDynamicCols(), true);
appendField(sb, "luName", h.getLuName(), true);
appendField(sb, "autoConnect", h.isAutoConnect(), true);
appendField(sb, "hostType", h.getHostType(), true);
appendField(sb, "useTls", h.isUseTls(), true);
appendField(sb, "tlsVerifyCert", h.isTlsVerifyCert(), true);
appendField(sb, "tn3270e", h.isTn3270e(), true);
appendField(sb, "graphicsMode", h.getGraphicsMode(), true);
appendField(sb, "codePage", h.getCodePage(), true);
appendField(sb, "proxyType", h.getProxyType(), true);
appendField(sb, "proxyHost", h.getProxyHost(), true);
appendField(sb, "proxyPort", h.getProxyPort(), true);
appendField(sb, "proxyUsername", h.getProxyUsername(), true);
appendField(sb, "proxyPassword", h.getProxyPassword(), false);
sb.append("}");
return sb.toString();
}
private static void appendField(StringBuilder sb, String key, String val, boolean comma) {
sb.append("\"").append(key).append("\":\"").append(escapeJson(val != null ? val : "")).append("\"");
if (comma) sb.append(",");
}
private static void appendField(StringBuilder sb, String key, int val, boolean comma) {
sb.append("\"").append(key).append("\":").append(val);
if (comma) sb.append(",");
}
private static void appendField(StringBuilder sb, String key, boolean val, boolean comma) {
sb.append("\"").append(key).append("\":").append(val);
if (comma) sb.append(",");
}
private static String escapeJson(String s) {
if (s == null) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"': sb.append("\\\""); break;
case '\\': sb.append("\\\\"); break;
case '\b': sb.append("\\b"); break;
case '\f': sb.append("\\f"); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default:
if (c < 32) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
break;
}
}
return sb.toString();
}
// ================= JSON Deserialization =================
public static List<SavedHost> parseHostsJson(String json) {
List<SavedHost> list = new ArrayList<>();
if (json == null || json.trim().isEmpty()) return list;
SimpleJsonParser parser = new SimpleJsonParser(json);
Object root = parser.parseValue();
if (root instanceof List) {
for (Object item : (List<?>) root) {
if (item instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) item;
list.add(fromMap(map));
}
}
} else if (root instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) root;
list.add(fromMap(map));
}
return list;
}
public static SavedHost fromMap(Map<String, Object> map) {
SavedHost h = new SavedHost();
if (map.containsKey("id")) h.setId(String.valueOf(map.get("id")));
if (map.containsKey("name")) h.setName(String.valueOf(map.get("name")));
if (map.containsKey("host")) h.setHost(String.valueOf(map.get("host")));
if (map.containsKey("port")) h.setPort(parseInt(map.get("port"), 23));
if (map.containsKey("model")) h.setModel(parseInt(map.get("model"), 4));
if (map.containsKey("dynamicRows")) h.setDynamicRows(parseInt(map.get("dynamicRows"), 62));
if (map.containsKey("dynamicCols")) h.setDynamicCols(parseInt(map.get("dynamicCols"), 160));
if (map.containsKey("luName")) h.setLuName(String.valueOf(map.get("luName")));
if (map.containsKey("autoConnect")) h.setAutoConnect(parseBool(map.get("autoConnect"), false));
if (map.containsKey("hostType")) h.setHostType(String.valueOf(map.get("hostType")));
if (map.containsKey("useTls")) h.setUseTls(parseBool(map.get("useTls"), false));
if (map.containsKey("tlsVerifyCert")) h.setTlsVerifyCert(parseBool(map.get("tlsVerifyCert"), true));
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
if (map.containsKey("graphicsMode")) {
String gm = String.valueOf(map.get("graphicsMode"));
h.setGraphicsMode(gm);
}
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
if (map.containsKey("proxyType")) h.setProxyType(String.valueOf(map.get("proxyType")));
if (map.containsKey("proxyHost")) h.setProxyHost(String.valueOf(map.get("proxyHost")));
if (map.containsKey("proxyPort")) h.setProxyPort(parseInt(map.get("proxyPort"), 0));
if (map.containsKey("proxyUsername")) h.setProxyUsername(String.valueOf(map.get("proxyUsername")));
if (map.containsKey("proxyPassword")) h.setProxyPassword(String.valueOf(map.get("proxyPassword")));
return h;
}
private static int parseInt(Object obj, int def) {
if (obj == null) return def;
if (obj instanceof Number) return ((Number) obj).intValue();
try {
return Integer.parseInt(String.valueOf(obj).trim());
} catch (Exception e) {
return def;
}
}
private static boolean parseBool(Object obj, boolean def) {
if (obj == null) return def;
if (obj instanceof Boolean) return (Boolean) obj;
return Boolean.parseBoolean(String.valueOf(obj).trim());
}
/**
* Minimal pure-Java JSON parser for nested Maps, Lists, Strings, Numbers, and Booleans.
*/
static class SimpleJsonParser {
private final String src;
private int pos = 0;
SimpleJsonParser(String src) {
this.src = src;
}
Object parseValue() {
skipWhitespace();
if (pos >= src.length()) return null;
char c = src.charAt(pos);
if (c == '{') return parseObject();
if (c == '[') return parseArray();
if (c == '"') return parseString();
if (c == 't' || c == 'f') return parseBoolean();
if (c == 'n') return parseNull();
if (c == '-' || Character.isDigit(c)) return parseNumber();
throw new IllegalArgumentException("Unexpected char '" + c + "' at position " + pos);
}
private Map<String, Object> parseObject() {
match('{');
Map<String, Object> map = new LinkedHashMap<>();
skipWhitespace();
if (peek() == '}') {
pos++;
return map;
}
while (true) {
skipWhitespace();
String key = parseString();
skipWhitespace();
match(':');
Object val = parseValue();
map.put(key, val);
skipWhitespace();
char c = peek();
if (c == ',') {
pos++;
} else if (c == '}') {
pos++;
break;
} else {
break;
}
}
return map;
}
private List<Object> parseArray() {
match('[');
List<Object> list = new ArrayList<>();
skipWhitespace();
if (peek() == ']') {
pos++;
return list;
}
while (true) {
Object val = parseValue();
list.add(val);
skipWhitespace();
char c = peek();
if (c == ',') {
pos++;
} else if (c == ']') {
pos++;
break;
} else {
break;
}
}
return list;
}
private String parseString() {
match('"');
StringBuilder sb = new StringBuilder();
while (pos < src.length()) {
char c = src.charAt(pos++);
if (c == '"') {
return sb.toString();
} else if (c == '\\') {
if (pos >= src.length()) break;
char esc = src.charAt(pos++);
switch (esc) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case '/': sb.append('/'); break;
case 'b': sb.append('\b'); break;
case 'f': sb.append('\f'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
case 'u':
if (pos + 4 <= src.length()) {
String hex = src.substring(pos, pos + 4);
pos += 4;
try {
sb.append((char) Integer.parseInt(hex, 16));
} catch (NumberFormatException e) {
sb.append("?");
}
}
break;
default:
sb.append(esc);
break;
}
} else {
sb.append(c);
}
}
return sb.toString();
}
private Boolean parseBoolean() {
if (src.startsWith("true", pos)) {
pos += 4;
return true;
} else if (src.startsWith("false", pos)) {
pos += 5;
return false;
}
throw new IllegalArgumentException("Expected boolean at " + pos);
}
private Object parseNull() {
if (src.startsWith("null", pos)) {
pos += 4;
return null;
}
throw new IllegalArgumentException("Expected null at " + pos);
}
private Number parseNumber() {
int start = pos;
if (src.charAt(pos) == '-') pos++;
while (pos < src.length() && (Character.isDigit(src.charAt(pos)) || src.charAt(pos) == '.' || src.charAt(pos) == 'e' || src.charAt(pos) == 'E' || src.charAt(pos) == '+' || src.charAt(pos) == '-')) {
pos++;
}
String numStr = src.substring(start, pos);
if (numStr.contains(".")) {
return Double.parseDouble(numStr);
}
return Long.parseLong(numStr);
}
private void skipWhitespace() {
while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) {
pos++;
}
}
private char peek() {
return pos < src.length() ? src.charAt(pos) : '\0';
}
private void match(char expected) {
skipWhitespace();
if (pos >= src.length() || src.charAt(pos) != expected) {
throw new IllegalArgumentException("Expected '" + expected + "' at " + pos);
}
pos++;
}
}
}
@@ -0,0 +1,222 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import java.util.Objects;
import java.util.UUID;
/**
* Data model for a saved 3270 connection profile, matching the schema of a3270 SavedHost.
* Includes per-connection host, model, protocol, and proxy configuration.
*/
public class SavedHost {
private String id;
private String name;
private String host;
private int port;
private int model;
private int dynamicRows;
private int dynamicCols;
private String luName;
private boolean autoConnect;
private String hostType;
private boolean useTls;
private boolean tlsVerifyCert;
private boolean tn3270e;
private String graphicsMode;
private String codePage;
// Per-connection proxy configuration
private String proxyType;
private String proxyHost;
private int proxyPort;
private String proxyUsername;
private String proxyPassword;
public SavedHost() {
this.id = UUID.randomUUID().toString();
this.name = "Mainframe";
this.host = "127.0.0.1";
this.port = 23;
this.model = 4;
this.dynamicRows = 62;
this.dynamicCols = 160;
this.luName = "";
this.autoConnect = false;
this.hostType = "TSO";
this.useTls = false;
this.tlsVerifyCert = true;
this.tn3270e = true;
this.graphicsMode = "BOTH";
this.codePage = "037";
this.proxyType = "NONE";
this.proxyHost = "";
this.proxyPort = 0;
this.proxyUsername = "";
this.proxyPassword = "";
}
public SavedHost(String id, String name, String host, int port, int model,
int dynamicRows, int dynamicCols, String luName, boolean autoConnect,
String hostType, boolean useTls, boolean tlsVerifyCert, boolean tn3270e,
String graphicsMode, String codePage, String proxyType, String proxyHost,
int proxyPort, String proxyUsername, String proxyPassword) {
this.id = (id != null && !id.trim().isEmpty()) ? id : UUID.randomUUID().toString();
this.name = name != null ? name : "";
this.host = host != null ? host : "127.0.0.1";
this.port = port > 0 ? port : 23;
this.model = model;
this.dynamicRows = dynamicRows > 0 ? dynamicRows : 62;
this.dynamicCols = dynamicCols > 0 ? dynamicCols : 160;
this.luName = luName != null ? luName : "";
this.autoConnect = autoConnect;
this.hostType = hostType != null ? hostType : "TSO";
this.useTls = useTls;
this.tlsVerifyCert = tlsVerifyCert;
this.tn3270e = tn3270e;
this.graphicsMode = graphicsMode != null ? graphicsMode : "BOTH";
this.codePage = codePage != null ? codePage : "037";
this.proxyType = (proxyType != null && !proxyType.trim().isEmpty()) ? proxyType.toUpperCase() : "NONE";
this.proxyHost = proxyHost != null ? proxyHost : "";
this.proxyPort = proxyPort;
this.proxyUsername = proxyUsername != null ? proxyUsername : "";
this.proxyPassword = proxyPassword != null ? proxyPassword : "";
}
public SavedHost copy() {
return new SavedHost(id, name, host, port, model, dynamicRows, dynamicCols, luName,
autoConnect, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, codePage,
proxyType, proxyHost, proxyPort, proxyUsername, proxyPassword);
}
/**
* Converts this saved host profile into an active ConnectionConfig.
*/
public ConnectionConfig toConnectionConfig() {
TerminalModel tm;
if (model == 0) {
tm = TerminalModel.IBM_DYNAMIC;
} else {
try {
tm = TerminalModel.forModel(model, true);
} catch (Exception e) {
tm = TerminalModel.IBM_3279_4;
}
}
ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls);
if (model == 0 || tm.isDynamic()) {
config.setDynamicDimensions(dynamicRows, dynamicCols);
}
if (luName != null && !luName.trim().isEmpty()) {
config.setLuName(luName.trim());
}
config.setTlsVerifyCert(tlsVerifyCert);
config.setTn3270eEnabled(tn3270e);
if (graphicsMode != null && !graphicsMode.isEmpty()) {
config.setGraphicsMode(GraphicsMode.fromString(graphicsMode));
}
if (codePage != null && !codePage.isEmpty()) {
config.setCodePage(codePage);
}
// Apply per-connection proxy if configured
if (proxyType != null && !proxyType.equalsIgnoreCase("NONE") && proxyHost != null && !proxyHost.trim().isEmpty()) {
try {
ConnectionConfig.ProxyType pt = ConnectionConfig.ProxyType.valueOf(proxyType.toUpperCase());
int pPort = proxyPort > 0 ? proxyPort : (pt == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080);
config.setProxy(pt, proxyHost.trim(), pPort,
(proxyUsername != null && !proxyUsername.trim().isEmpty()) ? proxyUsername.trim() : null,
(proxyPassword != null && !proxyPassword.isEmpty()) ? proxyPassword : null);
} catch (IllegalArgumentException ignored) {
}
}
return config;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public int getModel() { return model; }
public void setModel(int model) { this.model = model; }
public int getDynamicRows() { return dynamicRows; }
public void setDynamicRows(int dynamicRows) { this.dynamicRows = dynamicRows; }
public int getDynamicCols() { return dynamicCols; }
public void setDynamicCols(int dynamicCols) { this.dynamicCols = dynamicCols; }
public String getLuName() { return luName; }
public void setLuName(String luName) { this.luName = luName; }
public boolean isAutoConnect() { return autoConnect; }
public void setAutoConnect(boolean autoConnect) { this.autoConnect = autoConnect; }
public String getHostType() { return hostType; }
public void setHostType(String hostType) { this.hostType = hostType; }
public boolean isUseTls() { return useTls; }
public void setUseTls(boolean useTls) { this.useTls = useTls; }
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
public void setTlsVerifyCert(boolean tlsVerifyCert) { this.tlsVerifyCert = tlsVerifyCert; }
public boolean isTn3270e() { return tn3270e; }
public void setTn3270e(boolean tn3270e) { this.tn3270e = tn3270e; }
public String getGraphicsMode() { return graphicsMode; }
public void setGraphicsMode(String graphicsMode) { this.graphicsMode = graphicsMode; }
public String getCodePage() { return codePage; }
public void setCodePage(String codePage) { this.codePage = codePage; }
public String getProxyType() { return proxyType != null ? proxyType : "NONE"; }
public void setProxyType(String proxyType) { this.proxyType = (proxyType != null) ? proxyType.toUpperCase() : "NONE"; }
public String getProxyHost() { return proxyHost != null ? proxyHost : ""; }
public void setProxyHost(String proxyHost) { this.proxyHost = proxyHost; }
public int getProxyPort() { return proxyPort; }
public void setProxyPort(int proxyPort) { this.proxyPort = proxyPort; }
public String getProxyUsername() { return proxyUsername != null ? proxyUsername : ""; }
public void setProxyUsername(String proxyUsername) { this.proxyUsername = proxyUsername; }
public String getProxyPassword() { return proxyPassword != null ? proxyPassword : ""; }
public void setProxyPassword(String proxyPassword) { this.proxyPassword = proxyPassword; }
@Override
public String toString() {
if (name != null && !name.trim().isEmpty()) {
return name;
}
return host + (port > 0 ? ":" + port : "");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SavedHost savedHost = (SavedHost) o;
return Objects.equals(id, savedHost.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@@ -1,16 +1,31 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.util.List;
/**
* Connection dialog for entering host, port, model, and LU name.
* Connection dialog for entering host, port, model, LU name, and per-connection proxy configuration.
* Supports loading and saving host profiles matching the a3270 schema.
*/
public class ConnectDialog extends JDialog {
private static final String NEW_PROFILE_LABEL = "<New / Custom Connection>";
// Profile selector
private JComboBox<Object> profileCombo;
private JButton saveProfileBtn;
private JButton deleteProfileBtn;
private String selectedProfileId = null;
// Connection parameters
private JTextField hostField;
private JTextField portField;
private JComboBox<TerminalModel> modelCombo;
@@ -26,6 +41,14 @@ public class ConnectDialog extends JDialog {
private JCheckBox tn3270eCheckBox;
private JCheckBox keepAliveCheckBox;
private JCheckBox autoReconnectCheckBox;
// Per-connection proxy configuration
private JComboBox<ConnectionConfig.ProxyType> proxyTypeCombo;
private JTextField proxyHostField;
private JTextField proxyPortField;
private JTextField proxyUserField;
private JPasswordField proxyPassField;
private boolean confirmed;
private ConnectionConfig result;
@@ -35,64 +58,114 @@ public class ConnectDialog extends JDialog {
buildUI();
pack();
setLocationRelativeTo(parent);
setResizable(false);
setResizable(true);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
JPanel contentPanel = new JPanel(new GridBagLayout());
contentPanel.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 6, 5, 6);
gbc.insets = new Insets(4, 5, 4, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = ThemeManager.getUiFont();
// Host
int gridy = 0;
// 0. Profile Row
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel profileLabel = new JLabel("Profile:");
profileLabel.setFont(labelFont);
contentPanel.add(profileLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
JPanel profileBar = new JPanel(new BorderLayout(6, 0));
profileBar.setOpaque(false);
profileCombo = new JComboBox<>();
profileCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(profileCombo);
refreshProfileCombo();
profileCombo.addActionListener(e -> onProfileSelectionChanged());
profileBar.add(profileCombo, BorderLayout.CENTER);
JPanel profileBtnBar = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
profileBtnBar.setOpaque(false);
saveProfileBtn = new JButton("Save Profile...");
ThemeManager.styleButton(saveProfileBtn, ThemeManager.ButtonVariant.DEFAULT);
saveProfileBtn.setFont(labelFont);
saveProfileBtn.addActionListener(e -> onSaveProfile());
deleteProfileBtn = new JButton("Delete");
ThemeManager.styleButton(deleteProfileBtn, ThemeManager.ButtonVariant.CANCEL);
deleteProfileBtn.setFont(labelFont);
deleteProfileBtn.setEnabled(false);
deleteProfileBtn.addActionListener(e -> onDeleteProfile());
profileBtnBar.add(saveProfileBtn);
profileBtnBar.add(deleteProfileBtn);
profileBar.add(profileBtnBar, BorderLayout.EAST);
contentPanel.add(profileBar, gbc);
gridy++;
// 1. Host
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel hostLabel = new JLabel("Host:");
hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc);
contentPanel.add(hostLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
hostField = createField(20);
mainPanel.add(hostField, gbc);
contentPanel.add(hostField, gbc);
// Port
gridy++;
// 2. Port
gbc.gridx = 0;
gbc.gridy = 1;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:");
portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc);
contentPanel.add(portLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
portField = createField(6);
portField.setText("23");
mainPanel.add(portField, gbc);
contentPanel.add(portField, gbc);
// Model
gridy++;
// 3. Model
gbc.gridx = 0;
gbc.gridy = 2;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:");
modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc);
contentPanel.add(modelLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values());
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
modelCombo.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleComboBox(modelCombo);
mainPanel.add(modelCombo, gbc);
contentPanel.add(modelCombo, gbc);
// Dynamic Dimensions (shown when IBM-DYNAMIC is selected)
gridy++;
// 4. Dynamic Dimensions
gbc.gridx = 0;
gbc.gridy = 3;
gbc.gridy = gridy;
gbc.weightx = 0;
dynamicDimLabel = new JLabel("Screen Size:");
dynamicDimLabel.setFont(labelFont);
mainPanel.add(dynamicDimLabel, gbc);
contentPanel.add(dynamicDimLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
@@ -113,7 +186,7 @@ public class ConnectDialog extends JDialog {
dynamicDimPanel.add(dynamicRowsSpinner);
dynamicDimPanel.add(colsLabel);
dynamicDimPanel.add(dynamicColsSpinner);
mainPanel.add(dynamicDimPanel, gbc);
contentPanel.add(dynamicDimPanel, gbc);
Runnable updateDynamicVisibility = () -> {
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
@@ -125,40 +198,64 @@ public class ConnectDialog extends JDialog {
modelCombo.addActionListener(e -> updateDynamicVisibility.run());
updateDynamicVisibility.run();
// LU Name
gridy++;
// 5. LU Name
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:");
luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc);
contentPanel.add(luLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
luField = createField(12);
mainPanel.add(luField, gbc);
contentPanel.add(luField, gbc);
// Graphics Mode
gridy++;
// 6. Graphics Mode
gbc.gridx = 0;
gbc.gridy = 5;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel graphicsLabel = new JLabel("Graphics:");
graphicsLabel.setFont(labelFont);
mainPanel.add(graphicsLabel, gbc);
contentPanel.add(graphicsLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values());
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
graphicsCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(graphicsCombo);
mainPanel.add(graphicsCombo, gbc);
graphicsCombo.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
setBorder(new EmptyBorder(4, 8, 4, 8));
if (value instanceof haus.nightmare.lib3270j.graphics.GraphicsMode) {
setText(((haus.nightmare.lib3270j.graphics.GraphicsMode) value).getDescription());
}
if (isSelected) {
setBackground(ThemeManager.getSelectionBg());
setForeground(ThemeManager.getSelectionFg());
} else {
setBackground(ThemeManager.getBgComponent());
setForeground(ThemeManager.getFgMain());
}
return this;
}
});
contentPanel.add(graphicsCombo, gbc);
// Code Page
gridy++;
// 7. Code Page
gbc.gridx = 0;
gbc.gridy = 6;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel cpLabel = new JLabel("Code Page:");
cpLabel.setFont(labelFont);
mainPanel.add(cpLabel, gbc);
contentPanel.add(cpLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
String[] commonCodePages = {
@@ -179,43 +276,44 @@ public class ConnectDialog extends JDialog {
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
"905 - Turkey (Turkish Latin-3)",
"420 - Arabic Bilingual",
"424 - Hebrew (with Lowercase)",
"803 - Hebrew Old / Standard",
"838 - Thai",
"1160 - Thai (Euro \u20AC)",
"424 - Hebrew (with Latin)",
"803 - Hebrew Character Set",
"838 - Thai Extended",
"1160 - Thai Euro \u20AC",
"1025 - Cyrillic Multilingual",
"1123 - Cyrillic Ukraine",
"1154 - Cyrillic Multilingual (Euro \u20AC)",
"880 - Cyrillic Russian",
"1154 - Cyrillic Euro \u20AC",
"880 - Cyrillic (Russian)",
"1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)",
"1142 - Denmark / Norway (Euro \u20AC)",
"1143 - Sweden / Finland (Euro \u20AC)",
"1144 - Italy (Euro \u20AC)",
"1145 - Spain / Latin America (Euro \u20AC)",
"1146 - United Kingdom (Euro \u20AC)",
"1147 - France (Euro \u20AC)",
"1148 - International (Euro \u20AC)",
"1149 - Iceland (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS",
"1388 - Simplified Chinese Extended Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS",
"1371 - Traditional Chinese Extended Mixed DBCS",
"1388 - Simplified Chinese Extended DBCS",
"1371 - Traditional Chinese Extended DBCS",
"933 - Korean Mixed DBCS"
};
codePageCombo = new JComboBox<>(commonCodePages);
codePageCombo.setEditable(true);
String currentCp = haus.nightmare.j3270.config.Settings.getCodePage();
for (String item : commonCodePages) {
if (item.startsWith(currentCp + " ") || item.equals(currentCp)) {
codePageCombo.setSelectedItem(item);
break;
}
}
codePageCombo.setFont(ThemeManager.getUiFont());
codePageCombo.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleComboBox(codePageCombo);
mainPanel.add(codePageCombo, gbc);
setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
contentPanel.add(codePageCombo, gbc);
// TLS / SSL Checkbox
gridy++;
// 8. TLS Checkbox
gbc.gridx = 1;
gbc.gridy = 7;
gbc.weightx = 1.0;
tlsCheckBox = new JCheckBox("Enable TLS/SSL");
gbc.gridy = gridy;
tlsCheckBox = new JCheckBox("Use TLS / SSL Encryption");
ThemeManager.styleCheckBox(tlsCheckBox);
tlsCheckBox.setFont(labelFont);
tlsCheckBox.addActionListener(e -> {
@@ -228,46 +326,169 @@ public class ConnectDialog extends JDialog {
portField.setText("23");
}
});
mainPanel.add(tlsCheckBox, gbc);
contentPanel.add(tlsCheckBox, gbc);
// Verify Certificate Checkbox
gridy++;
// 9. Verify Cert Checkbox
gbc.gridx = 1;
gbc.gridy = 8;
gbc.gridy = gridy;
verifyCertCheckBox = new JCheckBox("Verify Server Certificate");
ThemeManager.styleCheckBox(verifyCertCheckBox);
verifyCertCheckBox.setFont(ThemeManager.getUiFont());
verifyCertCheckBox.setSelected(true);
verifyCertCheckBox.setEnabled(false);
mainPanel.add(verifyCertCheckBox, gbc);
contentPanel.add(verifyCertCheckBox, gbc);
// TN3270E Checkbox
gridy++;
// 10. TN3270E Checkbox
gbc.gridx = 1;
gbc.gridy = 9;
gbc.gridy = gridy;
tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)");
ThemeManager.styleCheckBox(tn3270eCheckBox);
tn3270eCheckBox.setFont(ThemeManager.getUiFont());
tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e());
mainPanel.add(tn3270eCheckBox, gbc);
contentPanel.add(tn3270eCheckBox, gbc);
// Keep-Alive Checkbox
gridy++;
// 11. Keep-Alive Checkbox
gbc.gridx = 1;
gbc.gridy = 10;
gbc.gridy = gridy;
keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)");
ThemeManager.styleCheckBox(keepAliveCheckBox);
keepAliveCheckBox.setFont(ThemeManager.getUiFont());
keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive());
mainPanel.add(keepAliveCheckBox, gbc);
contentPanel.add(keepAliveCheckBox, gbc);
// Auto-Reconnect Checkbox
gridy++;
// 12. Auto-Reconnect Checkbox
gbc.gridx = 1;
gbc.gridy = 11;
gbc.gridy = gridy;
autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect");
ThemeManager.styleCheckBox(autoReconnectCheckBox);
autoReconnectCheckBox.setFont(ThemeManager.getUiFont());
autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect());
mainPanel.add(autoReconnectCheckBox, gbc);
contentPanel.add(autoReconnectCheckBox, gbc);
gridy++;
// 13. PER-CONNECTION PROXY CONFIGURATION PANEL
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.gridwidth = 2;
gbc.weightx = 1.0;
JPanel proxyGroup = new JPanel(new GridBagLayout());
proxyGroup.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createEtchedBorder(),
"Proxy Configuration (This Connection)",
TitledBorder.LEFT,
TitledBorder.TOP,
labelFont
));
GridBagConstraints pgbc = new GridBagConstraints();
pgbc.insets = new Insets(3, 6, 3, 6);
pgbc.fill = GridBagConstraints.HORIZONTAL;
// Proxy Type
pgbc.gridx = 0;
pgbc.gridy = 0;
pgbc.weightx = 0;
JLabel ptLabel = new JLabel("Proxy Type:");
ptLabel.setFont(labelFont);
proxyGroup.add(ptLabel, pgbc);
pgbc.gridx = 1;
pgbc.gridwidth = 3;
pgbc.weightx = 1.0;
proxyTypeCombo = new JComboBox<>(ConnectionConfig.ProxyType.values());
proxyTypeCombo.setSelectedItem(ConnectionConfig.ProxyType.NONE);
proxyTypeCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(proxyTypeCombo);
proxyGroup.add(proxyTypeCombo, pgbc);
// Proxy Host & Port
pgbc.gridx = 0;
pgbc.gridy = 1;
pgbc.gridwidth = 1;
pgbc.weightx = 0;
JLabel phLabel = new JLabel("Proxy Host:");
phLabel.setFont(labelFont);
proxyGroup.add(phLabel, pgbc);
pgbc.gridx = 1;
pgbc.weightx = 1.0;
proxyHostField = createField(14);
proxyGroup.add(proxyHostField, pgbc);
pgbc.gridx = 2;
pgbc.weightx = 0;
JLabel ppLabel = new JLabel("Port:");
ppLabel.setFont(labelFont);
proxyGroup.add(ppLabel, pgbc);
pgbc.gridx = 3;
pgbc.weightx = 0.4;
proxyPortField = createField(5);
proxyGroup.add(proxyPortField, pgbc);
// Proxy User & Password
pgbc.gridx = 0;
pgbc.gridy = 2;
pgbc.gridwidth = 1;
pgbc.weightx = 0;
JLabel puLabel = new JLabel("Username:");
puLabel.setFont(labelFont);
proxyGroup.add(puLabel, pgbc);
pgbc.gridx = 1;
pgbc.weightx = 1.0;
proxyUserField = createField(12);
proxyGroup.add(proxyUserField, pgbc);
pgbc.gridx = 2;
pgbc.weightx = 0;
JLabel pwLabel = new JLabel("Password:");
pwLabel.setFont(labelFont);
proxyGroup.add(pwLabel, pgbc);
pgbc.gridx = 3;
pgbc.weightx = 0.4;
proxyPassField = createPasswordField(10);
proxyGroup.add(proxyPassField, pgbc);
// Dynamic enable/disable based on Proxy Type
Runnable updateProxyFields = () -> {
ConnectionConfig.ProxyType pt = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
boolean active = (pt != null && pt != ConnectionConfig.ProxyType.NONE);
proxyHostField.setEnabled(active);
proxyPortField.setEnabled(active);
proxyUserField.setEnabled(active);
proxyPassField.setEnabled(active && pt != ConnectionConfig.ProxyType.SOCKS4);
if (active) {
String currentPPort = proxyPortField.getText().trim();
if (currentPPort.isEmpty() || "0".equals(currentPPort)) {
proxyPortField.setText(pt == ConnectionConfig.ProxyType.HTTP ? "8080" : "1080");
}
}
};
proxyTypeCombo.addActionListener(e -> updateProxyFields.run());
updateProxyFields.run();
contentPanel.add(proxyGroup, gbc);
gridy++;
// 14. Action Buttons
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.gridwidth = 2;
gbc.weightx = 1.0;
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
JButton connectBtn = new JButton("Connect");
@@ -285,13 +506,14 @@ public class ConnectDialog extends JDialog {
buttonPanel.add(cancelBtn);
buttonPanel.add(connectBtn);
contentPanel.add(buttonPanel, gbc);
gbc.gridx = 0;
gbc.gridy = 12;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
JScrollPane scrollPane = new JScrollPane(contentPanel);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.getVerticalScrollBar().setUnitIncrement(14);
setContentPane(scrollPane);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// Enter key triggers connect
@@ -305,6 +527,189 @@ public class ConnectDialog extends JDialog {
return field;
}
private JPasswordField createPasswordField(int cols) {
JPasswordField field = new JPasswordField(cols);
field.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(field);
return field;
}
// ================= Profile Management =================
private void refreshProfileCombo() {
profileCombo.removeAllItems();
profileCombo.addItem(NEW_PROFILE_LABEL);
List<SavedHost> saved = HostStorage.getSavedHosts();
for (SavedHost h : saved) {
profileCombo.addItem(h);
}
if (selectedProfileId != null) {
for (int i = 0; i < profileCombo.getItemCount(); i++) {
Object item = profileCombo.getItemAt(i);
if (item instanceof SavedHost && ((SavedHost) item).getId().equals(selectedProfileId)) {
profileCombo.setSelectedIndex(i);
return;
}
}
}
profileCombo.setSelectedIndex(0);
}
private void onProfileSelectionChanged() {
Object sel = profileCombo.getSelectedItem();
if (sel instanceof SavedHost) {
SavedHost h = (SavedHost) sel;
selectedProfileId = h.getId();
deleteProfileBtn.setEnabled(true);
loadProfileFields(h);
} else {
selectedProfileId = null;
deleteProfileBtn.setEnabled(false);
}
}
public void loadProfile(SavedHost h) {
if (h == null) return;
selectedProfileId = h.getId();
refreshProfileCombo();
loadProfileFields(h);
}
private void loadProfileFields(SavedHost h) {
if (h == null) return;
hostField.setText(h.getHost());
portField.setText(String.valueOf(h.getPort()));
if (h.getModel() == 0) {
modelCombo.setSelectedItem(TerminalModel.IBM_DYNAMIC);
} else {
try {
modelCombo.setSelectedItem(TerminalModel.forModel(h.getModel(), true));
} catch (Exception ignored) {
}
}
dynamicRowsSpinner.setValue(h.getDynamicRows() > 0 ? h.getDynamicRows() : 62);
dynamicColsSpinner.setValue(h.getDynamicCols() > 0 ? h.getDynamicCols() : 160);
luField.setText(h.getLuName() != null ? h.getLuName() : "");
tlsCheckBox.setSelected(h.isUseTls());
verifyCertCheckBox.setEnabled(h.isUseTls());
verifyCertCheckBox.setSelected(h.isTlsVerifyCert());
tn3270eCheckBox.setSelected(h.isTn3270e());
setInitialCodePage(h.getCodePage());
if (h.getGraphicsMode() != null) {
graphicsCombo.setSelectedItem(haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(h.getGraphicsMode()));
}
// Per-connection proxy parameters
String ptStr = h.getProxyType();
try {
ConnectionConfig.ProxyType pt = (ptStr != null && !ptStr.trim().isEmpty())
? ConnectionConfig.ProxyType.valueOf(ptStr.toUpperCase())
: ConnectionConfig.ProxyType.NONE;
proxyTypeCombo.setSelectedItem(pt);
} catch (IllegalArgumentException e) {
proxyTypeCombo.setSelectedItem(ConnectionConfig.ProxyType.NONE);
}
proxyHostField.setText(h.getProxyHost() != null ? h.getProxyHost() : "");
proxyPortField.setText(h.getProxyPort() > 0 ? String.valueOf(h.getProxyPort()) : "");
proxyUserField.setText(h.getProxyUsername() != null ? h.getProxyUsername() : "");
proxyPassField.setText(h.getProxyPassword() != null ? h.getProxyPassword() : "");
}
private void onSaveProfile() {
String hStr = hostField.getText().trim();
if (hStr.isEmpty()) {
hostField.requestFocus();
JOptionPane.showMessageDialog(this, "Please specify a host before saving profile.", "Missing Host", JOptionPane.WARNING_MESSAGE);
return;
}
String defaultName = hStr;
Object currentSel = profileCombo.getSelectedItem();
if (currentSel instanceof SavedHost) {
defaultName = ((SavedHost) currentSel).getName();
}
String profileName = (String) JOptionPane.showInputDialog(
this,
"Enter Host Profile Name:",
"Save Profile",
JOptionPane.PLAIN_MESSAGE,
null,
null,
defaultName
);
if (profileName == null || profileName.trim().isEmpty()) {
return;
}
profileName = profileName.trim();
SavedHost hostToSave = buildCurrentSavedHost(profileName);
HostStorage.saveHost(hostToSave);
selectedProfileId = hostToSave.getId();
refreshProfileCombo();
}
private void onDeleteProfile() {
if (selectedProfileId == null) return;
int opt = JOptionPane.showConfirmDialog(this,
"Are you sure you want to delete this host profile?",
"Delete Profile",
JOptionPane.YES_NO_OPTION);
if (opt == JOptionPane.YES_OPTION) {
HostStorage.deleteHost(selectedProfileId);
selectedProfileId = null;
refreshProfileCombo();
}
}
private SavedHost buildCurrentSavedHost(String profileName) {
String id = selectedProfileId != null ? selectedProfileId : java.util.UUID.randomUUID().toString();
String h = hostField.getText().trim();
int port = 23;
try {
port = Integer.parseInt(portField.getText().trim());
} catch (NumberFormatException ignored) {}
TerminalModel m = (TerminalModel) modelCombo.getSelectedItem();
int mNum = (m != null) ? m.getModelNumber() : 4;
int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue();
String lu = luField.getText().trim();
boolean tls = tlsCheckBox.isSelected();
boolean verify = verifyCertCheckBox.isSelected();
boolean tn3270e = tn3270eCheckBox.isSelected();
String gfx = "BOTH";
if (graphicsCombo.getSelectedItem() != null) {
gfx = ((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()).name();
}
String cp = "037";
Object cpSel = codePageCombo.getSelectedItem();
if (cpSel != null) {
String s = cpSel.toString().trim();
int dash = s.indexOf(" -");
cp = (dash > 0) ? s.substring(0, dash).trim() : s;
}
ConnectionConfig.ProxyType pt = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
String ptStr = (pt != null) ? pt.name() : "NONE";
String pHost = proxyHostField.getText().trim();
int pPort = 0;
try {
pPort = Integer.parseInt(proxyPortField.getText().trim());
} catch (NumberFormatException ignored) {}
String pUser = proxyUserField.getText().trim();
String pPass = new String(proxyPassField.getPassword());
return new SavedHost(id, profileName, h, port, mNum, dRows, dCols, lu, false,
"TSO", tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
}
// ================= Connect Action =================
private void onConnect() {
String host = hostField.getText().trim();
if (host.isEmpty()) {
@@ -321,20 +726,35 @@ public class ConnectDialog extends JDialog {
}
TerminalModel selectedModel = (TerminalModel) modelCombo.getSelectedItem();
result = new ConnectionConfig(host, port, selectedModel);
if (selectedModel != null && selectedModel.isDynamic()) {
if (selectedModel == null) {
selectedModel = TerminalModel.IBM_3279_4;
}
// Support full host string parsing if the user pasted flags or prefixes into host field
if (host.contains(" ") || host.startsWith("L:") || host.startsWith("P:") || host.startsWith("--proxy=") || host.startsWith("-proxy=")) {
result = ConnectionConfig.parseHostString(host, port, selectedModel);
} else {
result = new ConnectionConfig(host, port, selectedModel);
}
if (selectedModel.isDynamic()) {
int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue();
result.setDynamicDimensions(dRows, dCols);
haus.nightmare.j3270.config.Settings.setDynamicRows(dRows);
haus.nightmare.j3270.config.Settings.setDynamicCols(dCols);
}
String lu = luField.getText().trim();
if (!lu.isEmpty()) {
result.setLuName(lu);
}
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem());
if (graphicsCombo.getSelectedItem() != null) {
haus.nightmare.lib3270j.graphics.GraphicsMode gm = (haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem();
result.setGraphicsMode(gm);
haus.nightmare.j3270.config.Settings.setGraphicsMode(gm);
}
Object cpSelection = codePageCombo.getSelectedItem();
if (cpSelection != null) {
String cpStr = cpSelection.toString().trim();
@@ -361,6 +781,30 @@ public class ConnectDialog extends JDialog {
result.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries());
haus.nightmare.j3270.config.Settings.setAutoConnectAutoReconnect(ar);
// Apply per-connection proxy parameters strictly to this connection
ConnectionConfig.ProxyType pType = (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
String pHost = proxyHostField.getText().trim();
if (pType != null && pType != ConnectionConfig.ProxyType.NONE && !pHost.isEmpty()) {
int pPort = 0;
try {
pPort = Integer.parseInt(proxyPortField.getText().trim());
} catch (NumberFormatException ignored) {}
if (pPort <= 0) {
pPort = (pType == ConnectionConfig.ProxyType.HTTP) ? 8080 : 1080;
}
String pUser = proxyUserField.getText().trim();
String pPass = new String(proxyPassField.getPassword());
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
}
if (selectedProfileId != null) {
SavedHost existing = HostStorage.findById(selectedProfileId);
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
? existing.getName() : host;
SavedHost updated = buildCurrentSavedHost(pName);
HostStorage.saveHost(updated);
}
confirmed = true;
dispose();
}
@@ -395,6 +839,12 @@ public class ConnectDialog extends JDialog {
tn3270eCheckBox.setSelected(tn3270e);
}
public void setInitialGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
if (mode != null && selectedProfileId == null && graphicsCombo != null) {
graphicsCombo.setSelectedItem(mode);
}
}
public void setInitialCodePage(String cp) {
if (cp != null && codePageCombo != null) {
for (int i = 0; i < codePageCombo.getItemCount(); i++) {
@@ -407,4 +857,49 @@ public class ConnectDialog extends JDialog {
codePageCombo.setSelectedItem(cp);
}
}
/**
* Set initial proxy settings for this connection.
*/
public void setInitialProxy(ConnectionConfig.ProxyType type, String host, int port, String user, String pass) {
if (proxyTypeCombo != null) {
proxyTypeCombo.setSelectedItem(type != null ? type : ConnectionConfig.ProxyType.NONE);
}
if (proxyHostField != null) {
proxyHostField.setText(host != null ? host : "");
}
if (proxyPortField != null) {
proxyPortField.setText(port > 0 ? String.valueOf(port) : "");
}
if (proxyUserField != null) {
proxyUserField.setText(user != null ? user : "");
}
if (proxyPassField != null) {
proxyPassField.setText(pass != null ? pass : "");
}
}
public ConnectionConfig.ProxyType getSelectedProxyType() {
return (ConnectionConfig.ProxyType) proxyTypeCombo.getSelectedItem();
}
public String getProxyHost() {
return proxyHostField.getText().trim();
}
public int getProxyPort() {
try {
return Integer.parseInt(proxyPortField.getText().trim());
} catch (Exception e) {
return 0;
}
}
public String getProxyUsername() {
return proxyUserField.getText().trim();
}
public String getProxyPassword() {
return new String(proxyPassField.getPassword());
}
}
@@ -0,0 +1,391 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Real-time hotspot and URL auto-detection engine matching IBM Host On-Demand
* ShowURLOptions and ScreenText parity.
*
* Scans screen text using regular expressions for:
* 1. URLs (http://, https://, ftp://, file://, mailto:, www.)
* 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
* 3. Menu choices ("1.", "2.", "===> 3", "=> 4")
*
* Provides hit-testing for mouse hover/clicks and generates keystroke actions.
*/
public class HotspotEngine {
public enum HotspotType {
URL,
PF_KEY,
ENTER,
MENU_CHOICE
}
/**
* Represents an active interactive hotspot region on the terminal screen.
*/
public static class Hotspot {
private final HotspotType type;
private final int startRow;
private final int startCol;
private final int endRow;
private final int endCol;
private final String displayText;
private final String action;
private final String tooltip;
public Hotspot(HotspotType type, int startRow, int startCol, int endRow, int endCol,
String displayText, String action, String tooltip) {
this.type = type;
this.startRow = startRow;
this.startCol = startCol;
this.endRow = endRow;
this.endCol = endCol;
this.displayText = displayText;
this.action = action;
this.tooltip = tooltip;
}
public HotspotType getType() {
return type;
}
public int getStartRow() {
return startRow;
}
public int getStartCol() {
return startCol;
}
public int getEndRow() {
return endRow;
}
public int getEndCol() {
return endCol;
}
public String getDisplayText() {
return displayText;
}
public String getAction() {
return action;
}
public String getTooltip() {
return tooltip;
}
/**
* Test whether the given row/col coordinate falls within this hotspot.
*/
public boolean contains(int row, int col) {
if (row < startRow || row > endRow) {
return false;
}
if (startRow == endRow) {
return col >= startCol && col <= endCol;
}
if (row == startRow) {
return col >= startCol;
}
if (row == endRow) {
return col <= endCol;
}
return true; // Intermediate rows
}
@Override
public String toString() {
return "Hotspot{" + type + " [" + startRow + "," + startCol + "]-[" + endRow + "," + endCol + "] '" + displayText + "'}";
}
}
// Configurable scan enablement
private boolean urlEnabled = true;
private boolean pfKeyEnabled = true;
private boolean menuChoiceEnabled = true;
// Regular expression patterns
// 1. URLs
private static final Pattern URL_PATTERN = Pattern.compile(
"(?:https?|ftp|file)://[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|www\\.[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|mailto:[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+",
Pattern.CASE_INSENSITIVE
);
// 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
private static final Pattern PF_KEY_PATTERN = Pattern.compile(
"(?:(?<=\\W)|^)(PF(?:[1-9]|1[0-9]|2[0-4])|F(?:[1-9]|1[0-9]|2[0-4])|PA[1-3]|\\[ENTER\\]|ENTER)(?:(?=\\W)|$)",
Pattern.CASE_INSENSITIVE
);
// 3. Menu choices: "1.", "2.", "===> 3", "=> 4", "--> 5"
private static final Pattern MENU_NUMBER_PATTERN = Pattern.compile(
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
);
private static final Pattern MENU_PROMPT_PATTERN = Pattern.compile(
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
);
// Caching state to avoid rescanning unchanged screens
private String cachedScreenText = "";
private int cachedRows = 0;
private int cachedCols = 0;
private List<Hotspot> cachedHotspots = Collections.emptyList();
public HotspotEngine() {
}
public boolean isUrlEnabled() {
return urlEnabled;
}
public void setUrlEnabled(boolean urlEnabled) {
this.urlEnabled = urlEnabled;
invalidateCache();
}
public boolean isPfKeyEnabled() {
return pfKeyEnabled;
}
public void setPfKeyEnabled(boolean pfKeyEnabled) {
this.pfKeyEnabled = pfKeyEnabled;
invalidateCache();
}
public boolean isMenuChoiceEnabled() {
return menuChoiceEnabled;
}
public void setMenuChoiceEnabled(boolean menuChoiceEnabled) {
this.menuChoiceEnabled = menuChoiceEnabled;
invalidateCache();
}
public void invalidateCache() {
cachedScreenText = "";
cachedHotspots = Collections.emptyList();
}
/**
* Scan the given ScreenBuffer for interactive hotspots.
*/
public synchronized List<Hotspot> scan(ScreenBuffer sb) {
if (sb == null) return Collections.emptyList();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
// Build plain text representation
StringBuilder fullText = new StringBuilder(rows * (cols + 1));
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int baddr = r * cols + c;
ExtendedAttribute ea = sb.getDisplayCell(baddr);
if (ea == null || ea.isFieldAttribute() || ea.ucs4 <= 0x20 || ea.ucs4 == 0xFF) {
fullText.append(' ');
} else {
fullText.append((char) ea.ucs4);
}
}
if (r < rows - 1) {
fullText.append('\n');
}
}
return scanText(fullText.toString(), rows, cols);
}
/**
* Scan formatted screen text (lines separated by '\n') for hotspots.
*/
public synchronized List<Hotspot> scanText(String screenText, int rows, int cols) {
if (screenText == null || rows <= 0 || cols <= 0) {
return Collections.emptyList();
}
if (screenText.equals(cachedScreenText) && rows == cachedRows && cols == cachedCols) {
return cachedHotspots;
}
List<Hotspot> hotspots = new ArrayList<>();
String[] lines = screenText.split("\n", -1);
int actualRows = Math.min(rows, lines.length);
// Scan line by line for precise row/column tracking
for (int r = 0; r < actualRows; r++) {
String line = lines[r];
boolean[] occupied = new boolean[line.length() + 1];
// 1. Scan URLs
if (urlEnabled) {
Matcher mUrl = URL_PATTERN.matcher(line);
while (mUrl.find()) {
int startCol = mUrl.start();
int endCol = mUrl.end();
String match = mUrl.group();
// Trim trailing punctuation like . , ; : ) ]
while (match.length() > 1) {
char last = match.charAt(match.length() - 1);
if (last == '.' || last == ',' || last == ';' || last == ':' || last == ')' || last == ']' || last == '>') {
match = match.substring(0, match.length() - 1);
endCol--;
} else {
break;
}
}
if (match.isEmpty()) continue;
String actionUrl = match;
if (match.toLowerCase().startsWith("www.")) {
actionUrl = "http://" + match;
}
Hotspot h = new Hotspot(
HotspotType.URL,
r, startCol, r, endCol - 1,
match, actionUrl, "Open URL: " + actionUrl
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
// 2. Scan PF / Function keys
if (pfKeyEnabled) {
Matcher mPf = PF_KEY_PATTERN.matcher(line);
while (mPf.find()) {
int startCol = mPf.start(1);
int endCol = mPf.end(1);
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String keyText = mPf.group(1).toUpperCase();
String action;
String tooltip;
if (keyText.equals("ENTER") || keyText.equals("[ENTER]")) {
action = "[enter]";
tooltip = "Send [ENTER]";
} else if (keyText.startsWith("PF")) {
String numStr = keyText.substring(2);
action = "[pf" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else if (keyText.startsWith("F")) {
String numStr = keyText.substring(1);
action = "[pf" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else if (keyText.startsWith("PA")) {
String numStr = keyText.substring(2);
action = "[pa" + numStr + "]";
tooltip = "Send " + action.toUpperCase();
} else {
continue;
}
Hotspot h = new Hotspot(
keyText.contains("ENTER") ? HotspotType.ENTER : HotspotType.PF_KEY,
r, startCol, r, endCol - 1,
mPf.group(1), action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
// 3. Scan Menu choices ("1.", "2.", "===> 3", "=> 4")
if (menuChoiceEnabled) {
// Pattern 3a: Number followed by dot (e.g. " 1. ")
Matcher mNum = MENU_NUMBER_PATTERN.matcher(line);
while (mNum.find()) {
int startCol = mNum.start(1);
int endCol = mNum.end();
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String choiceNum = mNum.group(1);
String action = choiceNum + "[enter]";
String tooltip = "Select option " + choiceNum;
Hotspot h = new Hotspot(
HotspotType.MENU_CHOICE,
r, startCol, r, endCol - 1,
choiceNum + ".", action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
// Pattern 3b: Prompt followed by number (e.g. "===> 3")
Matcher mPrompt = MENU_PROMPT_PATTERN.matcher(line);
while (mPrompt.find()) {
int startCol = mPrompt.start();
int endCol = mPrompt.end();
if (isRangeOccupied(occupied, startCol, endCol)) continue;
String choiceNum = mPrompt.group(1);
String action = choiceNum + "[enter]";
String tooltip = "Execute choice " + choiceNum;
Hotspot h = new Hotspot(
HotspotType.MENU_CHOICE,
r, startCol, r, endCol - 1,
mPrompt.group(), action, tooltip
);
hotspots.add(h);
markOccupied(occupied, startCol, endCol);
}
}
}
this.cachedScreenText = screenText;
this.cachedRows = rows;
this.cachedCols = cols;
this.cachedHotspots = Collections.unmodifiableList(hotspots);
return cachedHotspots;
}
/**
* Hit-test for a hotspot at the specified terminal row and column.
*/
public Hotspot findHotspot(int row, int col) {
for (Hotspot h : cachedHotspots) {
if (h.contains(row, col)) {
return h;
}
}
return null;
}
/**
* Query currently active cached hotspots.
*/
public List<Hotspot> getHotspots() {
return cachedHotspots;
}
private static void markOccupied(boolean[] arr, int start, int end) {
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
arr[i] = true;
}
}
private static boolean isRangeOccupied(boolean[] arr, int start, int end) {
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
if (arr[i]) return true;
}
return false;
}
}
@@ -206,7 +206,8 @@ public class KeyBindings {
/**
* Parse a .kmp file from a Reader.
* Supports both INI-style KEY<n>=action formats and bind statements.
* Supports both IBM Host On-Demand (HoD) .kmp format (hashKey prefixes)
* and IBM Personal Communications (PCOMM) INI-style and bind statements.
*
* @param reader the reader to parse from
* @return Map of j3270 action names to key stroke bindings
@@ -214,11 +215,38 @@ public class KeyBindings {
*/
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<>();
List<String> rawLines = new ArrayList<>();
boolean hasHodKeyRemap = false;
boolean hasHodHashKey = false;
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
rawLines.add(line);
String trimmed = line.trim();
if (trimmed.equalsIgnoreCase("[KeyRemap]")) {
hasHodKeyRemap = true;
}
int eq = trimmed.indexOf('=');
if (eq > 0) {
String left = trimmed.substring(0, eq).trim();
if (left.matches("^[A-Za-z][0-9]+$")) {
hasHodHashKey = true;
}
}
}
if (hasHodKeyRemap || hasHodHashKey) {
Map<String, String> hodMap = haus.nightmare.lib3270j.input.HodKeyMap.parseHodKmp(
new StringReader(String.join("\n", rawLines))
);
if (hodMap != null && !hodMap.isEmpty()) {
return hodMap;
}
}
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
for (String raw : rawLines) {
line = raw.trim();
// Strip comments
int commentIdx = line.indexOf(';');
@@ -262,6 +290,22 @@ public class KeyBindings {
return result;
}
/**
* Export key bindings to a .kmp file using IBM HoD format.
*/
public static void exportKmp(File file, Map<String, String> keyBindings) throws IOException {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
exportKmp(bw, keyBindings);
}
}
/**
* Export key bindings to a Writer using IBM HoD format.
*/
public static void exportKmp(Writer writer, Map<String, String> keyBindings) throws IOException {
haus.nightmare.lib3270j.input.HodKeyMap.exportHodKmp(writer, keyBindings);
}
private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
String[] parts = stmt.split("\\s+", 2);
if (parts.length < 2) return;
@@ -288,6 +332,19 @@ public class KeyBindings {
}
private static void parseKeyAssignment(String left, String right, Map<String, List<String>> actionToBindings) {
if (left.matches("^[A-Za-z][0-9]+$")) {
haus.nightmare.lib3270j.input.HodKeyMap.KeyEntry entry =
haus.nightmare.lib3270j.input.HodKeyMap.parseHashKey(left, right);
if (entry != null) {
String stroke = entry.toKeyStrokeString();
String act = haus.nightmare.lib3270j.input.HodKeyMap.resolveMnemonicToAction(right);
if (act != null && stroke != null && !stroke.isEmpty()) {
addBinding(actionToBindings, act, stroke);
return;
}
}
}
// Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
String action = resolveMnemonic(right);
String keyStroke = null;
@@ -0,0 +1,392 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLSession;
import haus.nightmare.lib3270j.macro.Macro;
import haus.nightmare.lib3270j.macro.MacroScreen;
import haus.nightmare.lib3270j.macro.MacroScreens;
import haus.nightmare.lib3270j.macro.MacroXmlParser;
import haus.nightmare.lib3270j.macro.action.MacroAction;
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/**
* Interactive Swing dialog for loading, editing, running, and debugging
* IBM Host On-Demand XML Macros (<HASCRIPT> / .mac files).
*/
public class MacroDialog extends JDialog implements Macro.MacroListener, Macro.MacroPromptHandler {
private final Telnet3270Client client;
private final TerminalPanel terminalPanel;
private JTextArea xmlArea;
private JTextArea logArea;
private JLabel statusLabel;
private JButton playBtn;
private JButton pauseBtn;
private JButton stopBtn;
private Macro macro;
public MacroDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
super(parent, "IBM HoD XML Macro Runtime (<HASCRIPT>)", false);
this.client = client;
this.terminalPanel = terminalPanel;
AppIcon.applyTo(this);
this.macro = new Macro(client);
this.macro.addMacroListener(this);
this.macro.setPromptHandler(this);
buildUI();
setSize(780, 580);
setLocationRelativeTo(parent);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
Font labelFont = ThemeManager.getUiFont();
// Top info label
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
topPanel.setOpaque(false);
JLabel descLabel = new JLabel("<html>Load, edit, and execute IBM Host On-Demand XML Macro files (<code>.mac</code> / <code>&lt;HASCRIPT&gt;</code>).<br>"
+ "Automates host flows via screen descriptors, actions, variables, and state transitions.</html>");
descLabel.setFont(labelFont);
topPanel.add(descLabel, BorderLayout.CENTER);
mainPanel.add(topPanel, BorderLayout.NORTH);
// Center split: XML editor and Execution Log
xmlArea = new JTextArea();
xmlArea.setFont(ThemeManager.getMonospacedUiFont());
xmlArea.setLineWrap(false);
xmlArea.setText(getDefaultMacroXml());
ThemeManager.styleTextArea(xmlArea);
JScrollPane xmlScroll = new JScrollPane(xmlArea);
ThemeManager.styleScrollPane(xmlScroll);
xmlScroll.setBorder(ThemeManager.createTitledBorder("Macro XML (<HASCRIPT>)"));
logArea = new JTextArea();
logArea.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -1));
logArea.setEditable(false);
logArea.setLineWrap(true);
ThemeManager.styleTextArea(logArea);
JScrollPane logScroll = new JScrollPane(logArea);
ThemeManager.styleScrollPane(logScroll);
logScroll.setBorder(ThemeManager.createTitledBorder("Execution Log"));
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, xmlScroll, logScroll);
splitPane.setResizeWeight(0.65);
splitPane.setOpaque(false);
mainPanel.add(splitPane, BorderLayout.CENTER);
// Bottom control panel
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
bottomPanel.setOpaque(false);
statusLabel = new JLabel("Status: Stopped");
statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(labelFont);
bottomPanel.add(statusLabel, BorderLayout.WEST);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
buttonPanel.setOpaque(false);
JButton loadBtn = new JButton("Load .mac...");
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
loadBtn.setFont(labelFont);
loadBtn.addActionListener(e -> loadMacroFile());
JButton saveBtn = new JButton("Save .mac...");
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
saveBtn.setFont(labelFont);
saveBtn.addActionListener(e -> saveMacroFile());
playBtn = new JButton("Play");
ThemeManager.styleButton(playBtn, ThemeManager.ButtonVariant.PRIMARY);
playBtn.setFont(labelFont);
playBtn.addActionListener(e -> playMacro());
pauseBtn = new JButton("Pause");
ThemeManager.styleButton(pauseBtn, ThemeManager.ButtonVariant.DEFAULT);
pauseBtn.setFont(labelFont);
pauseBtn.setEnabled(false);
pauseBtn.addActionListener(e -> togglePause());
stopBtn = new JButton("Stop");
ThemeManager.styleButton(stopBtn, ThemeManager.ButtonVariant.DEFAULT);
stopBtn.setFont(labelFont);
stopBtn.setEnabled(false);
stopBtn.addActionListener(e -> stopMacro());
JButton clearLogBtn = new JButton("Clear Log");
ThemeManager.styleButton(clearLogBtn, ThemeManager.ButtonVariant.DEFAULT);
clearLogBtn.setFont(labelFont);
clearLogBtn.addActionListener(e -> logArea.setText(""));
JButton closeBtn = new JButton("Close");
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
closeBtn.setFont(labelFont);
closeBtn.addActionListener(e -> dispose());
buttonPanel.add(loadBtn);
buttonPanel.add(saveBtn);
buttonPanel.add(Box.createHorizontalStrut(8));
buttonPanel.add(playBtn);
buttonPanel.add(pauseBtn);
buttonPanel.add(stopBtn);
buttonPanel.add(clearLogBtn);
buttonPanel.add(Box.createHorizontalStrut(8));
buttonPanel.add(closeBtn);
bottomPanel.add(buttonPanel, BorderLayout.EAST);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
}
private void loadMacroFile() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Open IBM HoD Macro");
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = r.readLine()) != null) {
sb.append(line).append("\n");
}
xmlArea.setText(sb.toString());
log("Loaded macro file: " + f.getName());
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Failed to load macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void saveMacroFile() {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Save IBM HoD Macro");
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
if (!f.getName().contains(".")) {
f = new File(f.getParentFile(), f.getName() + ".mac");
}
try (Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)) {
w.write(xmlArea.getText());
log("Saved macro file: " + f.getName());
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Failed to save macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
public void playMacro() {
String xml = xmlArea.getText();
if (xml == null || xml.trim().isEmpty()) {
JOptionPane.showMessageDialog(this, "Macro XML is empty.", "Warning", JOptionPane.WARNING_MESSAGE);
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
int opt = JOptionPane.showConfirmDialog(this,
"Session is not currently connected to a host.\nDo you want to run macro anyway (descriptors will wait for connection)?",
"Not Connected", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (opt != JOptionPane.YES_OPTION) {
return;
}
}
try {
MacroScreens screens = MacroXmlParser.parse(xml);
macro.setScreens(screens);
macro.setClient(client);
log("Starting macro: " + (screens.getName().isEmpty() ? "Untitled" : screens.getName()));
macro.play();
} catch (Exception ex) {
log("Parse / Launch Error: " + ex.getMessage());
JOptionPane.showMessageDialog(this, "Macro parsing error: " + ex.getMessage(), "Macro Error", JOptionPane.ERROR_MESSAGE);
}
}
public void togglePause() {
if (macro.getState() == Macro.STATE_PLAYING) {
macro.pause();
pauseBtn.setText("Resume");
log("Macro paused.");
} else if (macro.getState() == Macro.STATE_PAUSED) {
macro.resume();
pauseBtn.setText("Pause");
log("Macro resumed.");
}
}
public void stopMacro() {
macro.stop();
log("Macro stopped by user.");
}
private void log(String msg) {
SwingUtilities.invokeLater(() -> {
logArea.append(msg + "\n");
logArea.setCaretPosition(logArea.getDocument().getLength());
});
}
// ========== MacroListener Callbacks ==========
@Override
public void macroStateChanged(Macro macro, int oldState, int newState) {
SwingUtilities.invokeLater(() -> {
switch (newState) {
case Macro.STATE_PLAYING:
statusLabel.setText("Status: Playing");
statusLabel.setForeground(ThemeManager.getFgMain());
playBtn.setEnabled(false);
pauseBtn.setEnabled(true);
pauseBtn.setText("Pause");
stopBtn.setEnabled(true);
break;
case Macro.STATE_PAUSED:
statusLabel.setText("Status: Paused");
statusLabel.setForeground(ThemeManager.getFgMuted());
pauseBtn.setText("Resume");
break;
case Macro.STATE_ERROR:
statusLabel.setText("Status: Error");
statusLabel.setForeground(Color.RED);
playBtn.setEnabled(true);
pauseBtn.setEnabled(false);
stopBtn.setEnabled(false);
break;
case Macro.STATE_STOPPED:
default:
statusLabel.setText("Status: Stopped");
statusLabel.setForeground(ThemeManager.getFgMuted());
playBtn.setEnabled(true);
pauseBtn.setEnabled(false);
pauseBtn.setText("Pause");
stopBtn.setEnabled(false);
break;
}
});
}
@Override
public void macroScreenMatching(MacroScreen screen) {
log("Matched screen: [" + screen.getName() + "]");
if (terminalPanel != null) {
terminalPanel.repaint();
}
}
@Override
public void macroActionExecuting(MacroAction action) {
log(" -> Executing: " + action.getClass().getSimpleName());
if (terminalPanel != null) {
terminalPanel.repaint();
}
}
@Override
public void macroMessage(String title, String message) {
log("Message [" + title + "]: " + message);
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this, message, title.isEmpty() ? "Macro Message" : title, JOptionPane.INFORMATION_MESSAGE);
});
}
@Override
public void macroError(String error) {
log("ERROR: " + error);
}
// ========== MacroPromptHandler Callback ==========
@Override
public String prompt(MacroActionPrompt prompt) {
AtomicReference<String> result = new AtomicReference<>(null);
if (SwingUtilities.isEventDispatchThread()) {
result.set(showPromptModal(prompt));
} else {
try {
SwingUtilities.invokeAndWait(() -> {
result.set(showPromptModal(prompt));
});
} catch (Exception e) {
return null;
}
}
return result.get();
}
private String showPromptModal(MacroActionPrompt prompt) {
JPanel panel = new JPanel(new BorderLayout(5, 5));
JLabel msgLabel = new JLabel(prompt.getDescription().isEmpty() ? "Enter parameter:" : prompt.getDescription());
panel.add(msgLabel, BorderLayout.NORTH);
JTextField textField;
if (prompt.isEncrypted()) {
textField = new JPasswordField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
} else {
textField = new JTextField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
}
panel.add(textField, BorderLayout.CENTER);
int opt = JOptionPane.showConfirmDialog(this, panel,
prompt.getTitle().isEmpty() ? "Macro Input Prompt" : prompt.getTitle(),
JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
if (opt == JOptionPane.OK_OPTION) {
if (textField instanceof JPasswordField) {
return new String(((JPasswordField) textField).getPassword());
}
return textField.getText();
}
return null;
}
private String getDefaultMacroXml() {
return "<HASCRIPT name=\"SampleLogon\" timeout=\"15000\" usevars=\"true\">\n"
+ " <vars>\n"
+ " <create name=\"$userId\" type=\"string\" value=\"USER01\" />\n"
+ " <create name=\"$count\" type=\"integer\" value=\"0\" />\n"
+ " </vars>\n\n"
+ " <screen name=\"LogonScreen\" entryscreen=\"true\">\n"
+ " <description>\n"
+ " <oia status=\"NOTINHIBITED\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <input value=\"$userId + '[enter]'\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
+ " </actions>\n"
+ " <nextscreens timeout=\"10000\">\n"
+ " <nextscreen name=\"ReadyScreen\" />\n"
+ " </nextscreens>\n"
+ " </screen>\n\n"
+ " <screen name=\"ReadyScreen\" exitscreen=\"true\">\n"
+ " <description>\n"
+ " <oia status=\"NOTINHIBITED\" />\n"
+ " </description>\n"
+ " <actions>\n"
+ " <message title=\"Success\" value=\"Logon macro completed successfully!\" />\n"
+ " </actions>\n"
+ " </screen>\n"
+ "</HASCRIPT>";
}
}
@@ -0,0 +1,323 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenHistoryListener;
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenSnapshot;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.*;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Screen History viewer dialog matching IBM Host On-Demand ScreenHistory UI parity.
*
* Provides:
* - Interactive JSlider scrubber to review past 64 screen buffer frames
* - Previous / Next navigation buttons
* - Exact monospace terminal frame preview with selection capability
* - Copy Screen, Copy Selection, and Copy as Table actions
* - Live update synchronization as new host screens arrive
*/
public class ScreenHistoryDialog extends JDialog implements ScreenHistoryListener {
private final J3270App parentApp;
private final Telnet3270Client client;
private final ScreenHistoryBuffer historyBuffer;
private JSlider scrubSlider;
private JButton btnPrev;
private JButton btnNext;
private JLabel infoLabel;
private JTextArea previewArea;
private JButton btnCopyScreen;
private JButton btnCopySelection;
private JButton btnCopyTable;
private JButton btnClose;
private int currentIndex = -1; // 0-based index into historyBuffer
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public ScreenHistoryDialog(J3270App parent, Telnet3270Client client) {
super(parent, "Screen History", false);
this.parentApp = parent;
this.client = client;
this.historyBuffer = client != null ? client.getScreenHistoryBuffer() : null;
AppIcon.applyTo(this);
initComponents();
setupKeyNavigation();
if (historyBuffer != null) {
historyBuffer.addListener(this);
refreshHistoryView(historyBuffer.size() - 1);
} else {
updateEmptyState();
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
if (historyBuffer != null) {
historyBuffer.removeListener(ScreenHistoryDialog.this);
}
}
});
setSize(780, 560);
setLocationRelativeTo(parent);
}
private void initComponents() {
setLayout(new BorderLayout(6, 6));
JPanel contentPane = new JPanel(new BorderLayout(8, 8));
contentPane.setBorder(new EmptyBorder(10, 10, 10, 10));
setContentPane(contentPane);
// Top Navigation Bar
JPanel topPanel = new JPanel(new BorderLayout(8, 8));
topPanel.setBorder(BorderFactory.createTitledBorder("History Scrub Navigation"));
JPanel sliderPanel = new JPanel(new BorderLayout(6, 6));
btnPrev = new JButton("◄ Prev");
ThemeManager.styleButton(btnPrev, ThemeManager.ButtonVariant.DEFAULT);
btnPrev.addActionListener(e -> stepHistory(-1));
btnNext = new JButton("Next ►");
ThemeManager.styleButton(btnNext, ThemeManager.ButtonVariant.DEFAULT);
btnNext.addActionListener(e -> stepHistory(1));
scrubSlider = new JSlider(1, 1, 1);
scrubSlider.setMajorTickSpacing(10);
scrubSlider.setMinorTickSpacing(1);
scrubSlider.setPaintTicks(true);
scrubSlider.setSnapToTicks(true);
scrubSlider.addChangeListener(e -> {
if (!scrubSlider.getValueIsAdjusting()) {
int selected = scrubSlider.getValue() - 1;
displayScreen(selected);
}
});
sliderPanel.add(btnPrev, BorderLayout.WEST);
sliderPanel.add(scrubSlider, BorderLayout.CENTER);
sliderPanel.add(btnNext, BorderLayout.EAST);
infoLabel = new JLabel("Screen 0 of 0", SwingConstants.CENTER);
infoLabel.setFont(infoLabel.getFont().deriveFont(Font.BOLD, 13f));
topPanel.add(sliderPanel, BorderLayout.CENTER);
topPanel.add(infoLabel, BorderLayout.SOUTH);
contentPane.add(topPanel, BorderLayout.NORTH);
// Center Terminal Preview Pane
previewArea = new JTextArea();
previewArea.setEditable(false);
previewArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
previewArea.setBackground(Color.BLACK);
previewArea.setForeground(new Color(0, 255, 65)); // 3270 Green Phosphor
previewArea.setCaretColor(Color.WHITE);
previewArea.setLineWrap(false);
previewArea.setMargin(new Insets(6, 6, 6, 6));
JScrollPane scrollPane = new JScrollPane(previewArea);
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
contentPane.add(scrollPane, BorderLayout.CENTER);
// Bottom Action Bar
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
btnCopyScreen = new JButton("Copy Screen");
ThemeManager.styleButton(btnCopyScreen, ThemeManager.ButtonVariant.DEFAULT);
btnCopyScreen.setToolTipText("Copy entire historical screen to clipboard");
btnCopyScreen.addActionListener(e -> copyCurrentScreen());
btnCopySelection = new JButton("Copy Selection");
ThemeManager.styleButton(btnCopySelection, ThemeManager.ButtonVariant.DEFAULT);
btnCopySelection.setToolTipText("Copy selected text to clipboard");
btnCopySelection.addActionListener(e -> copySelectedText());
btnCopyTable = new JButton("Copy as Table");
ThemeManager.styleButton(btnCopyTable, ThemeManager.ButtonVariant.DEFAULT);
btnCopyTable.setToolTipText("Copy screen text with trimmed trailing spaces per row");
btnCopyTable.addActionListener(e -> copyAsTable());
btnClose = new JButton("Close");
ThemeManager.styleButton(btnClose, ThemeManager.ButtonVariant.DEFAULT);
btnClose.addActionListener(e -> setVisible(false));
bottomPanel.add(btnCopyScreen);
bottomPanel.add(btnCopySelection);
bottomPanel.add(btnCopyTable);
bottomPanel.add(Box.createHorizontalStrut(12));
bottomPanel.add(btnClose);
contentPane.add(bottomPanel, BorderLayout.SOUTH);
}
private void setupKeyNavigation() {
// Keyboard shortcuts for rapid scrub navigation
Action prevAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { stepHistory(-1); }
};
Action nextAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { stepHistory(1); }
};
Action homeAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { displayScreen(0); }
};
Action endAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) {
if (historyBuffer != null) displayScreen(historyBuffer.size() - 1);
}
};
Action closeAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { setVisible(false); }
};
JRootPane root = getRootPane();
InputMap im = root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
ActionMap am = root.getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.ALT_DOWN_MASK), "prevScreen");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "prevScreen");
am.put("prevScreen", prevAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.ALT_DOWN_MASK), "nextScreen");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "nextScreen");
am.put("nextScreen", nextAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.ALT_DOWN_MASK), "homeScreen");
am.put("homeScreen", homeAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent.ALT_DOWN_MASK), "endScreen");
am.put("endScreen", endAction);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeDialog");
am.put("closeDialog", closeAction);
}
private void stepHistory(int delta) {
if (historyBuffer == null || historyBuffer.isEmpty()) return;
int target = currentIndex + delta;
target = Math.max(0, Math.min(target, historyBuffer.size() - 1));
displayScreen(target);
}
private void displayScreen(int index) {
if (historyBuffer == null || historyBuffer.isEmpty()) {
updateEmptyState();
return;
}
int total = historyBuffer.size();
index = Math.max(0, Math.min(index, total - 1));
currentIndex = index;
ScreenSnapshot snap = historyBuffer.get(index);
if (snap != null) {
previewArea.setText(snap.getText());
previewArea.setCaretPosition(0);
String timeStr = dateFormat.format(new Date(snap.getTimestamp()));
infoLabel.setText("Screen " + (index + 1) + " of " + total + " (" + timeStr + ") — "
+ snap.getRows() + "x" + snap.getCols() + ", Cursor: ["
+ (snap.getCursorRow() + 1) + "," + (snap.getCursorCol() + 1) + "]");
scrubSlider.setValue(index + 1);
}
btnPrev.setEnabled(index > 0);
btnNext.setEnabled(index < total - 1);
btnCopyScreen.setEnabled(true);
btnCopySelection.setEnabled(true);
btnCopyTable.setEnabled(true);
}
private void updateEmptyState() {
currentIndex = -1;
previewArea.setText("No screen history captured yet.\n\nScreens are automatically recorded as the host session receives updates or unlocks the keyboard.");
infoLabel.setText("Screen 0 of 0 — Empty History");
scrubSlider.setEnabled(false);
btnPrev.setEnabled(false);
btnNext.setEnabled(false);
btnCopyScreen.setEnabled(false);
btnCopySelection.setEnabled(false);
btnCopyTable.setEnabled(false);
}
/**
* Refresh slider bounds and select target index.
*/
public synchronized void refreshHistoryView(int targetIndex) {
if (historyBuffer == null || historyBuffer.isEmpty()) {
updateEmptyState();
return;
}
int total = historyBuffer.size();
scrubSlider.setEnabled(true);
scrubSlider.setMinimum(1);
scrubSlider.setMaximum(total);
int select = targetIndex >= 0 ? Math.min(targetIndex, total - 1) : total - 1;
displayScreen(select);
}
@Override
public void onHistoryUpdated(ScreenHistoryBuffer buffer, ScreenSnapshot newSnapshot) {
SwingUtilities.invokeLater(() -> {
boolean wasAtLatest = (currentIndex >= 0 && currentIndex == buffer.size() - 2);
int newTotal = buffer.size();
scrubSlider.setMaximum(newTotal);
if (wasAtLatest || currentIndex < 0) {
// Keep tracked with latest frame
displayScreen(newTotal - 1);
} else {
// Keep operator's active scrub position, update label
displayScreen(currentIndex);
}
});
}
private void copyCurrentScreen() {
if (historyBuffer == null || currentIndex < 0) return;
ScreenSnapshot snap = historyBuffer.get(currentIndex);
if (snap != null) {
setClipboardText(snap.getText());
}
}
private void copySelectedText() {
String selected = previewArea.getSelectedText();
if (selected != null && !selected.isEmpty()) {
setClipboardText(selected);
} else {
copyCurrentScreen();
}
}
private void copyAsTable() {
if (historyBuffer == null || currentIndex < 0) return;
ScreenSnapshot snap = historyBuffer.get(currentIndex);
if (snap != null) {
setClipboardText(snap.getFormattedTable());
}
}
private void setClipboardText(String text) {
try {
Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
cb.setContents(new StringSelection(text), null);
} catch (Exception e) {
Toolkit.getDefaultToolkit().beep();
}
}
}
@@ -42,6 +42,18 @@ public class SettingsDialog extends JDialog {
private JCheckBox blockPasteCheck;
private JCheckBox enablePasteFromExcelCheck;
private JCheckBox pasteStopAtProtectedCheck;
private JCheckBox pasteWordBreakCheck;
private JCheckBox trimRectSizingHandlesCheck;
private JCheckBox trimRectRemainAfterEditCheck;
// Phase 25: Hotspots & Mouse Wheel controls
private JCheckBox hotspotsEnabledCheck;
private JCheckBox hotspotsUrlCheck;
private JCheckBox hotspotsPfCheck;
private JCheckBox hotspotsMenuCheck;
private JCheckBox mouseWheelEnabledCheck;
private JTextField mouseWheelUpField;
private JTextField mouseWheelDownField;
// Connection tab
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
@@ -338,7 +350,17 @@ public class SettingsDialog extends JDialog {
JTabbedPane behaviorTabs = new JTabbedPane();
ThemeManager.styleTabbedPane(behaviorTabs);
behaviorTabs.addTab("General", createGeneralBehaviorPanel());
JPanel generalContent = createGeneralBehaviorPanel();
JScrollPane generalScroll = new JScrollPane(generalContent);
ThemeManager.styleScrollPane(generalScroll);
generalScroll.setBorder(BorderFactory.createEmptyBorder());
if (generalScroll.getViewport() != null) {
generalScroll.getViewport().setBackground(generalContent.getBackground());
}
generalScroll.getVerticalScrollBar().setUnitIncrement(16);
generalScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
behaviorTabs.addTab("General", generalScroll);
behaviorTabs.addTab("Keymap", createKeymapPanel());
panel.add(behaviorTabs, BorderLayout.CENTER);
@@ -346,12 +368,13 @@ public class SettingsDialog extends JDialog {
}
private JPanel createGeneralBehaviorPanel() {
JPanel panel = new JPanel();
ScrollablePanel panel = new ScrollablePanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// Group 1: Typing & Operational Modes
JPanel typingGroup = new JPanel(new GridBagLayout());
typingGroup.setAlignmentX(Component.LEFT_ALIGNMENT);
typingGroup.setBorder(BorderFactory.createTitledBorder("Typing & Operational Modes"));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 6, 4, 6);
@@ -402,6 +425,7 @@ public class SettingsDialog extends JDialog {
// Group 2: Selection & Clipboard
JPanel selGroup = new JPanel(new GridBagLayout());
selGroup.setAlignmentX(Component.LEFT_ALIGNMENT);
selGroup.setBorder(BorderFactory.createTitledBorder("Selection & Clipboard"));
GridBagConstraints gbcS = new GridBagConstraints();
gbcS.insets = new Insets(4, 6, 4, 6);
@@ -430,7 +454,106 @@ public class SettingsDialog extends JDialog {
gbcS.gridy = 3;
selGroup.add(pasteStopAtProtectedCheck, gbcS);
pasteWordBreakCheck = new JCheckBox("Paste without splitting words across fields", Settings.getPasteWordBreak());
ThemeManager.styleCheckBox(pasteWordBreakCheck);
gbcS.gridy = 4;
selGroup.add(pasteWordBreakCheck, gbcS);
trimRectSizingHandlesCheck = new JCheckBox("Trim rectangle sizing handles", Settings.getTrimRectSizingHandles());
ThemeManager.styleCheckBox(trimRectSizingHandlesCheck);
gbcS.gridy = 5;
selGroup.add(trimRectSizingHandlesCheck, gbcS);
trimRectRemainAfterEditCheck = new JCheckBox("Trim rectangle remains after cut/copy/paste", Settings.getTrimRectRemainAfterEdit());
ThemeManager.styleCheckBox(trimRectRemainAfterEditCheck);
gbcS.gridy = 6;
selGroup.add(trimRectRemainAfterEditCheck, gbcS);
panel.add(selGroup);
panel.add(Box.createVerticalStrut(10));
// Group 3: Hotspots & Mouse Wheel Navigation (Phase 25)
JPanel hotGroup = new JPanel(new GridBagLayout());
hotGroup.setAlignmentX(Component.LEFT_ALIGNMENT);
hotGroup.setBorder(BorderFactory.createTitledBorder("Hotspots & Mouse Wheel Navigation"));
GridBagConstraints gbcH = new GridBagConstraints();
gbcH.insets = new Insets(3, 6, 3, 6);
gbcH.anchor = GridBagConstraints.WEST;
gbcH.fill = GridBagConstraints.HORIZONTAL;
gbcH.gridx = 0;
gbcH.weightx = 1.0;
hotspotsEnabledCheck = new JCheckBox("Enable Interactive Hotspots (links, function keys, choices)", Settings.getHotspotsEnabled());
ThemeManager.styleCheckBox(hotspotsEnabledCheck);
gbcH.gridy = 0;
hotGroup.add(hotspotsEnabledCheck, gbcH);
hotspotsUrlCheck = new JCheckBox("Auto-detect URLs (http://, https://, ftp://, www)", Settings.getHotspotsUrlEnabled());
ThemeManager.styleCheckBox(hotspotsUrlCheck);
gbcH.gridy = 1;
gbcH.insets = new Insets(2, 24, 2, 6);
hotGroup.add(hotspotsUrlCheck, gbcH);
hotspotsPfCheck = new JCheckBox("Auto-detect Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER])", Settings.getHotspotsPfEnabled());
ThemeManager.styleCheckBox(hotspotsPfCheck);
gbcH.gridy = 2;
hotGroup.add(hotspotsPfCheck, gbcH);
hotspotsMenuCheck = new JCheckBox("Auto-detect Menu choices (\"1.\", \"2.\", \"===> 3\")", Settings.getHotspotsMenuChoicesEnabled());
ThemeManager.styleCheckBox(hotspotsMenuCheck);
gbcH.gridy = 3;
hotGroup.add(hotspotsMenuCheck, gbcH);
hotspotsEnabledCheck.addActionListener(e -> {
boolean en = hotspotsEnabledCheck.isSelected();
hotspotsUrlCheck.setEnabled(en);
hotspotsPfCheck.setEnabled(en);
hotspotsMenuCheck.setEnabled(en);
});
boolean initHotEn = hotspotsEnabledCheck.isSelected();
hotspotsUrlCheck.setEnabled(initHotEn);
hotspotsPfCheck.setEnabled(initHotEn);
hotspotsMenuCheck.setEnabled(initHotEn);
mouseWheelEnabledCheck = new JCheckBox("Enable Mouse Wheel Navigation (scroll up/down)", Settings.getMouseWheelEnabled());
ThemeManager.styleCheckBox(mouseWheelEnabledCheck);
gbcH.gridy = 4;
gbcH.insets = new Insets(6, 6, 3, 6);
hotGroup.add(mouseWheelEnabledCheck, gbcH);
JPanel wheelKeysPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
wheelKeysPanel.setOpaque(false);
JLabel lblUp = new JLabel("Wheel Up:");
mouseWheelUpField = new JTextField(Settings.getMouseWheelUpKey(), 6);
ThemeManager.styleTextField(mouseWheelUpField);
JLabel lblDown = new JLabel("Wheel Down:");
mouseWheelDownField = new JTextField(Settings.getMouseWheelDownKey(), 6);
ThemeManager.styleTextField(mouseWheelDownField);
wheelKeysPanel.add(lblUp);
wheelKeysPanel.add(mouseWheelUpField);
wheelKeysPanel.add(Box.createHorizontalStrut(12));
wheelKeysPanel.add(lblDown);
wheelKeysPanel.add(mouseWheelDownField);
mouseWheelEnabledCheck.addActionListener(e -> {
boolean mwEn = mouseWheelEnabledCheck.isSelected();
lblUp.setEnabled(mwEn);
mouseWheelUpField.setEnabled(mwEn);
lblDown.setEnabled(mwEn);
mouseWheelDownField.setEnabled(mwEn);
});
boolean initMwEn = mouseWheelEnabledCheck.isSelected();
lblUp.setEnabled(initMwEn);
mouseWheelUpField.setEnabled(initMwEn);
lblDown.setEnabled(initMwEn);
mouseWheelDownField.setEnabled(initMwEn);
gbcH.gridy = 5;
gbcH.insets = new Insets(2, 24, 2, 6);
hotGroup.add(wheelKeysPanel, gbcH);
panel.add(hotGroup);
panel.add(Box.createVerticalGlue());
return panel;
@@ -676,14 +799,23 @@ public class SettingsDialog extends JDialog {
monoPanel.add(p);
}
JPanel bottomPanel = new JPanel(new BorderLayout(8, 0));
JPanel bottomPanel = new JPanel();
bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.Y_AXIS));
bottomPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
fourColorOverrideCheck = new JCheckBox("Base 4-Color Override mode (3279 green/white/red/turquoise)", Settings.getFourColorOverride());
fourColorOverrideCheck = new JCheckBox("Base 4 Override Mode (3279)", Settings.getFourColorOverride());
fourColorOverrideCheck.setToolTipText("Base 4-Color Override mode (3279 green/white/red/turquoise)");
ThemeManager.styleCheckBox(fourColorOverrideCheck);
bottomPanel.add(fourColorOverrideCheck, BorderLayout.WEST);
fourColorOverrideCheck.setAlignmentX(Component.LEFT_ALIGNMENT);
bottomPanel.add(fourColorOverrideCheck);
JButton btnResetColors = new JButton("Reset to Defaults");
bottomPanel.add(Box.createVerticalStrut(4));
JPanel btnRow = new JPanel(new BorderLayout());
btnRow.setOpaque(false);
btnRow.setAlignmentX(Component.LEFT_ALIGNMENT);
JButton btnResetColors = new JButton("Restore Defaults");
ThemeManager.styleButton(btnResetColors, ThemeManager.ButtonVariant.DEFAULT);
btnResetColors.addActionListener(e -> {
for (int i=0; i<16; i++) {
@@ -701,7 +833,8 @@ public class SettingsDialog extends JDialog {
}
JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save.");
});
bottomPanel.add(btnResetColors, BorderLayout.EAST);
btnRow.add(btnResetColors, BorderLayout.EAST);
bottomPanel.add(btnRow);
main.add(hostPanel, BorderLayout.NORTH);
main.add(monoPanel, BorderLayout.CENTER);
@@ -870,7 +1003,7 @@ public class SettingsDialog extends JDialog {
btnPanel.add(btnReset);
// Import Keymap — import from .kmp file
JButton btnImportKmp = new JButton("Import Keymap...");
JButton btnImportKmp = new JButton("Import .kmp...");
ThemeManager.styleButton(btnImportKmp, ThemeManager.ButtonVariant.DEFAULT);
btnImportKmp.addActionListener(e -> {
JFileChooser chooser = new JFileChooser();
@@ -908,6 +1041,36 @@ public class SettingsDialog extends JDialog {
});
btnPanel.add(btnImportKmp);
// Export Keymap — export to .kmp file
JButton btnExportKmp = new JButton("Export .kmp...");
ThemeManager.styleButton(btnExportKmp, ThemeManager.ButtonVariant.DEFAULT);
btnExportKmp.addActionListener(e -> {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("Export Keymap Profile");
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("Keyboard Map Profile (*.kmp)", "kmp"));
chooser.setSelectedFile(new File("User.kmp"));
int ret = chooser.showSaveDialog(this);
if (ret == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile();
if (!f.getName().toLowerCase().endsWith(".kmp")) {
f = new File(f.getParentFile(), f.getName() + ".kmp");
}
try {
KeyBindings.exportKmp(f, tempKeyBindings);
JOptionPane.showMessageDialog(this,
"Successfully exported key bindings to " + f.getName(),
"Keymap Exported",
JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this,
"Failed to export keymap: " + ex.getMessage(),
"Export Error",
JOptionPane.ERROR_MESSAGE);
}
}
});
btnPanel.add(btnExportKmp);
main.add(new JScrollPane(table), BorderLayout.CENTER);
main.add(btnPanel, BorderLayout.SOUTH);
@@ -987,6 +1150,36 @@ public class SettingsDialog extends JDialog {
if (pasteStopAtProtectedCheck != null) {
Settings.setPasteStopAtProtectedLine(pasteStopAtProtectedCheck.isSelected());
}
if (pasteWordBreakCheck != null) {
Settings.setPasteWordBreak(pasteWordBreakCheck.isSelected());
}
if (trimRectSizingHandlesCheck != null) {
Settings.setTrimRectSizingHandles(trimRectSizingHandlesCheck.isSelected());
}
if (trimRectRemainAfterEditCheck != null) {
Settings.setTrimRectRemainAfterEdit(trimRectRemainAfterEditCheck.isSelected());
}
if (hotspotsEnabledCheck != null) {
Settings.setHotspotsEnabled(hotspotsEnabledCheck.isSelected());
}
if (hotspotsUrlCheck != null) {
Settings.setHotspotsUrlEnabled(hotspotsUrlCheck.isSelected());
}
if (hotspotsPfCheck != null) {
Settings.setHotspotsPfEnabled(hotspotsPfCheck.isSelected());
}
if (hotspotsMenuCheck != null) {
Settings.setHotspotsMenuChoicesEnabled(hotspotsMenuCheck.isSelected());
}
if (mouseWheelEnabledCheck != null) {
Settings.setMouseWheelEnabled(mouseWheelEnabledCheck.isSelected());
}
if (mouseWheelUpField != null) {
Settings.setMouseWheelUpKey(mouseWheelUpField.getText().trim());
}
if (mouseWheelDownField != null) {
Settings.setMouseWheelDownKey(mouseWheelDownField.getText().trim());
}
// Apply Connection
if (startupBehaviorBox != null) {
@@ -1133,6 +1326,43 @@ public class SettingsDialog extends JDialog {
if (pasteStopAtProtectedCheck != null) {
pasteStopAtProtectedCheck.setSelected(Settings.getPasteStopAtProtectedLine());
}
if (pasteWordBreakCheck != null) {
pasteWordBreakCheck.setSelected(Settings.getPasteWordBreak());
}
if (trimRectSizingHandlesCheck != null) {
trimRectSizingHandlesCheck.setSelected(Settings.getTrimRectSizingHandles());
}
if (trimRectRemainAfterEditCheck != null) {
trimRectRemainAfterEditCheck.setSelected(Settings.getTrimRectRemainAfterEdit());
}
if (hotspotsEnabledCheck != null) {
hotspotsEnabledCheck.setSelected(Settings.getHotspotsEnabled());
boolean en = Settings.getHotspotsEnabled();
if (hotspotsUrlCheck != null) {
hotspotsUrlCheck.setSelected(Settings.getHotspotsUrlEnabled());
hotspotsUrlCheck.setEnabled(en);
}
if (hotspotsPfCheck != null) {
hotspotsPfCheck.setSelected(Settings.getHotspotsPfEnabled());
hotspotsPfCheck.setEnabled(en);
}
if (hotspotsMenuCheck != null) {
hotspotsMenuCheck.setSelected(Settings.getHotspotsMenuChoicesEnabled());
hotspotsMenuCheck.setEnabled(en);
}
}
if (mouseWheelEnabledCheck != null) {
boolean mwEn = Settings.getMouseWheelEnabled();
mouseWheelEnabledCheck.setSelected(mwEn);
if (mouseWheelUpField != null) {
mouseWheelUpField.setText(Settings.getMouseWheelUpKey());
mouseWheelUpField.setEnabled(mwEn);
}
if (mouseWheelDownField != null) {
mouseWheelDownField.setText(Settings.getMouseWheelDownKey());
mouseWheelDownField.setEnabled(mwEn);
}
}
// Connection
if (startupBehaviorBox != null) {
@@ -1302,4 +1532,39 @@ public class SettingsDialog extends JDialog {
}
}
}
private static class ScrollablePanel extends JPanel implements Scrollable {
public ScrollablePanel() {
super();
}
public ScrollablePanel(LayoutManager layout) {
super(layout);
}
@Override
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return 16;
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
return 48;
}
@Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
@Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
}
}
@@ -29,6 +29,7 @@ public class StatusBar extends JPanel {
private Telnet3270Client client;
private TerminalPanel terminalPanel;
private haus.nightmare.lib3270j.ecl.ECLOIANotify oiaNotifyListener;
public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
@@ -131,8 +132,15 @@ public class StatusBar extends JPanel {
}
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
if (this.client != null && this.client.getOIA() != null && this.oiaNotifyListener != null) {
this.client.getOIA().unregisterOIAEvent(this.oiaNotifyListener);
}
this.client = client;
this.terminalPanel = terminalPanel;
if (this.client != null && this.client.getOIA() != null) {
this.oiaNotifyListener = event -> updateStatus();
this.client.getOIA().registerOIAEvent(this.oiaNotifyListener);
}
updateStatus();
}
@@ -388,4 +396,8 @@ public class StatusBar extends JPanel {
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
}
public String getLockStatusText() {
return lockStatus != null ? lockStatus.getText() : "";
}
}
@@ -25,8 +25,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
// Font and cell dimensions
private Font terminalFont;
private Font boldTerminalFont;
private int cellWidth;
private int cellHeight;
int cellWidth;
int cellHeight;
private int fontAscent;
private int fontDescent;
private int padding = 4;
@@ -74,8 +74,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private int selectionStartRow = -1, selectionStartCol = -1;
private int selectionEndRow = -1, selectionEndCol = -1;
private boolean isDragging = false;
private boolean isActivationClick = false;
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 102); // 40% alpha blend
public enum DragHandle {
NONE, NW, N, NE, E, SE, S, SW, W, MOVE
}
private boolean trimRectSizingHandles = false;
private DragHandle activeDragHandle = DragHandle.NONE;
private int dragAnchorRow = -1;
private int dragAnchorCol = -1;
private int initialBoxR1 = -1, initialBoxR2 = -1, initialBoxC1 = -1, initialBoxC2 = -1;
// ========== Search Highlight state ==========
private int searchHighlightAddr = -1;
private int searchHighlightLen = 0;
@@ -88,6 +98,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
private boolean resizeGuard = false;
private StatusBar statusBar = null;
// ========== Phase 25: Hotspots & Mouse Wheel ==========
private final HotspotEngine hotspotEngine = new HotspotEngine();
private HotspotEngine.Hotspot hoveredHotspot = null;
private boolean hotspotsEnabled = haus.nightmare.j3270.config.Settings.getHotspotsEnabled();
private boolean mouseWheelEnabled = haus.nightmare.j3270.config.Settings.getMouseWheelEnabled();
private String mouseWheelUpAction = haus.nightmare.j3270.config.Settings.getMouseWheelUpKey();
private String mouseWheelDownAction = haus.nightmare.j3270.config.Settings.getMouseWheelDownKey();
// ========== Mode and Listener State ==========
private boolean insertMode = false;
@@ -117,7 +135,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
/**
* Compute the horizontal render offset to center the grid within the panel.
*/
private int getRenderOffsetX() {
int getRenderOffsetX() {
int termCols = 80;
if (client != null) termCols = client.getScreenBuffer().getDisplayCols();
int gridW = termCols * cellWidth;
@@ -128,7 +146,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
/**
* Compute the vertical render offset to center the grid within the panel.
*/
private int getRenderOffsetY() {
int getRenderOffsetY() {
int termRows = 24;
if (client != null) termRows = client.getScreenBuffer().getDisplayRows();
int gridH = termRows * cellHeight;
@@ -178,6 +196,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
setupCursorBlink();
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
trimRectSizingHandles = haus.nightmare.j3270.config.Settings.getTrimRectSizingHandles();
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
@@ -186,7 +205,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
MouseAdapter mouseHandler = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
requestFocusInWindow();
Window win = SwingUtilities.getWindowAncestor(TerminalPanel.this);
boolean wasWindowInactive = (win != null && (!win.isActive() || !win.isFocused()));
boolean wasPanelUnfocused = !hasFocus();
isActivationClick = wasWindowInactive || wasPanelUnfocused;
if (wasWindowInactive) {
win.toFront();
win.requestFocus();
}
requestFocus();
// Right-click context menu
if (SwingUtilities.isRightMouseButton(e)) {
@@ -206,18 +234,30 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
row = Math.max(0, Math.min(row, displayRows - 1));
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
boolean isSelectableField = false;
if (sb.isFormatted()) {
int faPos = sb.findFieldAttribute(row * displayCols + col);
if (faPos >= 0) {
int fa = sb.getCell(faPos).fa & 0xFF;
isSelectableField = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa);
}
}
if (isGraphic || lightPenMode || isSelectableField) {
if (isGraphic || lightPenMode) {
clearSelection();
} else if (SwingUtilities.isLeftMouseButton(e) && trimRectSizingHandles && hasSelection()) {
DragHandle handle = getDragHandleAt(e.getX(), e.getY());
if (handle != DragHandle.NONE) {
activeDragHandle = handle;
isDragging = true;
dragAnchorRow = row;
dragAnchorCol = col;
initialBoxR1 = Math.min(selectionStartRow, selectionEndRow);
initialBoxR2 = Math.max(selectionStartRow, selectionEndRow);
initialBoxC1 = Math.min(selectionStartCol, selectionEndCol);
initialBoxC2 = Math.max(selectionStartCol, selectionEndCol);
repaint();
return;
} else {
activeDragHandle = DragHandle.NONE;
selectionStartRow = row;
selectionStartCol = col;
selectionEndRow = row;
selectionEndCol = col;
}
} else {
activeDragHandle = DragHandle.NONE;
selectionStartRow = row;
selectionStartCol = col;
selectionEndRow = row;
@@ -251,8 +291,43 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
col = Math.max(0, Math.min(col, sb.getDisplayCols() - 1));
row = Math.max(0, Math.min(row, sb.getDisplayRows() - 1));
selectionEndRow = row;
selectionEndCol = col;
if (activeDragHandle != DragHandle.NONE) {
if (activeDragHandle == DragHandle.MOVE) {
int deltaR = row - dragAnchorRow;
int deltaC = col - dragAnchorCol;
int h = initialBoxR2 - initialBoxR1;
int w = initialBoxC2 - initialBoxC1;
int newR1 = Math.max(0, Math.min(initialBoxR1 + deltaR, sb.getDisplayRows() - 1 - h));
int newC1 = Math.max(0, Math.min(initialBoxC1 + deltaC, sb.getDisplayCols() - 1 - w));
selectionStartRow = newR1;
selectionStartCol = newC1;
selectionEndRow = newR1 + h;
selectionEndCol = newC1 + w;
} else {
int newR1 = initialBoxR1;
int newR2 = initialBoxR2;
int newC1 = initialBoxC1;
int newC2 = initialBoxC2;
switch (activeDragHandle) {
case NW: newR1 = row; newC1 = col; break;
case N: newR1 = row; break;
case NE: newR1 = row; newC2 = col; break;
case E: newC2 = col; break;
case SE: newR2 = row; newC2 = col; break;
case S: newR2 = row; break;
case SW: newR2 = row; newC1 = col; break;
case W: newC1 = col; break;
default: break;
}
selectionStartRow = Math.min(newR1, newR2);
selectionEndRow = Math.max(newR1, newR2);
selectionStartCol = Math.min(newC1, newC2);
selectionEndCol = Math.max(newC1, newC2);
}
} else {
selectionEndRow = row;
selectionEndCol = col;
}
}
repaint();
}
@@ -274,12 +349,72 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
py = Math.max(0, Math.min(py, gHeight - 1));
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
}
// Phase 26.1: Drag handle cursor hover detection
if (trimRectSizingHandles && hasSelection() && client != null && client.getConnectionState().isFullSession()) {
DragHandle handle = getDragHandleAt(e.getX(), e.getY());
if (handle != DragHandle.NONE) {
setCursor(getCursorForDragHandle(handle));
setToolTipText(null);
return;
}
}
// Phase 25.2: Real-time hotspot hover detection
if (hotspotsEnabled && client != null && client.getScreenBuffer() != null) {
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getDisplayRows();
int cols = sb.getDisplayCols();
int col = (e.getX() - ox) / cellWidth;
int row = (e.getY() - oy) / cellHeight;
if (row >= 0 && row < rows && col >= 0 && col < cols) {
hotspotEngine.scan(sb);
HotspotEngine.Hotspot h = hotspotEngine.findHotspot(row, col);
if (h != null) {
if (hoveredHotspot != h) {
hoveredHotspot = h;
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
setToolTipText(h.getTooltip());
repaint();
}
return;
}
}
}
if (hoveredHotspot != null) {
hoveredHotspot = null;
setCursor(Cursor.getDefaultCursor());
setToolTipText(null);
repaint();
} else if (activeDragHandle == DragHandle.NONE) {
setCursor(Cursor.getDefaultCursor());
}
}
@Override
public void mouseExited(MouseEvent e) {
if (hoveredHotspot != null) {
hoveredHotspot = null;
setCursor(Cursor.getDefaultCursor());
setToolTipText(null);
repaint();
} else if (activeDragHandle == DragHandle.NONE) {
setCursor(Cursor.getDefaultCursor());
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (isDragging) {
isDragging = false;
if (activeDragHandle != DragHandle.NONE) {
activeDragHandle = DragHandle.NONE;
isActivationClick = false;
repaint();
return;
}
if (client != null && client.getConnectionState().isFullSession()) {
ScreenBuffer sb = client.getScreenBuffer();
int ox = getRenderOffsetX();
@@ -292,7 +427,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
row = Math.max(0, Math.min(row, displayRows - 1));
int clickAddr = row * displayCols + col;
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null;
boolean isGraphic = client.getGocaDecoder() != null &&
(client.getGocaDecoder().isGraphicsCursorActive() ||
(client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()));
if (isGraphic) {
clearSelection();
@@ -307,8 +444,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
py = Math.max(0, Math.min(py, gHeight - 1));
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
int gx = client.getGocaDecoder().getGraphicCursorX();
int gy = client.getGocaDecoder().getGraphicCursorY();
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
client.getInputProcessor().sendGraphicMouseAid(
gx, gy,
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
button,
e.isShiftDown(),
@@ -318,25 +458,75 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
return;
}
// Automatic Light-Pen / Selectable Field detection on mouse click:
boolean wasDrag = (selectionStartRow != -1 && selectionEndRow != -1 &&
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol));
boolean activation = isActivationClick;
isActivationClick = false;
if (wasDrag) {
// Preserve drag/block selection so user can interact with menus or press Ctrl+C
repaint();
return;
}
// Single click (not a drag):
clearSelection();
sb.setCursorAddress(clickAddr);
// If this was an activation click (e.g. clicking to resume focus),
// do not fire field actions or send AIDs
if (activation) {
refreshScreen();
return;
}
// Phase 25.2: Interactive Hotspot click execution (URLs, PF keys, choices)
if (hotspotsEnabled && client.getConnectionState().isFullSession()) {
HotspotEngine.Hotspot h = hotspotEngine.findHotspot(row, col);
if (h != null) {
if (h.getType() == HotspotEngine.HotspotType.URL) {
openUrl(h.getAction());
return;
} else {
client.getPS().SendKeys(h.getAction());
refreshScreen();
return;
}
}
}
// Automatic Light-Pen / Selectable Field detection on mouse click (HoD parity):
if (sb.isFormatted()) {
int faPos = sb.findFieldAttribute(clickAddr);
if (faPos >= 0) {
int fa = sb.getCell(faPos).fa & 0xFF;
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
clearSelection();
if (client.lightPenSelect(clickAddr)) {
refreshScreen();
return;
int size = displayRows * displayCols;
int desPos = (faPos + 1) % size;
char desAscii = (char) sb.getCell(desPos).ucs4;
int desEbcdic = sb.getCell(desPos).ec & 0xFF;
boolean isToggleField = (desEbcdic == 0x6F || desAscii == '?' || desAscii == '\uFF1F' ||
desEbcdic == 0x6E || desAscii == '>' || desAscii == '\uFF1E');
boolean isEnterField = (desEbcdic == 0x50 || desAscii == '&' || desAscii == '\uFF06');
boolean isProtected = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa);
boolean isDoubleClick = (e.getClickCount() >= 2);
// For detectable fields (FA bits 4-5 = 01 or 11):
// Double-clicking or invoking mnemonic [cursel] toggles the designator character (? -> >).
// If field has immediate select attribute, immediately transmit Cursor Select AID (0x7E).
if (lightPenMode || isDoubleClick || isToggleField || isEnterField || !isProtected) {
if (client.lightPenSelect(clickAddr)) {
refreshScreen();
return;
}
}
}
}
}
if (lightPenMode) {
clearSelection();
sb.setCursorAddress(clickAddr);
boolean result = client.lightPenSelect(clickAddr);
if (result) {
refreshScreen();
@@ -346,12 +536,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
}
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
sb.setCursorAddress(clickAddr);
clearSelection();
refreshScreen();
return;
}
refreshScreen();
return;
}
repaint();
}
@@ -360,6 +546,21 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
addMouseListener(mouseHandler);
addMouseMotionListener(mouseHandler);
// Phase 25.2: Mouse wheel navigation (Wheel Up -> [pf7], Wheel Down -> [pf8])
addMouseWheelListener(e -> {
if (!mouseWheelEnabled || client == null || !client.getConnectionState().isFullSession()) {
return;
}
int rotation = e.getWheelRotation();
if (rotation < 0) {
client.getPS().SendKeys(mouseWheelUpAction);
refreshScreen();
} else if (rotation > 0) {
client.getPS().SendKeys(mouseWheelDownAction);
refreshScreen();
}
});
// Handle resize: keep user configured font persistent without auto-fit reset
addComponentListener(new ComponentAdapter() {
@Override
@@ -370,8 +571,104 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
});
}
// ========== Phase 25: Hotspot URL Opener ==========
private void openUrl(String url) {
if (url == null || url.trim().isEmpty()) return;
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(new java.net.URI(url));
return;
}
} catch (Exception ignored) {
}
// Fallback: command line launcher
try {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
Runtime.getRuntime().exec(new String[]{"rundll32", "url.dll,FileProtocolHandler", url});
} else if (os.contains("mac")) {
Runtime.getRuntime().exec(new String[]{"open", url});
} else {
Runtime.getRuntime().exec(new String[]{"xdg-open", url});
}
} catch (Exception ex) {
Toolkit.getDefaultToolkit().beep();
}
}
public HotspotEngine getHotspotEngine() {
return hotspotEngine;
}
// ========== Selection / Copy-Paste ==========
public boolean isBlockSelectMode() {
return blockSelectMode;
}
public void setBlockSelectMode(boolean val) {
this.blockSelectMode = val;
repaint();
}
public boolean isTrimRectSizingHandles() {
return trimRectSizingHandles;
}
public void setTrimRectSizingHandles(boolean val) {
this.trimRectSizingHandles = val;
repaint();
}
public DragHandle getDragHandleAt(int mx, int my) {
if (!hasSelection() || client == null) return DragHandle.NONE;
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
int r1 = Math.min(selectionStartRow, selectionEndRow);
int r2 = Math.max(selectionStartRow, selectionEndRow);
int c1 = Math.min(selectionStartCol, selectionEndCol);
int c2 = Math.max(selectionStartCol, selectionEndCol);
int x1 = ox + c1 * cellWidth;
int y1 = oy + r1 * cellHeight;
int x2 = ox + (c2 + 1) * cellWidth;
int y2 = oy + (r2 + 1) * cellHeight;
int xm = (x1 + x2) / 2;
int ym = (y1 + y2) / 2;
int tol = 6;
if (Math.abs(mx - x1) <= tol && Math.abs(my - y1) <= tol) return DragHandle.NW;
if (Math.abs(mx - xm) <= tol && Math.abs(my - y1) <= tol) return DragHandle.N;
if (Math.abs(mx - x2) <= tol && Math.abs(my - y1) <= tol) return DragHandle.NE;
if (Math.abs(mx - x2) <= tol && Math.abs(my - ym) <= tol) return DragHandle.E;
if (Math.abs(mx - x2) <= tol && Math.abs(my - y2) <= tol) return DragHandle.SE;
if (Math.abs(mx - xm) <= tol && Math.abs(my - y2) <= tol) return DragHandle.S;
if (Math.abs(mx - x1) <= tol && Math.abs(my - y2) <= tol) return DragHandle.SW;
if (Math.abs(mx - x1) <= tol && Math.abs(my - ym) <= tol) return DragHandle.W;
if (mx >= x1 && mx <= x2 && my >= y1 && my <= y2) {
return DragHandle.MOVE;
}
return DragHandle.NONE;
}
public Cursor getCursorForDragHandle(DragHandle handle) {
switch (handle) {
case NW: return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR);
case N: return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
case NE: return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR);
case E: return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
case SE: return Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR);
case S: return Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR);
case SW: return Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR);
case W: return Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR);
case MOVE: return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
default: return Cursor.getDefaultCursor();
}
}
public void clearSelection() {
selectionStartRow = -1;
selectionStartCol = -1;
@@ -417,23 +714,30 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int r2 = Math.max(selectionStartRow, selectionEndRow);
int c1, c2;
if (blockSelectMode) {
if (blockSelectMode || trimRectSizingHandles) {
c1 = Math.min(selectionStartCol, selectionEndCol);
c2 = Math.max(selectionStartCol, selectionEndCol);
StringBuilder result = new StringBuilder();
for (int r = r1; r <= r2; r++) {
if (r > r1) result.append('\n');
StringBuilder line = new StringBuilder();
for (int c = c1; c <= c2; c++) {
int baddr = r * cols + c;
ExtendedAttribute ea = sb.getCell(baddr);
char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
result.append(' ');
line.append(' ');
} else {
result.append(ch);
line.append(ch);
}
}
// Trim trailing whitespace on copied lines per HoD CutPasteOptions
int end = line.length();
while (end > 0 && line.charAt(end - 1) == ' ') {
end--;
}
result.append(line.substring(0, end));
}
return result.toString();
} else {
@@ -448,21 +752,34 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
StringBuilder result = new StringBuilder();
StringBuilder line = new StringBuilder();
int prevRow = startAddr / cols;
for (int addr = startAddr; addr <= endAddr; addr++) {
int curRow = addr / cols;
if (curRow != prevRow) {
result.append('\n');
int end = line.length();
while (end > 0 && line.charAt(end - 1) == ' ') {
end--;
}
if (result.length() > 0) result.append('\n');
result.append(line.substring(0, end));
line.setLength(0);
prevRow = curRow;
}
ExtendedAttribute ea = sb.getCell(addr);
char ch = ea.ucs4;
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
result.append(' ');
line.append(' ');
} else {
result.append(ch);
line.append(ch);
}
}
int end = line.length();
while (end > 0 && line.charAt(end - 1) == ' ') {
end--;
}
if (result.length() > 0) result.append('\n');
result.append(line.substring(0, end));
return result.toString();
}
}
@@ -473,6 +790,65 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
StringSelection ss = new StringSelection(text);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
}
if (!haus.nightmare.j3270.config.Settings.getTrimRectRemainAfterEdit()) {
clearSelection();
repaint();
}
}
public void cutSelection() {
if (!hasSelection() || client == null || !client.getConnectionState().isFullSession()) return;
String text = getSelectedText();
if (!text.isEmpty()) {
StringSelection ss = new StringSelection(text);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
}
ScreenBuffer sb = client.getScreenBuffer();
int cols = sb.getCols();
int r1 = Math.min(selectionStartRow, selectionEndRow);
int r2 = Math.max(selectionStartRow, selectionEndRow);
int c1 = Math.min(selectionStartCol, selectionEndCol);
int c2 = Math.max(selectionStartCol, selectionEndCol);
if (blockSelectMode || trimRectSizingHandles) {
for (int r = r1; r <= r2; r++) {
for (int c = c1; c <= c2; c++) {
int baddr = r * cols + c;
if (!sb.isProtected(baddr) && !sb.getCell(baddr).isFieldAttribute()) {
ExtendedAttribute ea = sb.getCell(baddr);
ea.ucs4 = ' ';
ea.ec = (byte) 0x40;
if (sb.isFormatted()) {
int faAddr = sb.findFieldAttribute(baddr);
if (faAddr >= 0) {
sb.getCell(faAddr).fa = (byte) (sb.getCell(faAddr).fa | haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY);
}
}
}
}
}
} else {
int startAddr = Math.min(selectionStartRow * cols + selectionStartCol, selectionEndRow * cols + selectionEndCol);
int endAddr = Math.max(selectionStartRow * cols + selectionStartCol, selectionEndRow * cols + selectionEndCol);
for (int baddr = startAddr; baddr <= endAddr; baddr++) {
if (!sb.isProtected(baddr) && !sb.getCell(baddr).isFieldAttribute()) {
ExtendedAttribute ea = sb.getCell(baddr);
ea.ucs4 = ' ';
ea.ec = (byte) 0x40;
if (sb.isFormatted()) {
int faAddr = sb.findFieldAttribute(baddr);
if (faAddr >= 0) {
sb.getCell(faAddr).fa = (byte) (sb.getCell(faAddr).fa | haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY);
}
}
}
}
}
sb.updateDisplaySnapshot();
if (!haus.nightmare.j3270.config.Settings.getTrimRectRemainAfterEdit()) {
clearSelection();
}
refreshScreen();
}
public void pasteClipboard() {
@@ -499,10 +875,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel();
boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine();
boolean blockPaste = haus.nightmare.j3270.config.Settings.getBlockPaste();
boolean wordBreak = haus.nightmare.j3270.config.Settings.getPasteWordBreak();
if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || blockPaste || text.contains("\t") || text.contains("\n") || text.contains("\r"))) {
client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected, blockPaste);
if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || blockPaste || wordBreak || text.contains("\t") || text.contains("\n") || text.contains("\r"))) {
client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected, blockPaste, wordBreak);
} else if (client.getPS() != null) {
client.getPS().setPasteWordBreak(wordBreak);
if (blockPaste) {
client.getPS().pasteRectangular(text, curRow, curCol);
} else {
@@ -518,6 +896,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
}
}
if (!haus.nightmare.j3270.config.Settings.getTrimRectRemainAfterEdit() && hasSelection()) {
clearSelection();
}
refreshScreen();
}
} catch (Exception ignored) {}
@@ -555,6 +936,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
JPopupMenu popup = new JPopupMenu();
ThemeManager.stylePopupMenu(popup);
JMenuItem cutItem = new JMenuItem("Cut");
ThemeManager.styleMenuItem(cutItem);
cutItem.setEnabled(hasSelection() && client != null && client.getConnectionState().isFullSession());
cutItem.addActionListener(ev -> cutSelection());
popup.add(cutItem);
JMenuItem copyItem = new JMenuItem("Copy");
ThemeManager.styleMenuItem(copyItem);
copyItem.setEnabled(hasSelection());
@@ -590,6 +977,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
});
popup.add(blockPasteItem);
JCheckBoxMenuItem handlesItem = new JCheckBoxMenuItem("Trim Sizing Handles", trimRectSizingHandles);
ThemeManager.styleMenuItem(handlesItem);
handlesItem.addActionListener(ev -> {
trimRectSizingHandles = handlesItem.isSelected();
haus.nightmare.j3270.config.Settings.setTrimRectSizingHandles(trimRectSizingHandles);
repaint();
});
popup.add(handlesItem);
popup.show(this, e.getX(), e.getY());
}
@@ -599,7 +995,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
int r1 = Math.min(selectionStartRow, selectionEndRow);
int r2 = Math.max(selectionStartRow, selectionEndRow);
if (blockSelectMode) {
if (blockSelectMode || trimRectSizingHandles) {
int c1 = Math.min(selectionStartCol, selectionEndCol);
int c2 = Math.max(selectionStartCol, selectionEndCol);
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
@@ -701,9 +1097,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
bindKeyToMap(im, "APL", haus.nightmare.j3270.config.Settings.getKeyBinding("APL", "alt F3"));
// Copy/Paste/Lightpen bindings
String defCut = haus.nightmare.j3270.config.Settings.isMac() ? "meta X, ctrl X" : "ctrl X, shift DELETE";
String defCopy = haus.nightmare.j3270.config.Settings.getDefaultBinding("COPY");
String defPaste = haus.nightmare.j3270.config.Settings.getDefaultBinding("PASTE");
String defSelectAll = haus.nightmare.j3270.config.Settings.getDefaultBinding("SELECTALL");
bindKeyToMap(im, "CUT", haus.nightmare.j3270.config.Settings.getKeyBinding("CUT", defCut));
bindKeyToMap(im, "COPY", haus.nightmare.j3270.config.Settings.getKeyBinding("COPY", defCopy));
bindKeyToMap(im, "PASTE", haus.nightmare.j3270.config.Settings.getKeyBinding("PASTE", defPaste));
bindKeyToMap(im, "SELECTALL", haus.nightmare.j3270.config.Settings.getKeyBinding("SELECTALL", defSelectAll));
@@ -740,6 +1138,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
am.put("j3270-CURSEL", createAction(this::handleCursorSelect));
am.put("j3270-CUT", createAction(this::cutSelection));
am.put("j3270-COPY", createAction(this::copySelection));
am.put("j3270-PASTE", createAction(this::pasteClipboard));
am.put("j3270-SELECTALL", createAction(this::selectAll));
@@ -1255,11 +1654,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
setupFont();
applyModeSettings();
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
trimRectSizingHandles = haus.nightmare.j3270.config.Settings.getTrimRectSizingHandles();
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
// Phase 25: Reload Hotspots and Mouse Wheel preferences
hotspotsEnabled = haus.nightmare.j3270.config.Settings.getHotspotsEnabled();
hotspotEngine.setUrlEnabled(haus.nightmare.j3270.config.Settings.getHotspotsUrlEnabled());
hotspotEngine.setPfKeyEnabled(haus.nightmare.j3270.config.Settings.getHotspotsPfEnabled());
hotspotEngine.setMenuChoiceEnabled(haus.nightmare.j3270.config.Settings.getHotspotsMenuChoicesEnabled());
hotspotEngine.invalidateCache();
mouseWheelEnabled = haus.nightmare.j3270.config.Settings.getMouseWheelEnabled();
mouseWheelUpAction = haus.nightmare.j3270.config.Settings.getMouseWheelUpKey();
mouseWheelDownAction = haus.nightmare.j3270.config.Settings.getMouseWheelDownKey();
revalidate();
repaint();
}
@@ -1309,7 +1720,41 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
blinkTimer = new Timer(500, e -> {
cursorVisible = !cursorVisible;
textBlinkVisible = !textBlinkVisible;
repaint();
// Check if there are any blinking cells or fields on the screen
boolean hasBlinkingCells = false;
ScreenBuffer sb = (client != null) ? client.getScreenBuffer() : null;
if (sb != null) {
int total = sb.getDisplayRows() * sb.getDisplayCols();
byte curFaGr = 0;
for (int i = 0; i < total; i++) {
ExtendedAttribute c = sb.getDisplayCell(i);
if (c.isFieldAttribute()) {
curFaGr = c.gr;
}
if ((c.gr & ExtendedAttribute.GR_BLINK) != 0 || (curFaGr & ExtendedAttribute.GR_BLINK) != 0) {
hasBlinkingCells = true;
break;
}
}
}
if (hasBlinkingCells) {
repaint();
} else if (sb != null && client != null && client.getConnectionState() != null && client.getConnectionState().isFullSession()) {
// Only repaint the cursor cell bounding rect
int cols = sb.getDisplayCols();
int ox = getRenderOffsetX();
int oy = getRenderOffsetY();
int curAddr = sb.getDisplayCursorAddress();
int curRow = curAddr / cols;
int curCol = curAddr % cols;
int cx = ox + curCol * cellWidth;
int cy = oy + curRow * cellHeight;
repaint(cx, cy, cellWidth, cellHeight);
} else {
repaint();
}
});
blinkTimer.start();
}
@@ -1575,6 +2020,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
}
// Field Outlining (0xC2 / TY_FOUTLINE)
drawOutlining(g2, ea, currentFieldEa, baddr, row, col, x, y, cellWidth, cellHeight, rows, cols, sb, fgColor);
if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR);
g2.fillRect(x, y, cellWidth, cellHeight);
@@ -1594,7 +2042,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
// Check text blink (500ms cycle)
boolean blink = (gr & GR_BLINK) != 0;
boolean blink = (gr & GR_BLINK) != 0 || (currentFieldEa != null && (currentFieldEa.gr & GR_BLINK) != 0);
boolean suppressGlyph = blink && !textBlinkVisible;
// Draw character or Programmed Symbol
@@ -1628,12 +2076,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
}
if (underline) {
if (underline && !suppressGlyph) {
g2.setColor(fgColor);
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
g2.drawLine(x, ulY, x + cellWidth - 1, ulY);
}
// Field Outlining (0xC2 / TY_FOUTLINE)
drawOutlining(g2, ea, currentFieldEa, baddr, row, col, x, y, cellWidth, cellHeight, rows, cols, sb, fgColor);
// Draw search highlight
if (isCellSearchHighlighted(row, col)) {
g2.setColor(SEARCH_HIGHLIGHT_COLOR);
@@ -1648,6 +2099,63 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
}
}
// Phase 26.1: Rectangular Block Selection Sizing Handles & Box Outline
if (trimRectSizingHandles && hasSelection() && client != null && client.getConnectionState().isFullSession()) {
int r1 = Math.min(selectionStartRow, selectionEndRow);
int r2 = Math.max(selectionStartRow, selectionEndRow);
int c1 = Math.min(selectionStartCol, selectionEndCol);
int c2 = Math.max(selectionStartCol, selectionEndCol);
int x1 = ox + c1 * cellWidth;
int y1 = oy + r1 * cellHeight;
int x2 = ox + (c2 + 1) * cellWidth;
int y2 = oy + (r2 + 1) * cellHeight;
int w = x2 - x1;
int h = y2 - y1;
int xm = (x1 + x2) / 2;
int ym = (y1 + y2) / 2;
Color oldColor = g2.getColor();
Stroke oldStroke = g2.getStroke();
g2.setColor(Color.YELLOW);
g2.drawRect(x1, y1, w, h);
drawSizingHandle(g2, x1, y1);
drawSizingHandle(g2, xm, y1);
drawSizingHandle(g2, x2, y1);
drawSizingHandle(g2, x2, ym);
drawSizingHandle(g2, x2, y2);
drawSizingHandle(g2, xm, y2);
drawSizingHandle(g2, x1, y2);
drawSizingHandle(g2, x1, ym);
g2.setStroke(oldStroke);
g2.setColor(oldColor);
}
// Phase 25.2: Draw visual underline on hovered hotspot
if (hotspotsEnabled && hoveredHotspot != null) {
Color oldColor = g2.getColor();
Stroke oldStroke = g2.getStroke();
g2.setColor(new Color(0, 220, 255)); // Bright cyan hotspot underline
g2.setStroke(new BasicStroke(2.0f));
int hr0 = hoveredHotspot.getStartRow();
int hr1 = hoveredHotspot.getEndRow();
for (int r = hr0; r <= hr1; r++) {
int c0 = (r == hr0) ? hoveredHotspot.getStartCol() : 0;
int c1 = (r == hr1) ? hoveredHotspot.getEndCol() : cols - 1;
int x1 = ox + c0 * cellWidth;
int x2 = ox + (c1 + 1) * cellWidth;
int ulY = oy + r * cellHeight + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2;
ulY = Math.min(oy + (r + 1) * cellHeight - 1, ulY);
g2.drawLine(x1, ulY, x2 - 1, ulY);
}
g2.setStroke(oldStroke);
g2.setColor(oldColor);
}
// Draw Graphic Cursor if active
boolean isGraphicCursor = (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive())
|| (client.getGraphicsPlane() != null && client.getGraphicsPlane().isGraphicCursorAttached());
@@ -1801,6 +2309,64 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal;
}
/**
* Render field outline lines around cells based on ea.ol or field attribute outline.
* Conforms to IBM 3270 Extended Data Stream / Host On-Demand DS3270 TY_FOUTLINE (0xC2).
*
* Bit 0 (0x01): Underline -> draw line at y + cellHeight - 1
* Bit 1 (0x02): Right vertical line -> draw line at x + cellWidth - 1
* Bit 2 (0x04): Overline -> draw line at y
* Bit 3 (0x08): Left vertical line -> draw line at x
*/
private void drawOutlining(Graphics2D g2, ExtendedAttribute ea, ExtendedAttribute currentFieldEa,
int baddr, int row, int col, int x, int y,
int cellWidth, int cellHeight, int rows, int cols,
ScreenBuffer sb, Color fgColor) {
int outline = ea.ol & 0xFF;
if (outline == 0 && currentFieldEa != null && currentFieldEa.ol != 0) {
int fol = currentFieldEa.ol & 0xFF;
if ((fol & ExtendedAttribute.OL_UNDERLINE) != 0) outline |= ExtendedAttribute.OL_UNDERLINE;
if ((fol & ExtendedAttribute.OL_OVERLINE) != 0) outline |= ExtendedAttribute.OL_OVERLINE;
int size = rows * cols;
int prevAddr = (baddr - 1 + size) % size;
if (sb.getDisplayCell(prevAddr).isFieldAttribute() || col == 0) {
if ((fol & ExtendedAttribute.OL_LEFT) != 0) outline |= ExtendedAttribute.OL_LEFT;
}
int nextAddr = (baddr + 1) % size;
if (sb.getDisplayCell(nextAddr).isFieldAttribute() || col == cols - 1) {
if ((fol & ExtendedAttribute.OL_RIGHT) != 0) outline |= ExtendedAttribute.OL_RIGHT;
}
}
if (outline != 0) {
g2.setColor(fgColor);
// Bit 0 (0x01): Underline -> draw line at y + cellHeight - 1
if ((outline & ExtendedAttribute.OL_UNDERLINE) != 0) {
g2.drawLine(x, y + cellHeight - 1, x + cellWidth - 1, y + cellHeight - 1);
}
// Bit 1 (0x02): Right vertical line -> draw line at x + cellWidth - 1
if ((outline & ExtendedAttribute.OL_RIGHT) != 0) {
g2.drawLine(x + cellWidth - 1, y, x + cellWidth - 1, y + cellHeight - 1);
}
// Bit 2 (0x04): Overline -> draw line at y
if ((outline & ExtendedAttribute.OL_OVERLINE) != 0) {
g2.drawLine(x, y, x + cellWidth - 1, y);
}
// Bit 3 (0x08): Left vertical line -> draw line at x
if ((outline & ExtendedAttribute.OL_LEFT) != 0) {
g2.drawLine(x, y, x, y + cellHeight - 1);
}
}
}
private void drawSizingHandle(Graphics2D g2, int cx, int cy) {
g2.setColor(Color.YELLOW);
g2.fillRect(cx - 2, cy - 2, 5, 5);
g2.setColor(Color.BLACK);
g2.drawRect(cx - 2, cy - 2, 5, 5);
}
public void dispose() {
if (blinkTimer != null)
blinkTimer.stop();
@@ -0,0 +1,343 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.j3270.ui.ConnectDialog;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Frame;
import java.awt.GraphicsEnvironment;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
public class HostStorageAndProxyTest {
private List<SavedHost> originalHosts;
@BeforeEach
public void setUp() {
originalHosts = HostStorage.getSavedHosts();
HostStorage.clearAll();
}
@AfterEach
public void tearDown() {
HostStorage.clearAll();
HostStorage.saveAll(originalHosts);
}
@Test
@DisplayName("SavedHost correctly models per-connection proxy and converts to ConnectionConfig")
public void testSavedHostProxyToConnectionConfig() {
// 1. Host with SOCKS5 proxy and auth
SavedHost host1 = new SavedHost(
UUID.randomUUID().toString(),
"Prod Mainframe",
"mvs.corp.local",
2323,
4,
43,
80,
"TSO01",
false,
"TSO",
true,
true,
true,
"BOTH",
"037",
"SOCKS5",
"proxy.internal.corp",
1080,
"admin",
"secret123"
);
ConnectionConfig cfg1 = host1.toConnectionConfig();
assertEquals("mvs.corp.local", cfg1.getHost());
assertEquals(2323, cfg1.getPort());
assertTrue(cfg1.isUseTls());
assertEquals("TSO01", cfg1.getLuName());
assertEquals(ConnectionConfig.ProxyType.SOCKS5, cfg1.getProxyType());
assertEquals("proxy.internal.corp", cfg1.getProxyHost());
assertEquals(1080, cfg1.getProxyPort());
assertEquals("admin", cfg1.getProxyUsername());
assertEquals("secret123", cfg1.getProxyPassword());
// 2. Host with direct connection (NONE proxy)
SavedHost host2 = new SavedHost(
UUID.randomUUID().toString(),
"Local VM370",
"127.0.0.1",
23,
2,
24,
80,
"",
false,
"CMS",
false,
false,
false,
"BOTH",
"037",
"NONE",
"",
0,
"",
""
);
ConnectionConfig cfg2 = host2.toConnectionConfig();
assertEquals("127.0.0.1", cfg2.getHost());
assertEquals(23, cfg2.getPort());
assertFalse(cfg2.isUseTls());
assertEquals(ConnectionConfig.ProxyType.NONE, cfg2.getProxyType());
assertNull(cfg2.getProxyHost());
}
@Test
@DisplayName("HostStorage persists and restores multiple host profiles with isolated per-connection proxies")
public void testHostStoragePersistenceAndIsolation() {
SavedHost h1 = new SavedHost(
"id-1",
"System A (SOCKS5)",
"sysa.corp",
23,
4,
62,
160,
"",
false,
"TSO",
false,
true,
true,
"BOTH",
"037",
"SOCKS5",
"socks.gateway.net",
1080,
"alice",
"passA"
);
SavedHost h2 = new SavedHost(
"id-2",
"System B (HTTP CONNECT)",
"sysb.dmz",
992,
5,
27,
132,
"LU99",
false,
"TSO",
true,
true,
true,
"BOTH",
"1047",
"HTTP",
"squid.proxy.org",
3128,
"bob",
"passB"
);
SavedHost h3 = new SavedHost(
"id-3",
"System C (Direct)",
"sysc.lan",
23,
2,
24,
80,
"",
true, // autoConnect
"CMS",
false,
true,
true,
"BOTH",
"500",
"NONE",
"",
0,
"",
""
);
HostStorage.saveHost(h1);
HostStorage.saveHost(h2);
HostStorage.saveHost(h3);
List<SavedHost> loaded = HostStorage.getSavedHosts();
assertEquals(3, loaded.size());
SavedHost loadedA = HostStorage.findById("id-1");
assertNotNull(loadedA);
assertEquals("System A (SOCKS5)", loadedA.getName());
assertEquals("SOCKS5", loadedA.getProxyType());
assertEquals("socks.gateway.net", loadedA.getProxyHost());
assertEquals(1080, loadedA.getProxyPort());
assertEquals("alice", loadedA.getProxyUsername());
assertEquals("passA", loadedA.getProxyPassword());
SavedHost loadedB = HostStorage.findById("id-2");
assertNotNull(loadedB);
assertEquals("HTTP", loadedB.getProxyType());
assertEquals("squid.proxy.org", loadedB.getProxyHost());
assertEquals(3128, loadedB.getProxyPort());
assertEquals("bob", loadedB.getProxyUsername());
assertEquals("passB", loadedB.getProxyPassword());
SavedHost loadedC = HostStorage.findById("id-3");
assertNotNull(loadedC);
assertEquals("NONE", loadedC.getProxyType());
assertEquals("", loadedC.getProxyHost());
assertTrue(loadedC.isAutoConnect());
// Verify getAutoConnectHost returns System C with its own connection settings
SavedHost auto = HostStorage.getAutoConnectHost();
assertNotNull(auto);
assertEquals("id-3", auto.getId());
assertEquals(ConnectionConfig.ProxyType.NONE, auto.toConnectionConfig().getProxyType());
// Verify deleteHost
HostStorage.deleteHost("id-2");
assertEquals(2, HostStorage.getSavedHosts().size());
assertNull(HostStorage.findById("id-2"));
}
@Test
@DisplayName("HostStorage JSON matches a3270 format and parses a3270 JSON strings")
public void testA3270JsonFormatCompatibility() {
// String representative of a3270 saved_hosts preference JSON
String a3270Json = "[\n" +
" {\n" +
" \"id\": \"android-uuid-1\",\n" +
" \"name\": \"Dallas Mainframe\",\n" +
" \"host\": \"dallas.ibm.com\",\n" +
" \"port\": 2323,\n" +
" \"model\": 4,\n" +
" \"dynamicRows\": 62,\n" +
" \"dynamicCols\": 160,\n" +
" \"luName\": \"DAL01\",\n" +
" \"autoConnect\": false,\n" +
" \"hostType\": \"TSO\",\n" +
" \"useTls\": true,\n" +
" \"tlsVerifyCert\": false,\n" +
" \"tn3270e\": true,\n" +
" \"graphicsMode\": \"BOTH\",\n" +
" \"codePage\": \"037\",\n" +
" \"proxyType\": \"SOCKS5\",\n" +
" \"proxyHost\": \"10.20.30.40\",\n" +
" \"proxyPort\": 9050,\n" +
" \"proxyUsername\": \"dallasuser\",\n" +
" \"proxyPassword\": \"dallaspass\"\n" +
" }\n" +
"]";
List<SavedHost> parsed = HostStorage.parseHostsJson(a3270Json);
assertEquals(1, parsed.size());
SavedHost h = parsed.get(0);
assertEquals("android-uuid-1", h.getId());
assertEquals("Dallas Mainframe", h.getName());
assertEquals("dallas.ibm.com", h.getHost());
assertEquals(2323, h.getPort());
assertEquals(4, h.getModel());
assertTrue(h.isUseTls());
assertFalse(h.isTlsVerifyCert());
assertEquals("SOCKS5", h.getProxyType());
assertEquals("10.20.30.40", h.getProxyHost());
assertEquals(9050, h.getProxyPort());
assertEquals("dallasuser", h.getProxyUsername());
assertEquals("dallaspass", h.getProxyPassword());
// Roundtrip serialization
String serialized = HostStorage.serializeHostsJson(parsed);
List<SavedHost> roundtripped = HostStorage.parseHostsJson(serialized);
assertEquals(1, roundtripped.size());
SavedHost r = roundtripped.get(0);
assertEquals(h.getId(), r.getId());
assertEquals(h.getProxyType(), r.getProxyType());
assertEquals(h.getProxyHost(), r.getProxyHost());
assertEquals(h.getProxyPort(), r.getProxyPort());
assertEquals(h.getProxyUsername(), r.getProxyUsername());
assertEquals(h.getProxyPassword(), r.getProxyPassword());
}
@Test
@DisplayName("ConnectDialog loads profile proxy and applies per-connection proxy to ConnectionConfig")
public void testConnectDialogProxyHandling() {
if (!GraphicsEnvironment.isHeadless()) {
ConnectDialog dialog = new ConnectDialog((Frame) null);
dialog.setInitialHost("test.mainframe.com");
dialog.setInitialPort(23);
dialog.setInitialProxy(ConnectionConfig.ProxyType.SOCKS5, "127.0.0.1", 1080, "testuser", "testpass");
assertEquals(ConnectionConfig.ProxyType.SOCKS5, dialog.getSelectedProxyType());
assertEquals("127.0.0.1", dialog.getProxyHost());
assertEquals(1080, dialog.getProxyPort());
assertEquals("testuser", dialog.getProxyUsername());
assertEquals("testpass", dialog.getProxyPassword());
// Loading a profile updates dialog proxy fields
SavedHost profile = new SavedHost(
"p1", "Profile 1", "prod.ibm.net", 992, 4, 43, 80, "", false,
"TSO", true, true, true, "BOTH", "037",
"HTTP", "proxy.corp.net", 8080, "corpuser", "corppass"
);
dialog.loadProfile(profile);
assertEquals(ConnectionConfig.ProxyType.HTTP, dialog.getSelectedProxyType());
assertEquals("proxy.corp.net", dialog.getProxyHost());
assertEquals(8080, dialog.getProxyPort());
assertEquals("corpuser", dialog.getProxyUsername());
assertEquals("corppass", dialog.getProxyPassword());
dialog.dispose();
}
}
@Test
@DisplayName("SavedHost preserves all GraphicsMode settings across JSON serialization without clobbering to BOTH")
public void testSavedHostGraphicsModeSerializationRoundtrip() {
for (haus.nightmare.lib3270j.graphics.GraphicsMode gm : haus.nightmare.lib3270j.graphics.GraphicsMode.values()) {
SavedHost host = new SavedHost(
UUID.randomUUID().toString(),
"Gfx " + gm.name(),
"test.host.com",
23,
4,
24,
80,
"",
false,
"TSO",
false,
false,
true,
gm.name(),
"037",
"NONE",
"",
0,
"",
""
);
String json = HostStorage.serializeHostsJson(List.of(host));
List<SavedHost> parsed = HostStorage.parseHostsJson(json);
assertEquals(1, parsed.size());
assertEquals(gm.name(), parsed.get(0).getGraphicsMode(),
"Graphics mode " + gm.name() + " must be preserved and not coerced to BOTH");
assertEquals(gm, parsed.get(0).toConnectionConfig().getGraphicsMode(),
"ConnectionConfig must reflect GraphicsMode." + gm.name());
}
}
}
@@ -0,0 +1,192 @@
package haus.nightmare.j3270.ui;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for HotspotEngine (Phase 25.2).
* Verifies regex detection of URLs, PF keys, Enter, and menu choices,
* hit-testing, action strings, and word boundary isolation.
*/
public class HotspotEngineTest {
private HotspotEngine engine;
@BeforeEach
public void setUp() {
engine = new HotspotEngine();
}
@Test
@DisplayName("Detects HTTP, HTTPS, FTP, and WWW URLs with action strings")
public void testUrlDetection() {
String screen = "Visit https://ibm.com/support or http://example.org:8080/path?q=1 for help.\n" +
"Download from ftp://ftp.archive.org/pub/files or browse www.google.com.";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 2, 80);
assertEquals(4, hotspots.size());
HotspotEngine.Hotspot h0 = hotspots.get(0);
assertEquals(HotspotEngine.HotspotType.URL, h0.getType());
assertEquals("https://ibm.com/support", h0.getDisplayText());
assertEquals("https://ibm.com/support", h0.getAction());
assertEquals(0, h0.getStartRow());
assertEquals(6, h0.getStartCol());
assertTrue(h0.contains(0, 10));
assertFalse(h0.contains(0, 5));
HotspotEngine.Hotspot h1 = hotspots.get(1);
assertEquals("http://example.org:8080/path?q=1", h1.getDisplayText());
assertEquals("http://example.org:8080/path?q=1", h1.getAction());
HotspotEngine.Hotspot h2 = hotspots.get(2);
assertEquals("ftp://ftp.archive.org/pub/files", h2.getDisplayText());
assertEquals("ftp://ftp.archive.org/pub/files", h2.getAction());
HotspotEngine.Hotspot h3 = hotspots.get(3);
assertEquals("www.google.com", h3.getDisplayText());
assertEquals("http://www.google.com", h3.getAction(), "www. URLs should have http:// prepended for action");
}
@Test
@DisplayName("Detects PF1..PF24, F1..F24, PA1..PA3, and [ENTER] function keys")
public void testFunctionKeyDetection() {
String screen = "PF1=Help PF3=Exit PF7=Up PF8=Down PF12=Cancel\n" +
"F1=Help F12=Cancel PA1=Abort [ENTER]=Continue";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 2, 80);
assertEquals(9, hotspots.size());
// PF1
assertEquals(HotspotEngine.HotspotType.PF_KEY, hotspots.get(0).getType());
assertEquals("PF1", hotspots.get(0).getDisplayText());
assertEquals("[pf1]", hotspots.get(0).getAction());
// PF3
assertEquals("PF3", hotspots.get(1).getDisplayText());
assertEquals("[pf3]", hotspots.get(1).getAction());
// PF7
assertEquals("PF7", hotspots.get(2).getDisplayText());
assertEquals("[pf7]", hotspots.get(2).getAction());
// PF8
assertEquals("PF8", hotspots.get(3).getDisplayText());
assertEquals("[pf8]", hotspots.get(3).getAction());
// PF12
assertEquals("PF12", hotspots.get(4).getDisplayText());
assertEquals("[pf12]", hotspots.get(4).getAction());
// F1 -> [pf1]
assertEquals("F1", hotspots.get(5).getDisplayText());
assertEquals("[pf1]", hotspots.get(5).getAction());
// F12 -> [pf12]
assertEquals("F12", hotspots.get(6).getDisplayText());
assertEquals("[pf12]", hotspots.get(6).getAction());
// PA1 -> [pa1]
assertEquals(HotspotEngine.HotspotType.PF_KEY, hotspots.get(7).getType());
assertEquals("PA1", hotspots.get(7).getDisplayText());
assertEquals("[pa1]", hotspots.get(7).getAction());
// [ENTER] -> [enter]
assertEquals(HotspotEngine.HotspotType.ENTER, hotspots.get(8).getType());
assertEquals("[ENTER]", hotspots.get(8).getDisplayText());
assertEquals("[enter]", hotspots.get(8).getAction());
}
@Test
@DisplayName("Isolates function keys with strict word boundaries to avoid false positives")
public void testWordBoundaryIsolation() {
String screen = "STAFF1 MEMBER SPF12 FILE TRANSFER PROMPT";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 1, 80);
// Neither STAFF1 nor SPF12 nor TRANSFER should be detected as PF1, F1, or F12
assertEquals(0, hotspots.size(), "Embedded letters in words should not trigger PF key hotspots");
}
@Test
@DisplayName("Detects numbered menu choices and arrow prompts")
public void testMenuChoiceDetection() {
String screen = " 1. User Settings\n" +
" 2. Session Profiles\n" +
"===> 3";
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 3, 80);
assertEquals(3, hotspots.size());
HotspotEngine.Hotspot m1 = hotspots.get(0);
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, m1.getType());
assertEquals("1.", m1.getDisplayText());
assertEquals("1[enter]", m1.getAction());
assertEquals(0, m1.getStartRow());
HotspotEngine.Hotspot m2 = hotspots.get(1);
assertEquals("2.", m2.getDisplayText());
assertEquals("2[enter]", m2.getAction());
assertEquals(1, m2.getStartRow());
HotspotEngine.Hotspot m3 = hotspots.get(2);
assertEquals("===> 3", m3.getDisplayText());
assertEquals("3[enter]", m3.getAction());
assertEquals(2, m3.getStartRow());
}
@Test
@DisplayName("Hit-testing finds hotspot under specific coordinate")
public void testFindHotspot() {
String screen = "PF3=Exit https://github.com ===> 1";
engine.scanText(screen, 1, 80);
// Under PF3 (cols 0..2)
HotspotEngine.Hotspot hPf = engine.findHotspot(0, 1);
assertNotNull(hPf);
assertEquals("PF3", hPf.getDisplayText());
// Space between hotspots
assertNull(engine.findHotspot(0, 5));
// Under URL (cols 11..28)
HotspotEngine.Hotspot hUrl = engine.findHotspot(0, 15);
assertNotNull(hUrl);
assertEquals("https://github.com", hUrl.getDisplayText());
// Under prompt (cols 32..37)
HotspotEngine.Hotspot hPrompt = engine.findHotspot(0, 34);
assertNotNull(hPrompt);
assertEquals("===> 1", hPrompt.getDisplayText());
}
@Test
@DisplayName("Toggling engine options enables or disables specific hotspot categories")
public void testTogglingCategories() {
String screen = "PF3=Exit https://example.com 1. Options";
engine.scanText(screen, 1, 80);
assertEquals(3, engine.getHotspots().size());
// Disable URLs
engine.setUrlEnabled(false);
List<HotspotEngine.Hotspot> noUrls = engine.scanText(screen, 1, 80);
assertEquals(2, noUrls.size());
assertFalse(noUrls.stream().anyMatch(h -> h.getType() == HotspotEngine.HotspotType.URL));
// Disable PF keys
engine.setPfKeyEnabled(false);
List<HotspotEngine.Hotspot> menuOnly = engine.scanText(screen, 1, 80);
assertEquals(1, menuOnly.size());
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, menuOnly.get(0).getType());
// Disable menu choices
engine.setMenuChoiceEnabled(false);
List<HotspotEngine.Hotspot> empty = engine.scanText(screen, 1, 80);
assertEquals(0, empty.size());
}
}
@@ -0,0 +1,265 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.Cursor;
import java.awt.HeadlessException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.LinkedHashMap;
import java.util.Map;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class SelectionHandlesAndTrimTest {
private boolean origRemainAfterEdit;
private boolean origSizingHandles;
@BeforeEach
public void setUp() {
origRemainAfterEdit = Settings.getTrimRectRemainAfterEdit();
origSizingHandles = Settings.getTrimRectSizingHandles();
}
@AfterEach
public void tearDown() {
Settings.setTrimRectRemainAfterEdit(origRemainAfterEdit);
Settings.setTrimRectSizingHandles(origSizingHandles);
}
private TerminalPanel createTestPanel() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
panel.setClient(client);
return panel;
}
@Test
@DisplayName("Trailing whitespace is trimmed on copied lines in block selection mode")
public void testBlockSelectionTrailingWhitespaceTrim() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
// Populate row 0: "HELLO "
String text0 = "HELLO";
for (int i = 0; i < text0.length(); i++) {
sb.getCell(i).ucs4 = text0.charAt(i);
}
// Populate row 1: "WORLD 123 "
String text1 = "WORLD 123";
for (int i = 0; i < text1.length(); i++) {
sb.getCell(cols + i).ucs4 = text1.charAt(i);
}
// Select rectangle from (row 0, col 0) to (row 1, col 20)
panel.setBlockSelectMode(true);
panel.setSelectionRange(0, cols + 20);
String copied = panel.getSelectedText();
// Should be "HELLO\nWORLD 123" without the trailing spaces up to col 20
assertEquals("HELLO\nWORLD 123", copied);
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Trailing whitespace is trimmed on copied lines in stream selection mode")
public void testStreamSelectionTrailingWhitespaceTrim() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
// Populate row 0: "LINE1 "
String text0 = "LINE1";
for (int i = 0; i < text0.length(); i++) {
sb.getCell(i).ucs4 = text0.charAt(i);
}
// Populate row 1: "LINE2"
String text1 = "LINE2";
for (int i = 0; i < text1.length(); i++) {
sb.getCell(cols + i).ucs4 = text1.charAt(i);
}
panel.setBlockSelectMode(false);
panel.setTrimRectSizingHandles(false);
// Select from row 0, col 0 to row 1, col 10
panel.setSelectionRange(0, cols + 10);
String copied = panel.getSelectedText();
assertEquals("LINE1\nLINE2", copied);
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Drag handle detection identifies 8 resize handles, MOVE inside box, and NONE outside")
public void testDragHandleDetection() {
try {
TerminalPanel panel = createTestPanel();
panel.setBlockSelectMode(true);
panel.setTrimRectSizingHandles(true);
// Without selection, should return NONE
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(100, 100));
// Select rectangle: rows 2 to 5, cols 10 to 20
ScreenBuffer sb = panel.getClient().getScreenBuffer();
int cols = sb.getCols();
panel.setSelectionRange(2 * cols + 10, 5 * cols + 20);
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
int cw = panel.cellWidth;
int ch = panel.cellHeight;
int x1 = ox + 10 * cw;
int y1 = oy + 2 * ch;
int x2 = ox + 21 * cw;
int y2 = oy + 6 * ch;
int xm = (x1 + x2) / 2;
int ym = (y1 + y2) / 2;
// Test 8 handles
assertEquals(TerminalPanel.DragHandle.NW, panel.getDragHandleAt(x1, y1));
assertEquals(TerminalPanel.DragHandle.N, panel.getDragHandleAt(xm, y1));
assertEquals(TerminalPanel.DragHandle.NE, panel.getDragHandleAt(x2, y1));
assertEquals(TerminalPanel.DragHandle.E, panel.getDragHandleAt(x2, ym));
assertEquals(TerminalPanel.DragHandle.SE, panel.getDragHandleAt(x2, y2));
assertEquals(TerminalPanel.DragHandle.S, panel.getDragHandleAt(xm, y2));
assertEquals(TerminalPanel.DragHandle.SW, panel.getDragHandleAt(x1, y2));
assertEquals(TerminalPanel.DragHandle.W, panel.getDragHandleAt(x1, ym));
// Test inside box -> MOVE
assertEquals(TerminalPanel.DragHandle.MOVE, panel.getDragHandleAt(xm, ym));
// Test far outside -> NONE
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(0, 0));
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(x2 + 50, y2 + 50));
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("Cursor mapping for all drag handles")
public void testCursorForDragHandles() {
try {
TerminalPanel panel = createTestPanel();
assertEquals(Cursor.NW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NW).getType());
assertEquals(Cursor.N_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.N).getType());
assertEquals(Cursor.NE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NE).getType());
assertEquals(Cursor.E_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.E).getType());
assertEquals(Cursor.SE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SE).getType());
assertEquals(Cursor.S_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.S).getType());
assertEquals(Cursor.SW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SW).getType());
assertEquals(Cursor.W_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.W).getType());
assertEquals(Cursor.MOVE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.MOVE).getType());
assertEquals(Cursor.DEFAULT_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NONE).getType());
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("trimRectRemainAfterEdit controls whether selection is retained after copy")
public void testTrimRectRemainAfterEdit() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
sb.getCell(0).ucs4 = 'A';
panel.setSelectionRange(0, 5);
assertTrue(panel.hasSelection());
// By default (false), copySelection() should clear the selection
Settings.setTrimRectRemainAfterEdit(false);
panel.copySelection();
assertFalse(panel.hasSelection(), "Selection should be cleared after copy when remainAfterEdit is false");
// When set to true, copySelection() should preserve the selection
Settings.setTrimRectRemainAfterEdit(true);
panel.setSelectionRange(0, 5);
assertTrue(panel.hasSelection());
panel.copySelection();
assertTrue(panel.hasSelection(), "Selection should remain active after copy when remainAfterEdit is true");
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("cutSelection clears unprotected cells, sets FA_MODIFY, and respects remainAfterEdit")
public void testCutSelectionBehavior() {
try {
TerminalPanel panel = createTestPanel();
ScreenBuffer sb = panel.getClient().getScreenBuffer();
// Field at 0: Unprotected field
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
String data = "TESTDATA";
for (int i = 0; i < data.length(); i++) {
sb.getCell(1 + i).ucs4 = data.charAt(i);
}
// Select the field contents
panel.setSelectionRange(1, 8);
assertTrue(panel.hasSelection());
Settings.setTrimRectRemainAfterEdit(false);
panel.cutSelection();
// Characters should be replaced with spaces
for (int i = 1; i <= 8; i++) {
assertEquals(' ', sb.getCell(i).ucs4, "Cell " + i + " should be erased to space");
}
// FA_MODIFY should remain or be set
assertTrue((sb.getCell(0).ec & FA_MODIFY) != 0, "FA_MODIFY should be set on field attribute");
assertFalse(panel.hasSelection(), "Selection should be cleared when remainAfterEdit is false");
} catch (HeadlessException ignored) {}
}
@Test
@DisplayName("KeyBindings exportKmp generates HoD format and round-trips through parseKmp")
public void testKeyBindingsExportAndParseRoundTrip() throws Exception {
Map<String, String> keymap = new LinkedHashMap<>();
keymap.put("ENTER", "ENTER");
keymap.put("shift TAB", "TAB");
keymap.put("CLEAR", "PAUSE");
keymap.put("PF1", "F1");
keymap.put("PF13", "shift F1");
keymap.put("ESCAPE", "ESCAPE");
StringWriter writer = new StringWriter();
KeyBindings.exportKmp(writer, keymap);
String exported = writer.toString();
assertTrue(exported.contains("[KeyRemap]"), "Exported KMP must contain [KeyRemap] section header");
assertTrue(exported.contains("[enter]"), "Exported KMP must contain HoD mnemonics");
assertTrue(exported.contains("[backtab]"), "Exported KMP must contain [backtab]");
// Roundtrip parse
Map<String, String> parsed = KeyBindings.parseKmp(new StringReader(exported));
assertNotNull(parsed);
assertEquals("ENTER", parsed.get("ENTER"));
assertEquals("TAB", parsed.get("shift TAB"));
assertEquals("PAUSE", parsed.get("CLEAR"));
assertEquals("F1", parsed.get("PF1"));
assertEquals("shift F1", parsed.get("PF13"));
assertEquals("ESCAPE", parsed.get("ESCAPE"));
}
}
@@ -59,4 +59,49 @@ public class StatusBarTest {
// Ignored in headless environments
}
}
@Test
public void testStatusBarXSystemAndXProt() {
try {
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_2, false);
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
StatusBar statusBar = new StatusBar();
statusBar.setClient(client, null);
statusBar.updateStatus();
// Initially not locked
assertEquals("", statusBar.getLockStatusText());
// 1. Setup protected field and type into it -> triggers X PROT
client.getScreenBuffer().setFieldAttribute(0, (byte) haus.nightmare.lib3270j.protocol.DS3270Constants.FA_PROTECT);
client.getScreenBuffer().setCursorAddress(1);
client.typeCharacter('A');
statusBar.updateStatus();
assertEquals("X PROT", statusBar.getLockStatusText());
// 2. Reset -> clears X PROT
client.reset();
statusBar.updateStatus();
assertEquals("", statusBar.getLockStatusText());
// 3. Send Enter -> triggers X SYSTEM
client.sendEnter();
statusBar.updateStatus();
assertEquals("X SYSTEM", statusBar.getLockStatusText());
// 4. Host unlocks keyboard
client.getInputProcessor().setKeyboardLocked(false);
statusBar.updateStatus();
assertEquals("", statusBar.getLockStatusText());
// 5. Subsequent Enter -> triggers X SYSTEM again
client.sendEnter();
statusBar.updateStatus();
assertEquals("X SYSTEM", statusBar.getLockStatusText());
} catch (HeadlessException e) {
// Ignored in headless environments
}
}
}
@@ -0,0 +1,101 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.HeadlessException;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
import static org.junit.jupiter.api.Assertions.*;
public class TerminalPanelSelectionTest {
@Test
@DisplayName("Selection drag over protected/intensified fields is preserved on mouse release (Bugs 3 & 4)")
public void testSelectionOverProtectedFieldsAndReleasePreservation() {
try {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a protected, intensified selectable field at pos 0 (ISPF header simulation)
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
String headerText = " ISPF PRIMARY OPTION MENU";
for (int i = 0; i < headerText.length(); i++) {
sb.getCell(1 + i).ucs4 = headerText.charAt(i);
sb.getCell(1 + i).ec = (byte) (0x40 + i);
}
panel.setClient(client);
// Cell dimensions in panel
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
int ox = panel.getRenderOffsetX();
int oy = panel.getRenderOffsetY();
MouseListener[] mouseListeners = panel.getMouseListeners();
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
assertTrue(mouseListeners.length > 0);
assertTrue(motionListeners.length > 0);
MouseListener ml = mouseListeners[0];
MouseMotionListener mml = motionListeners[0];
// 1. Mouse Pressed at cell (row 0, col 2)
int startX = ox + 2 * cellW + cellW / 2;
int startY = oy + cellH / 2;
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, startX, startY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(pressEvent);
// 2. Drag to cell (row 0, col 15)
int dragX = ox + 15 * cellW + cellW / 2;
int dragY = oy + cellH / 2;
MouseEvent dragEvent = new MouseEvent(panel, MouseEvent.MOUSE_DRAGGED, System.currentTimeMillis(),
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
mml.mouseDragged(dragEvent);
assertTrue(panel.hasSelection(), "Drag over protected field must create active selection (Bug 3)");
// 3. Mouse Released at cell (row 0, col 15)
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(releaseEvent);
// Selection MUST NOT be dropped upon mouse release! (Bug 4)
assertTrue(panel.hasSelection(), "Selection must remain active after mouse release (Bug 4)");
String selected = panel.getSelectedText();
assertNotNull(selected);
assertTrue(selected.length() > 0, "Selected text must not be empty");
// 4. A subsequent single click without drag should clear the selection and move the cursor
int clickX = ox + 5 * cellW + cellW / 2;
int clickY = oy + cellH / 2;
MouseEvent clickPress = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mousePressed(clickPress);
MouseEvent clickRelease = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
ml.mouseReleased(clickRelease);
assertFalse(panel.hasSelection(), "Single click must clear previous selection");
assertEquals(5, sb.getCursorAddress(), "Single click must position cursor at clicked cell");
} catch (HeadlessException e) {
// Handled gracefully in headless environments
}
}
}
@@ -0,0 +1,45 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Communications event matching IBM Host On-Demand ECLCommEvent.
*/
public class ECLCommEvent extends ECLEvent {
private final boolean status;
private final int eventType;
public ECLCommEvent(Object object, boolean status) {
super(object);
this.status = status;
this.eventType = status ? 1 : 2;
}
public ECLCommEvent(Object object, int eventType, boolean status) {
super(object);
this.eventType = eventType;
this.status = status;
}
public boolean connected() {
return this.status;
}
public boolean isConnected() {
return this.status;
}
public int getEventType() {
return this.eventType;
}
public ECLConnection GetConnection() {
Object src = GetSource();
if (src instanceof ECLConnection) return (ECLConnection) src;
if (src instanceof haus.nightmare.lib3270j.ecl.ECLConnection) {
return new ECLConnection((haus.nightmare.lib3270j.ecl.ECLConnection) src);
}
return null;
}
}
@@ -0,0 +1,10 @@
package com.ibm.eNetwork.ECL;
/**
* Communications notification interface matching IBM Host On-Demand ECLCommNotify.
*/
public interface ECLCommNotify {
void NotifyEvent(ECLConnection conn, boolean connected);
default void NotifyStop(ECLConnection conn, int reason) {}
default void NotifyError(ECLConnection conn, ECLErr err) {}
}
@@ -0,0 +1,79 @@
package com.ibm.eNetwork.ECL;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Communications and connection management facade matching IBM Host On-Demand ECLConnection.
*/
public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection implements ECLConstants {
private final List<ECLCommNotify> commNotifies = new CopyOnWriteArrayList<>();
public ECLConnection() {
super();
setupListener();
}
public ECLConnection(Properties props) {
super(props);
setupListener();
}
public ECLConnection(String host, int port) {
super(host, port);
setupListener();
}
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session) {
super(session);
setupListener();
}
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLConnection delegate) {
super(delegate != null ? delegate.getSession() : null);
setupListener();
}
private void setupListener() {
RegisterCommEvent(new haus.nightmare.lib3270j.ecl.ECLCommListener() {
@Override
public void CommNotifyEvent(haus.nightmare.lib3270j.ecl.ECLCommEvent event) {
boolean conn = event != null && event.isConnected();
for (ECLCommNotify n : commNotifies) {
try {
n.NotifyEvent(ECLConnection.this, conn);
} catch (Exception ignored) {}
}
}
});
}
public void RegisterCommEvent(ECLCommNotify notify) {
if (notify != null && !commNotifies.contains(notify)) {
commNotifies.add(notify);
}
}
public void registerCommEvent(ECLCommNotify notify) { RegisterCommEvent(notify); }
public void UnregisterCommEvent(ECLCommNotify notify) {
if (notify != null) {
commNotifies.remove(notify);
}
}
public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); }
@Override
public void StartCommunication() throws IOException {
super.StartCommunication();
}
@Override
public void StopCommunication() {
super.StopCommunication();
}
}
@@ -0,0 +1,138 @@
package com.ibm.eNetwork.ECL;
/**
* IBM Host Access Class Library (HACL) common constants.
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLConstants.
*/
public interface ECLConstants extends haus.nightmare.lib3270j.ecl.ECLConstants {
// Presentation space logical planes (HoD bitmask values)
int TEXT_PLANE = 1;
int COLOR_PLANE = 2;
int DBCS_PLANE = 4;
int GRID_PLANE = 8;
int FIELD_PLANE = 16;
int EXFIELD_PLANE = 32;
int NLS_PLANE = 64;
int HOST_PLANE = 128;
int RAWTEXT_PLANE = 256;
int COLOR_ATTRIBUTES_PLANE = 512;
int ALL_PLANES = 319;
// Search directions
int SEARCH_FORWARD = 1;
int SEARCH_BACKWARD = 2;
// Field filters
int ALL_FIELDS = 0;
int MODIFIED_FIELD = 1;
int ALPHANUM_FIELD = 2;
int HILIGHT_FIELD = 16;
int PROTECTED_FIELD = 32;
int DISPLAYABLE_FIELD = 64;
int PENDETECTABLE_FIELD = 128;
int UNMODIFIED_FIELD = 256;
int NUMERIC_FIELD = 512;
int LOLIGHT_FIELD = 4096;
int UNPROTECTED_FIELD = 8192;
int NON_DISPLAYABLE_FIELD = 16384;
int NON_PENDETECTABLE_FIELD = 32768;
// Standard Mnemonics Constants
String ENTER_STR = "[enter]";
String CLEAR_STR = "[clear]";
String RESET_STR = "[reset]";
String TAB_STR = "[tab]";
String FWDTAB_STR = "[tab]";
String BACKTAB_STR = "[backtab]";
String BACKSP_STR = "[backspace]";
String DELCHAR_STR = "[delete]";
String INSERT_STR = "[insert]";
String HOME_STR = "[home]";
String NEWLINE_STR = "[newline]";
String ENDLINE_STR = "[eof]";
String ERASEEOF_STR = "[eraseeof]";
String ERASEFLD_STR = "[erasefld]";
String ERASEINPUT_STR = "[erinp]";
String ATTN_STR = "[attn]";
String SYSREQ_STR = "[sysreq]";
String DUP_STR = "[dup]";
String FLDMRK_STR = "[fieldmark]";
String PA1_STR = "[pa1]";
String PA2_STR = "[pa2]";
String PA3_STR = "[pa3]";
String CURUP_STR = "[up]";
String CURDOWN_STR = "[down]";
String CURLEFT_STR = "[left]";
String CURRIGHT_STR = "[right]";
String PAGEUP_STR = "[pageup]";
String PAGEDWN_STR = "[pagedn]";
String COPY_STR = "[copy]";
String PASTE_STR = "[paste]";
String CUT_STR = "[cut]";
String SELECTALL_STR = "[selectall]";
// Function keys F1 - F24
String F1_STR = "[pf1]";
String F2_STR = "[pf2]";
String F3_STR = "[pf3]";
String F4_STR = "[pf4]";
String F5_STR = "[pf5]";
String F6_STR = "[pf6]";
String F7_STR = "[pf7]";
String F8_STR = "[pf8]";
String F9_STR = "[pf9]";
String F10_STR = "[pf10]";
String F11_STR = "[pf11]";
String F12_STR = "[pf12]";
String F13_STR = "[pf13]";
String F14_STR = "[pf14]";
String F15_STR = "[pf15]";
String F16_STR = "[pf16]";
String F17_STR = "[pf17]";
String F18_STR = "[pf18]";
String F19_STR = "[pf19]";
String F20_STR = "[pf20]";
String F21_STR = "[pf21]";
String F22_STR = "[pf22]";
String F23_STR = "[pf23]";
String F24_STR = "[pf24]";
// Shift F1 - F24
String SHIFT_F1_STR = "[Spf1]";
String SHIFT_F2_STR = "[Spf2]";
String SHIFT_F3_STR = "[Spf3]";
String SHIFT_F4_STR = "[Spf4]";
String SHIFT_F5_STR = "[Spf5]";
String SHIFT_F6_STR = "[Spf6]";
String SHIFT_F7_STR = "[Spf7]";
String SHIFT_F8_STR = "[Spf8]";
String SHIFT_F9_STR = "[Spf9]";
String SHIFT_F10_STR = "[Spf10]";
String SHIFT_F11_STR = "[Spf11]";
String SHIFT_F12_STR = "[Spf12]";
String SHIFT_F13_STR = "[Spf13]";
String SHIFT_F14_STR = "[Spf14]";
String SHIFT_F15_STR = "[Spf15]";
String SHIFT_F16_STR = "[Spf16]";
String SHIFT_F17_STR = "[Spf17]";
String SHIFT_F18_STR = "[Spf18]";
String SHIFT_F19_STR = "[Spf19]";
String SHIFT_F20_STR = "[Spf20]";
// Field attributes
short ATTR_PROTECTED = 32;
short ATTR_ALPHANUM = 16;
short ATTR_DISPLAY1 = 8;
short ATTR_DISPLAY2 = 4;
short ATTR_MDT = 1;
// Extended attributes
short EXT_ATTR_3270_UNDERLINE = 192;
short EXT_ATTR_3270_REVERSE = 128;
short EXT_ATTR_3270_BLINK = 64;
short EXT_ATTR_3270_EBCDIC = 0;
// Timeout
int NO_TIMEOUT = -1;
}
@@ -0,0 +1,54 @@
package com.ibm.eNetwork.ECL;
/**
* IBM Host Access Class Library (HACL) ECLErr checked exception facade.
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLErr.
*/
public class ECLErr extends haus.nightmare.lib3270j.ecl.ECLErr {
private static final long serialVersionUID = 1L;
public ECLErr() {
super();
}
public ECLErr(String text) {
super(text);
}
public ECLErr(String location, String msgNum) {
super(location, msgNum);
}
public ECLErr(String location, String msgNum, String insert) {
super(location, msgNum, insert);
}
public ECLErr(String location, String msgNum, String insert1, String insert2) {
super(location, msgNum, insert1, insert2);
}
public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) {
super(location, msgNum, insert1, insert2, log);
}
public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) {
super(location, msgNum, insert1, insert2, insert3);
}
public ECLErr(String location, String msgNum, int reasonCode) {
super(location, msgNum, reasonCode);
}
public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
super(location, msgNum, insert1, reasonCode);
}
public ECLErr(Throwable cause) {
super(cause);
}
public ECLErr(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,137 @@
package com.ibm.eNetwork.ECL;
/**
* Discrete 3270 field facade matching IBM Host On-Demand ECLField.
*/
public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements ECLConstants {
private final haus.nightmare.lib3270j.ecl.ECLField delegate;
public ECLField(haus.nightmare.lib3270j.ecl.ECLField delegate) {
super(delegate != null ? delegate.getPS() : null,
delegate != null ? delegate.getStart() : 0,
delegate != null ? delegate.getDataStart() : 0,
delegate != null ? delegate.getEnd() : 0,
delegate != null ? delegate.getLength() : 0,
(byte) (delegate != null ? delegate.getAttribute() : 0));
this.delegate = delegate;
}
public int GetStart() {
return delegate != null ? delegate.GetStart() + 1 : 1;
}
public int getStart() { return GetStart(); }
public int GetStartRow() {
return delegate != null ? delegate.GetStartRow() + 1 : 1;
}
public int getStartRow() { return GetStartRow(); }
public int GetStartCol() {
return delegate != null ? delegate.GetStartCol() + 1 : 1;
}
public int getStartCol() { return GetStartCol(); }
public int GetEnd() {
return delegate != null ? delegate.GetEnd() + 1 : 1;
}
public int getEnd() { return GetEnd(); }
public int GetEndRow() {
return delegate != null ? delegate.GetEndRow() + 1 : 1;
}
public int getEndRow() { return GetEndRow(); }
public int GetEndCol() {
return delegate != null ? delegate.GetEndCol() + 1 : 1;
}
public int getEndCol() { return GetEndCol(); }
public int GetLength() {
return delegate != null ? delegate.GetLength() : 0;
}
public int getLength() { return GetLength(); }
public String GetString() {
return delegate != null ? delegate.getString() : "";
}
public String getString() { return GetString(); }
public int GetString(char[] buffer, int len) throws ECLErr {
if (buffer == null) throw new ECLErr("ECLField:GetString", "ECL0011", "1");
String text = GetString();
int count = Math.min(len, Math.min(buffer.length, text.length()));
for (int i = 0; i < count; i++) {
buffer[i] = text.charAt(i);
}
return count;
}
public void SetString(String text) throws ECLErr {
if (delegate != null) {
delegate.setText(text);
}
}
public void SetText(String text) throws ECLErr {
SetString(text);
}
public void setText(String text) {
if (delegate != null) {
delegate.setText(text);
}
}
public boolean IsModified() {
return delegate != null && delegate.isModified();
}
public boolean isModified() { return IsModified(); }
public boolean IsProtected() {
return delegate != null && delegate.isProtected();
}
public boolean isProtected() { return IsProtected(); }
public boolean IsNumeric() {
return delegate != null && delegate.isNumeric();
}
public boolean isNumeric() { return IsNumeric(); }
public boolean IsHighIntensity() {
return delegate != null && delegate.isHighIntensity();
}
public boolean isHighIntensity() { return IsHighIntensity(); }
public boolean IsPenDetectable() {
return delegate != null && delegate.isPenDetectable();
}
public boolean isPenDetectable() { return IsPenDetectable(); }
public boolean IsDisplay() {
return delegate != null && delegate.isDisplay();
}
public boolean isDisplay() { return IsDisplay(); }
public short GetAttribute() {
return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0;
}
public short getAttribute() { return GetAttribute(); }
public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; }
}
@@ -0,0 +1,113 @@
package com.ibm.eNetwork.ECL;
import java.util.Vector;
/**
* Collection of 3270 fields matching IBM Host On-Demand ECLFieldList.
*/
public class ECLFieldList extends Vector<ECLField> implements ECLConstants {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLFieldList delegate;
private final ECLPS ps;
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate, ECLPS ps) {
this.delegate = delegate;
this.ps = ps;
refreshFields();
}
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate) {
this(delegate, null);
}
private void refreshFields() {
clear();
if (delegate != null) {
delegate.refresh();
for (haus.nightmare.lib3270j.ecl.ECLField f : delegate.getFields()) {
add(new ECLField(f));
}
}
}
public void Refresh() {
refreshFields();
}
public void Refresh(int planes) throws ECLErr {
refreshFields();
}
public void refresh() {
refreshFields();
}
public int GetFieldCount() {
return size();
}
public int getFieldCount() { return GetFieldCount(); }
public ECLField GetFirstField() {
return GetFirstField(0);
}
public ECLField GetFirstField(int filter) {
for (ECLField f : this) {
if (matchesFilter(f, filter)) return f;
}
return null;
}
public ECLField GetNextField(ECLField current) {
return GetNextField(current, 0);
}
public ECLField GetNextField(ECLField current, int filter) {
if (current == null) return GetFirstField(filter);
int idx = indexOf(current);
if (idx >= 0) {
for (int i = idx + 1; i < size(); i++) {
ECLField f = get(i);
if (matchesFilter(f, filter)) return f;
}
}
return null;
}
public ECLField FindField(int pos) throws ECLErr {
if (ps != null && (pos < 1 || pos > ps.GetSize())) {
throw new ECLErr("ECLFieldList:FindField", "ECL0010", "1", String.valueOf(pos));
}
for (ECLField f : this) {
int start = f.GetStart();
int end = f.GetEnd();
if (start <= end) {
if (pos >= start && pos <= end) return f;
} else {
// Wrapped field
if (pos >= start || pos <= end) return f;
}
}
return null;
}
public ECLField FindField(int row, int col) throws ECLErr {
if (ps == null) return null;
int pos = ps.ConvertRowColToPos(row, col);
return FindField(pos);
}
private boolean matchesFilter(ECLField f, int filter) {
if (filter == 0 || filter == ALL_FIELDS) return true;
if ((filter & MODIFIED_FIELD) != 0 && !f.IsModified()) return false;
if ((filter & PROTECTED_FIELD) != 0 && !f.IsProtected()) return false;
if ((filter & UNPROTECTED_FIELD) != 0 && f.IsProtected()) return false;
if ((filter & NUMERIC_FIELD) != 0 && !f.IsNumeric()) return false;
if ((filter & HILIGHT_FIELD) != 0 && !f.IsHighIntensity()) return false;
if ((filter & DISPLAYABLE_FIELD) != 0 && !f.IsDisplay()) return false;
return true;
}
}
@@ -0,0 +1,39 @@
package com.ibm.eNetwork.ECL;
import java.util.Properties;
/**
* Host Print Session facade matching IBM Host On-Demand ECLHostPrintSession.
*/
public class ECLHostPrintSession extends ECLSession {
public static final String SESSION_PRINT_SESSION_LABEL_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_TYPE_DEFAULT = "5";
public static final String SESSION_PRINT_SESSION_HOST_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_HOST_PORT_DEFAULT = "23";
public static final String SESSION_PRINT_SESSION_LU_NAME_DEFAULT = "";
public static final String SESSION_PRINT_SESSION_AUTOCONNECT_DEFAULT = "true";
public static final String SESSION_PRINT_SESSION_AUTORECONNECT_DEFAULT = "true";
public static final String SESSION_LU_TYPE = "printerLUType";
public ECLHostPrintSession(Properties properties) throws ECLErr {
super(properties);
}
public ECLHostPrintSession() throws ECLErr {
super();
}
public void cancelPrintJob() {
ECLPS ps = GetPS();
if (ps != null) {
try {
ps.SendKeys("[cancelpjob]");
} catch (Exception ignored) {}
}
}
public void CancelPrintJob() {
cancelPrintJob();
}
}
@@ -0,0 +1,130 @@
package com.ibm.eNetwork.ECL;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Operator Information Area facade matching IBM Host On-Demand ECLOIA.
*/
public class ECLOIA implements ECLConstants {
private final haus.nightmare.lib3270j.ecl.ECLOIA delegate;
private final List<ECLOIANotify> oiaNotifies = new CopyOnWriteArrayList<>();
public ECLOIA(haus.nightmare.lib3270j.ecl.ECLOIA delegate) {
this.delegate = delegate;
if (delegate != null) {
delegate.RegisterOIAEvent(new haus.nightmare.lib3270j.ecl.ECLOIAListener() {
@Override
public void OIANotifyEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent event) {
ECLOIAEvent facadeEvt = new ECLOIAEvent(event);
for (ECLOIANotify n : oiaNotifies) {
try {
n.OIANotifyEvent(facadeEvt);
} catch (Exception ignored) {}
}
}
});
}
}
public void RegisterOIAEvent(ECLOIANotify notify) {
if (notify != null && !oiaNotifies.contains(notify)) {
oiaNotifies.add(notify);
}
}
public void registerOIAEvent(ECLOIANotify notify) { RegisterOIAEvent(notify); }
public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
if (delegate != null) {
delegate.RegisterOIAEvent(listener);
}
}
public void UnregisterOIAEvent(ECLOIANotify notify) {
if (notify != null) {
oiaNotifies.remove(notify);
}
}
public void unregisterOIAEvent(ECLOIANotify notify) { UnregisterOIAEvent(notify); }
public void UnregisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
if (delegate != null) {
delegate.UnregisterOIAEvent(listener);
}
}
public boolean IsAlphanumeric() {
return delegate != null && delegate.IsAlphanumeric();
}
public boolean isAlphanumeric() { return IsAlphanumeric(); }
public boolean IsNumeric() {
return delegate != null && delegate.IsNumeric();
}
public boolean isNumeric() { return IsNumeric(); }
public boolean IsDBCS() {
return delegate != null && delegate.IsDBCS();
}
public boolean isDBCS() { return IsDBCS(); }
public boolean IsInsertMode() {
return delegate != null && delegate.IsInsertMode();
}
public boolean isInsertMode() { return IsInsertMode(); }
public boolean IsCommError() {
return delegate != null && delegate.IsCommError();
}
public boolean isCommError() { return IsCommError(); }
public boolean IsKeyLocked() {
return delegate != null && delegate.isKeyLocked();
}
public boolean isKeyLocked() { return IsKeyLocked(); }
public boolean IsMessageWaiting() {
return delegate != null && delegate.IsMessageWaiting();
}
public boolean isMessageWaiting() { return IsMessageWaiting(); }
public int GetInputInhibited() {
return delegate != null ? delegate.getInputInhibited() : 0;
}
public int getInputInhibited() { return GetInputInhibited(); }
public int GetAlphanumericType() {
return delegate != null ? delegate.getAlphanumericType() : 0;
}
public int getAlphanumericType() { return GetAlphanumericType(); }
public boolean WaitForInput(long timeout) {
return delegate != null && delegate.WaitForInput(timeout);
}
public boolean WaitForSystemAvailable(long timeout) {
return delegate != null && delegate.WaitForSystemAvailable(timeout);
}
public boolean WaitForAppAvailable(long timeout) {
return delegate != null && delegate.WaitForAppAvailable(timeout);
}
public boolean WaitForTransition(long timeout) {
return delegate != null && delegate.WaitForTransition(timeout);
}
public haus.nightmare.lib3270j.ecl.ECLOIA getDelegate() { return delegate; }
}
@@ -0,0 +1,80 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Event object dispatched on Operator Information Area (ECLOIA) status changes matching IBM HoD.
*/
public class ECLOIAEvent extends ECLEvent {
public String data;
public long newState;
public long changedMask;
private int inputInhibited = 0;
private int alphanumericType = 0;
private boolean insertMode = false;
public ECLOIAEvent(Object source, long newState, long changedMask, String data) {
super(source);
this.newState = newState;
this.changedMask = changedMask;
this.data = data != null ? data : "";
}
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
boolean insertMode, String statusString) {
super(source);
this.inputInhibited = inputInhibited;
this.alphanumericType = alphanumericType;
this.insertMode = insertMode;
this.data = statusString != null ? statusString : "";
this.newState = inputInhibited;
this.changedMask = eventType;
}
public ECLOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent delegate) {
super(delegate != null ? delegate.getSource() : null);
if (delegate != null) {
this.inputInhibited = delegate.getInputInhibited();
this.alphanumericType = delegate.getAlphanumericType();
this.insertMode = delegate.isInsertMode();
this.data = delegate.getStatusString();
this.newState = delegate.getInputInhibited();
this.changedMask = delegate.getEventType();
}
}
public ECLOIA GetOIA() {
Object src = GetSource();
if (src instanceof ECLOIA) {
return (ECLOIA) src;
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLOIA) {
return new ECLOIA((haus.nightmare.lib3270j.ecl.ECLOIA) src);
}
return null;
}
public ECLOIA getOIA() { return GetOIA(); }
public int GetInputInhibited() { return inputInhibited; }
public int getInputInhibited() { return inputInhibited; }
public int GetAlphanumericType() { return alphanumericType; }
public int getAlphanumericType() { return alphanumericType; }
public boolean IsInsertMode() { return insertMode; }
public boolean isInsertMode() { return insertMode; }
public boolean IsInputInhibited() { return inputInhibited != 0; }
public boolean isInputInhibited() { return IsInputInhibited(); }
public String GetStatusString() { return data; }
public String getStatusString() { return data; }
@Override
public String toString() {
return "ECLOIAEvent: changedMask=0x" + Long.toHexString(this.changedMask) +
", newState=0x" + Long.toHexString(this.newState) + ", data=" + this.data;
}
}
@@ -0,0 +1,17 @@
package com.ibm.eNetwork.ECL;
/**
* Operator Information Area notification interface matching IBM Host On-Demand ECLOIANotify.
*/
public interface ECLOIANotify {
void NotifyEvent(ECLOIA oia, int inhibitReason, int state, String text);
default void NotifyStop(ECLOIA oia, int reason) {}
default void NotifyError(ECLOIA oia, ECLErr err) {}
default void OIANotifyStop(ECLOIA oia, int reason) { NotifyStop(oia, reason); }
default void OIANotifyError(ECLOIA oia, ECLErr err) { NotifyError(oia, err); }
default void OIANotifyEvent(ECLOIAEvent event) {
if (event != null) {
NotifyEvent(event.GetOIA(), event.GetInputInhibited(), 0, "");
}
}
}
@@ -0,0 +1,514 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLPSListener;
import java.io.Serializable;
/**
* Presentation Space facade matching IBM Host On-Demand ECLPS.
* Provides 1-based indexing, method signature parity, and checked ECLErr exception semantics.
*/
public class ECLPS implements ECLConstants, Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLPS delegate;
public ECLPS(haus.nightmare.lib3270j.ecl.ECLPS delegate) {
this.delegate = delegate;
}
public ECLPS(haus.nightmare.lib3270j.ecl.ECLSession session) {
this(session != null ? session.GetPS() : null);
}
public ECLPS(ECLSession session) {
this(session != null ? session.getDelegate() : null);
}
public haus.nightmare.lib3270j.ecl.ECLPS getDelegate() {
return delegate;
}
// ========== Dimensions & Position Conversion (1-based) ==========
public int GetSize() {
return delegate != null ? delegate.GetSize() : 0;
}
public int getSize() { return GetSize(); }
public int GetRows() {
return delegate != null ? delegate.GetRows() : 0;
}
public int getRows() { return GetRows(); }
public int GetCols() {
return delegate != null ? delegate.GetCols() : 0;
}
public int getCols() { return GetCols(); }
public int GetSizeRows() { return GetRows(); }
public int GetSizeCols() { return GetCols(); }
public int ConvertRowColToPos(int row, int col) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertRowColToPos(row, col);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertRowColToPos(int row, int col) throws ECLErr {
return ConvertRowColToPos(row, col);
}
public int ConvertPosToRow(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToRow(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertPosToRow(int pos) throws ECLErr {
return ConvertPosToRow(pos);
}
public int ConvertPosToCol(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToCol(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 1;
}
public int convertPosToCol(int pos) throws ECLErr {
return ConvertPosToCol(pos);
}
public void ConvertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
if (delegate != null) {
try {
delegate.ConvertPosToRowCol(pos, rowCol);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void convertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
ConvertPosToRowCol(pos, rowCol);
}
public int[] ConvertPosToRowCol(int pos) throws ECLErr {
if (delegate != null) {
try {
return delegate.ConvertPosToRowCol(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return new int[]{1, 1};
}
public int[] convertPosToRowCol(int pos) throws ECLErr {
return ConvertPosToRowCol(pos);
}
// ========== Cursor Operations (1-based) ==========
public int GetCursorPos() {
return delegate != null ? delegate.GetCursorPos() : 1;
}
public int getCursorPos() { return GetCursorPos(); }
public int GetCursorRow() {
return delegate != null ? delegate.GetCursorRow() : 1;
}
public int getCursorRow() { return GetCursorRow(); }
public int GetCursorCol() {
return delegate != null ? delegate.GetCursorCol() : 1;
}
public int getCursorCol() { return GetCursorCol(); }
public void SetCursorPos(int pos) throws ECLErr {
if (delegate != null) {
try {
delegate.SetCursorPos(pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setCursorPos(int pos) throws ECLErr {
SetCursorPos(pos);
}
public void SetCursorPos(int row, int col) throws ECLErr {
if (delegate != null) {
try {
delegate.SetCursorPos(row, col);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setCursorPos(int row, int col) throws ECLErr {
SetCursorPos(row, col);
}
// ========== GetString & SetString Operations (1-based) ==========
public int GetString(char[] buffer, int len, int pos) throws ECLErr {
return GetString(buffer, len, pos, len);
}
public int GetString(char[] buffer, int len, int pos, int length) throws ECLErr {
if (delegate != null) {
try {
return delegate.GetString(buffer, len, pos, length);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return 0;
}
public int GetString(char[] buffer, int len, int row, int col, int length) throws ECLErr {
return GetString(buffer, len, ConvertRowColToPos(row, col), length);
}
public String GetString(int pos, int length) throws ECLErr {
if (delegate != null) {
try {
return delegate.GetString(pos, length);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return "";
}
public String getString(int pos, int length) throws ECLErr {
return GetString(pos, length);
}
public String GetString(int row, int col, int length) throws ECLErr {
return GetString(ConvertRowColToPos(row, col), length);
}
public String getString(int row, int col, int length) throws ECLErr {
return GetString(row, col, length);
}
public void SetString(String text, int pos) throws ECLErr {
if (delegate != null) {
try {
delegate.SetString(text, pos);
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
}
public void setString(String text, int pos) throws ECLErr {
SetString(text, pos);
}
public void SetString(String text, int row, int col) throws ECLErr {
SetString(text, ConvertRowColToPos(row, col));
}
public void setString(String text, int row, int col) throws ECLErr {
SetString(text, row, col);
}
public void SetString(String text) throws ECLErr {
SetString(text, GetCursorPos());
}
public void setString(String text) throws ECLErr {
SetString(text);
}
public void SetText(String text, int pos) throws ECLErr {
SetString(text, pos);
}
public void SetText(String text, int row, int col) throws ECLErr {
SetString(text, row, col);
}
public void setText(String text, int pos) {
if (delegate != null) delegate.setText(text, pos);
}
public void setText(String text, int row, int col) {
if (delegate != null) delegate.setText(text, row, col);
}
// ========== Search Operations (1-based) ==========
public int SearchText(String text, int dir, int startPos) {
return delegate != null ? delegate.SearchText(text, dir, startPos) : 0;
}
public int SearchText(String text, int dir) {
return delegate != null ? delegate.SearchText(text, dir) : 0;
}
public int SearchText(String text) {
return delegate != null ? delegate.SearchText(text) : 0;
}
public int SearchText(String text, int row, int col, int dir) throws ECLErr {
return SearchText(text, dir, ConvertRowColToPos(row, col));
}
public int SearchString(String text, int dir, int startPos) {
return SearchText(text, dir, startPos);
}
public int SearchString(String text, int dir) {
return SearchText(text, dir);
}
public int SearchString(String text) {
return SearchText(text);
}
public int SearchString(String text, int row, int col, int dir) throws ECLErr {
return SearchText(text, row, col, dir);
}
public int SearchPS(String text) {
return delegate != null ? delegate.SearchPS(text) : 0;
}
public int SearchPS(String text, int startPos) {
return delegate != null ? delegate.SearchPS(text, startPos) : 0;
}
public int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
return delegate != null ? delegate.SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap) : 0;
}
// ========== Keystrokes & Mnemonics ==========
public void SendKeys(String keys) {
if (delegate != null) delegate.SendKeys(keys);
}
public void sendKeys(String keys) { SendKeys(keys); }
public void SendKeys(String keys, int pos) {
if (delegate != null) delegate.SendKeys(keys, pos);
}
public void sendKeys(String keys, int pos) { SendKeys(keys, pos); }
public void SendKeys(String keys, int row, int col) {
if (delegate != null) delegate.SendKeys(keys, row, col);
}
public void sendKeys(String keys, int row, int col) { SendKeys(keys, row, col); }
public void SendCharacters(String keys) {
if (delegate != null) delegate.SendCharacters(keys);
}
public void sendCharacters(String keys) { SendCharacters(keys); }
public void SendCharacters(String keys, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, delayMs);
}
public void sendCharacters(String keys, int delayMs) { SendCharacters(keys, delayMs); }
public void SendCharacters(String keys, int pos, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, pos, delayMs);
}
public void sendCharacters(String keys, int pos, int delayMs) { SendCharacters(keys, pos, delayMs); }
public void SendCharacters(String keys, int row, int col, int delayMs) {
if (delegate != null) delegate.SendCharacters(keys, row, col, delayMs);
}
public void sendCharacters(String keys, int row, int col, int delayMs) { SendCharacters(keys, row, col, delayMs); }
public String[] GetSendKeyMnemonics() {
return delegate != null ? delegate.GetSendKeyMnemonics() : new String[0];
}
public String[] getSendKeyMnemonics() { return GetSendKeyMnemonics(); }
// ========== Field Operations ==========
public ECLFieldList GetFieldList() {
haus.nightmare.lib3270j.ecl.ECLFieldList fl = delegate != null ? delegate.GetFieldList() : null;
return new ECLFieldList(fl, this);
}
public ECLFieldList getFieldList() { return GetFieldList(); }
public ECLField GetField(int pos) throws ECLErr {
return GetFieldList().FindField(pos);
}
public ECLField getField(int pos) throws ECLErr { return GetField(pos); }
public ECLField GetField(int row, int col) throws ECLErr {
return GetFieldList().FindField(row, col);
}
public ECLField getField(int row, int col) throws ECLErr { return GetField(row, col); }
public ECLField GetFirstField() {
return GetFieldList().GetFirstField();
}
public ECLField getFirstField() { return GetFirstField(); }
public ECLField GetNextField(ECLField prev) {
return GetFieldList().GetNextField(prev);
}
public ECLField getNextField(ECLField prev) { return GetNextField(prev); }
// ========== Listener Overloads for HoD Interfaces ==========
public void RegisterPSEvent(ECLPSNotify notify) {
if (delegate != null) delegate.RegisterPSEvent(notify);
}
public void registerPSEvent(ECLPSNotify notify) { RegisterPSEvent(notify); }
public void RegisterPSEvent(ECLPSNotify notify, int eventType) {
if (delegate != null) delegate.RegisterPSEvent(notify, eventType);
}
public void registerPSEvent(ECLPSNotify notify, int eventType) { RegisterPSEvent(notify, eventType); }
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc) {
if (delegate != null) delegate.RegisterPSEvent(notify, desc);
}
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc) { RegisterPSEvent(notify, desc); }
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
if (delegate != null) delegate.RegisterPSEvent(notify, desc, eventType);
}
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) { RegisterPSEvent(notify, desc, eventType); }
public void UnregisterPSEvent(ECLPSNotify notify) {
if (delegate != null) delegate.UnregisterPSEvent(notify);
}
public void unregisterPSEvent(ECLPSNotify notify) { UnregisterPSEvent(notify); }
public void RegisterPSEvent(ECLPSListener listener) {
if (delegate != null) delegate.RegisterPSEvent(listener);
}
public void registerPSEvent(ECLPSListener listener) { RegisterPSEvent(listener); }
public void UnregisterPSEvent(ECLPSListener listener) {
if (delegate != null) delegate.UnregisterPSEvent(listener);
}
public void unregisterPSEvent(ECLPSListener listener) { UnregisterPSEvent(listener); }
// ========== Synchronization & Waits ==========
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
return delegate != null && delegate.WaitForScreen(desc, timeoutMs);
}
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); }
public boolean WaitForScreen(ECLScreenDesc desc) {
return delegate != null && delegate.WaitForScreen(desc);
}
public boolean waitForScreen(ECLScreenDesc desc) { return WaitForScreen(desc); }
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
return delegate != null && delegate.WaitWhileScreen(desc, timeoutMs);
}
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { return WaitWhileScreen(desc, timeoutMs); }
public boolean WaitWhileScreen(ECLScreenDesc desc) {
return delegate != null && delegate.WaitWhileScreen(desc);
}
public boolean waitWhileScreen(ECLScreenDesc desc) { return WaitWhileScreen(desc); }
public boolean WaitForString(String text, long timeoutMs) {
return delegate != null && delegate.WaitForString(text, timeoutMs);
}
public boolean waitForString(String text, long timeoutMs) { return WaitForString(text, timeoutMs); }
public boolean WaitForString(String text) {
return delegate != null && delegate.WaitForString(text);
}
public boolean waitForString(String text) { return WaitForString(text); }
public boolean WaitForString(String text, int row, int col, long timeoutMs) {
return delegate != null && delegate.WaitForString(text, row, col, timeoutMs);
}
public boolean waitForString(String text, int row, int col, long timeoutMs) { return WaitForString(text, row, col, timeoutMs); }
public boolean WaitForCursor(int row, int col, long timeoutMs) {
return delegate != null && delegate.WaitForCursor(row, col, timeoutMs);
}
public boolean waitForCursor(int row, int col, long timeoutMs) { return WaitForCursor(row, col, timeoutMs); }
// ========== Screen Update Inquiries ==========
public ECLPSUpdate GetScreen(int startRow, int startCol, int length) throws ECLErr {
return GetScreen(startRow, startCol, length, false);
}
public ECLPSUpdate GetScreen(int startRow, int startCol, int length, boolean fillNulls) throws ECLErr {
int pos = ConvertRowColToPos(startRow, startCol);
char[] buf = new char[length];
GetString(buf, length, pos, length);
if (fillNulls) {
for (int i = 0; i < buf.length; i++) {
if (buf[i] == '\0') buf[i] = ' ';
}
}
int endPos = pos + length - 1;
int endRow = ConvertPosToRow(endPos);
int endCol = ConvertPosToCol(endPos);
return new ECLPSUpdate(this, startRow, startCol, endRow, endCol, new String(buf));
}
public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
return delegate.GetScreen(buffer, len, row, col, length, plane);
}
}
@@ -0,0 +1,10 @@
package com.ibm.eNetwork.ECL;
/**
* Presentation space notification interface matching IBM Host On-Demand ECLPSNotify.
*/
public interface ECLPSNotify {
void NotifyEvent(ECLPS ps);
default void NotifyStop(ECLPS ps, int reason) {}
default void NotifyError(ECLPS ps, ECLErr err) {}
}
@@ -0,0 +1,90 @@
package com.ibm.eNetwork.ECL;
import java.io.Serializable;
/**
* Conforms to IBM Host On-Demand ECLPSUpdate.
*/
public class ECLPSUpdate implements Serializable {
private static final long serialVersionUID = 1L;
private final haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate;
private final int startRow;
private final int startCol;
private final int endRow;
private final int endCol;
private final int start;
private final int end;
private final boolean fullUpdate;
private final String text;
private final transient ECLPS ps;
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol,
int start, int end, boolean fullUpdate, String text) {
this.ps = ps;
this.startRow = startRow;
this.startCol = startCol;
this.endRow = endRow;
this.endCol = endCol;
this.start = start;
this.end = end;
this.fullUpdate = fullUpdate;
this.text = text != null ? text : "";
this.delegate = null;
}
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol, String text) {
this(ps, startRow, startCol, endRow, endCol, 0, 0, false, text);
}
public ECLPSUpdate(int startRow, int startCol, int endRow, int endCol, String text) {
this(null, startRow, startCol, endRow, endCol, 0, 0, false, text);
}
public ECLPSUpdate(haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate) {
this.delegate = delegate;
this.ps = (delegate != null && delegate.getPS() != null) ? new ECLPS(delegate.getPS()) : null;
this.startRow = delegate != null ? delegate.getStartRow() : 0;
this.startCol = delegate != null ? delegate.getStartCol() : 0;
this.endRow = delegate != null ? delegate.getEndRow() : 0;
this.endCol = delegate != null ? delegate.getEndCol() : 0;
this.start = delegate != null ? delegate.getStart() : 0;
this.end = delegate != null ? delegate.getEnd() : 0;
this.fullUpdate = delegate != null && delegate.isFullUpdate();
this.text = delegate != null ? delegate.getText() : "";
}
public ECLPS getPS() { return ps; }
public ECLPS GetPS() { return ps; }
public int getStartRow() { return startRow; }
public int GetStartRow() { return startRow + 1; }
public int getStartCol() { return startCol; }
public int GetStartCol() { return startCol + 1; }
public int getEndRow() { return endRow; }
public int GetEndRow() { return endRow + 1; }
public int getEndCol() { return endCol; }
public int GetEndCol() { return endCol + 1; }
public int getStart() { return start; }
public int GetStart() { return start + 1; }
public int getEnd() { return end; }
public int GetEnd() { return end + 1; }
public boolean isFullUpdate() { return fullUpdate; }
public boolean IsFullUpdate() { return fullUpdate; }
public String getText() { return text; }
public String GetText() { return text; }
@Override
public String toString() {
return String.format("ECLPSUpdate[start=(%d,%d), end=(%d,%d), full=%b]",
startRow, startCol, endRow, endCol, fullUpdate);
}
}
@@ -0,0 +1,21 @@
package com.ibm.eNetwork.ECL;
import java.io.Serializable;
/**
* Screen descriptor matching engine facade matching IBM Host On-Demand ECLScreenDesc.
*/
public class ECLScreenDesc extends haus.nightmare.lib3270j.ecl.ECLScreenDesc implements Serializable {
private static final long serialVersionUID = 1L;
public ECLScreenDesc() {
super();
}
public boolean Matches(ECLPS ps, ECLOIA oia) {
haus.nightmare.lib3270j.ecl.ECLPS p = (ps != null) ? ps.getDelegate() : null;
haus.nightmare.lib3270j.ecl.ECLOIA o = (oia != null) ? oia.getDelegate() : null;
return super.Matches(p, o);
}
}
@@ -0,0 +1,8 @@
package com.ibm.eNetwork.ECL;
/**
* Listener interface for ECLScreenReco matching notifications matching IBM HoD.
*/
public interface ECLScreenNotify {
void screenMatching(ECLScreenRecoEvent event);
}
@@ -0,0 +1,78 @@
package com.ibm.eNetwork.ECL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Screen recognition facade matching IBM Host On-Demand ECLScreenReco.
*/
public class ECLScreenReco {
private final haus.nightmare.lib3270j.ecl.ECLScreenReco delegate;
private final ECLPS ps;
private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>();
public ECLScreenReco() {
this((ECLPS) null);
}
public ECLScreenReco(ECLSession session) {
this(session != null ? session.GetPS() : null);
}
public ECLScreenReco(ECLPS ps) {
this(ps, (ECLOIA) null);
}
public ECLScreenReco(ECLPS ps, ECLOIA oia) {
this.ps = ps;
this.delegate = new haus.nightmare.lib3270j.ecl.ECLScreenReco(ps != null ? ps.getDelegate() : null,
oia != null ? oia.getDelegate() : null);
}
public ECLScreenReco(haus.nightmare.lib3270j.ecl.ECLScreenReco delegate) {
this.delegate = delegate;
this.ps = null;
}
public void RegisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
if (desc != null && notify != null) {
notifyMap.put(desc, notify);
if (delegate != null) {
delegate.RegisterScreen(desc, event -> notify.screenMatching(new ECLScreenRecoEvent(this, desc, ps)));
}
}
}
public void registerScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
RegisterScreen(desc, notify);
}
public void UnregisterScreen(ECLScreenDesc desc) {
if (desc != null) {
notifyMap.remove(desc);
if (delegate != null) {
delegate.UnregisterScreen(desc);
}
}
}
public void unregisterScreen(ECLScreenDesc desc) {
UnregisterScreen(desc);
}
public boolean IsMatch(ECLScreenDesc desc) {
if (delegate != null) {
return delegate.IsMatch(desc);
}
return false;
}
public static boolean IsMatch(ECLPS ps, ECLScreenDesc desc) {
if (ps == null) return false;
if (desc == null) return true;
return desc.Matches(ps, (ECLOIA) null);
}
public haus.nightmare.lib3270j.ecl.ECLScreenReco getDelegate() { return delegate; }
}
@@ -0,0 +1,39 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLEvent;
/**
* Event delivered when an ECLScreenDesc matches the Presentation Space.
*/
public class ECLScreenRecoEvent extends ECLEvent {
private final ECLScreenReco source;
private final ECLScreenDesc screenDesc;
private final ECLPS ps;
public ECLScreenRecoEvent(ECLScreenReco source, ECLScreenDesc screenDesc, ECLPS ps) {
super(source);
this.source = source;
this.screenDesc = screenDesc;
this.ps = ps;
}
public ECLScreenRecoEvent(Object source, ECLPS ps) {
super(source);
this.source = (source instanceof ECLScreenReco) ? (ECLScreenReco) source : null;
this.screenDesc = null;
this.ps = ps;
}
@Override
public Object GetSource() { return source != null ? source : super.GetSource(); }
public ECLScreenReco GetScreenReco() { return source; }
public ECLScreenReco getScreenReco() { return source; }
public ECLScreenDesc GetScreenDesc() { return screenDesc; }
public ECLScreenDesc getScreenDesc() { return screenDesc; }
public ECLPS GetPS() { return ps; }
public ECLPS getPS() { return ps; }
}
@@ -0,0 +1,190 @@
package com.ibm.eNetwork.ECL;
import java.io.IOException;
import java.util.Properties;
/**
* Top-level session facade matching IBM Host On-Demand ECLSession.
* Extends ECLConnection and provides access to Presentation Space (ECLPS),
* Operator Information Area (ECLOIA), File Transfer (ECLXfer), and Screen Recognition (ECLScreenReco).
*/
public class ECLSession extends ECLConnection implements ECLConstants {
public static final String SESSION_NAME = "SESSION_NAME";
public static final String SESSION_LABEL = "SESSION_LABEL";
public static final String SESSION_TYPE = "SESSION_TYPE";
public static final String SESSION_HOST = "SESSION_HOST";
public static final String SESSION_HOST_PORT = "SESSION_HOST_PORT";
public static final String SESSION_PORT = "SESSION_PORT";
public static final String SESSION_PS_SIZE = "SESSION_PS_SIZE";
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
public static final String SESSION_SSL = "SESSION_SSL";
public static final String SESSION_SECURITY_PROTOCOL = "SESSION_SECURITY_PROTOCOL";
public static final String SESSION_TLS_PROTOCOL_VERSION = "tlsProtocolVersion";
public static final String SESSION_LU_NAME = "SESSION_LU_NAME";
public static final String SESSION_TN3270E = "SESSION_TN3270E";
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
public static final String SESSION_AUTORECONNECT = "SESSION_AUTORECONNECT";
public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES";
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_AUTO_SYS_UNLOCK = "autoSysUnlock";
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 BLOCK_PASTE = "blockPaste";
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
public static final String PASTE_WORD_BREAK = "pasteWordBreak";
private final haus.nightmare.lib3270j.ecl.ECLSession delegate;
private final ECLPS ps;
private final ECLOIA oia;
private final ECLXfer xfer;
private final ECLScreenReco screenReco;
public ECLSession(Properties properties) throws ECLErr {
super(properties);
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(properties);
this.ps = new ECLPS(delegate.getPS());
this.oia = new ECLOIA(delegate.getOIA());
this.xfer = new ECLXfer(delegate.getXfer());
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLSession() throws ECLErr {
this(new Properties());
}
public ECLSession(String host, int port) throws ECLErr {
super(host, port);
Properties props = new Properties();
if (host != null) props.setProperty(SESSION_HOST, host);
props.setProperty(SESSION_PORT, String.valueOf(port));
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(props);
this.ps = new ECLPS(delegate.getPS());
this.oia = new ECLOIA(delegate.getOIA());
this.xfer = new ECLXfer(delegate.getXfer());
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLSession(haus.nightmare.lib3270j.ecl.ECLSession delegate) {
super(delegate);
this.delegate = delegate;
this.ps = new ECLPS(delegate != null ? delegate.getPS() : null);
this.oia = new ECLOIA(delegate != null ? delegate.getOIA() : null);
this.xfer = new ECLXfer(delegate != null ? delegate.getXfer() : null);
this.screenReco = new ECLScreenReco(this.ps, this.oia);
}
public ECLPS GetPS() {
return ps;
}
public ECLPS getPS() {
return ps;
}
public ECLOIA GetOIA() {
return oia;
}
public ECLOIA getOIA() {
return oia;
}
public ECLXfer GetXfer() {
return xfer;
}
public ECLXfer getXfer() {
return xfer;
}
public ECLScreenReco GetScreenReco() {
return screenReco;
}
public ECLScreenReco getScreenReco() {
return screenReco;
}
public ECLConnection GetConnection() {
return this;
}
public ECLConnection getConnection() {
return this;
}
public haus.nightmare.lib3270j.ecl.ECLSession getDelegate() {
return delegate;
}
@Override
public void StartCommunication() throws IOException {
if (delegate != null) {
delegate.StartCommunication();
} else {
super.StartCommunication();
}
}
@Override
public void StopCommunication() {
if (delegate != null) {
delegate.StopCommunication();
} else {
super.StopCommunication();
}
}
public boolean StartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout, (ECLScreenDesc) null);
}
public boolean startCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout);
}
public boolean StartCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
if (delegate != null) {
try {
return delegate.StartCommunicationWithBlocking(timeout, desc);
} catch (IOException e) {
throw new ECLErr("ECLSession.StartCommunicationWithBlocking", "ECL0001", e.getMessage());
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return false;
}
public boolean startCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
return StartCommunicationWithBlocking(timeout, desc);
}
public boolean RestartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
if (delegate != null) {
try {
return delegate.RestartCommunicationWithBlocking(timeout);
} catch (IOException e) {
throw new ECLErr("ECLSession.RestartCommunicationWithBlocking", "ECL0001", e.getMessage());
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
throw new ECLErr(e.getMessage());
}
}
return false;
}
public boolean restartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
return RestartCommunicationWithBlocking(timeout);
}
public void dispose() {
if (delegate != null) {
delegate.dispose();
}
}
}
@@ -0,0 +1,54 @@
package com.ibm.eNetwork.ECL;
import com.ibm.eNetwork.ECL.event.ECLXferListener;
/**
* File transfer facade matching IBM Host On-Demand ECLXfer.
*/
public class ECLXfer {
private final haus.nightmare.lib3270j.ecl.ECLXfer delegate;
public ECLXfer(ECLSession session, Boolean bidi) {
this.delegate = (session != null && session.getDelegate() != null)
? session.getDelegate().GetXfer() : null;
}
public ECLXfer(haus.nightmare.lib3270j.ecl.ECLXfer delegate) {
this.delegate = delegate;
}
public void SendFile(String localFile, String hostFile, String options) throws ECLErr {
if (delegate != null) {
int rc = delegate.SendFile(localFile, hostFile, options);
if (rc != 0) {
throw new ECLErr("ECLXfer:SendFile", "ECL0150", String.valueOf(rc));
}
}
}
public void ReceiveFile(String localFile, String hostFile, String options) throws ECLErr {
if (delegate != null) {
int rc = delegate.ReceiveFile(localFile, hostFile, options);
if (rc != 0) {
throw new ECLErr("ECLXfer:ReceiveFile", "ECL0150", String.valueOf(rc));
}
}
}
public void Cancel() {
if (delegate != null) {
delegate.Cancel();
}
}
public void RegisterXferEvent(ECLXferListener listener) {
// Can bridge listener if needed
}
public void UnregisterXferEvent(ECLXferListener listener) {
// Can bridge listener if needed
}
public haus.nightmare.lib3270j.ecl.ECLXfer getDelegate() { return delegate; }
}
@@ -0,0 +1,25 @@
package com.ibm.eNetwork.ECL.event;
/**
* Base event class matching IBM Host On-Demand ECLEvent.
*/
public class ECLEvent {
private final Object originator;
public ECLEvent(Object object) {
this.originator = object;
}
public Object GetSource() {
return this.originator;
}
public Object getSource() {
return this.originator;
}
@Override
public String toString() {
return super.toString() + ": " + (this.originator != null ? this.originator.toString() : "null");
}
}
@@ -0,0 +1,126 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLPS;
import com.ibm.eNetwork.ECL.ECLPSUpdate;
/**
* Presentation Space event class matching IBM Host On-Demand ECLPSEvent.
*/
public class ECLPSEvent extends ECLEvent {
public static final int USER_EVENTS = 1;
public static final int HOST_EVENTS = 2;
public static final int ALL_EVENTS = 3;
private int type = 0;
private int start = 1;
private int end = 1;
private int startRow = 1;
private int startCol = 1;
private int endRow = 1;
private int endCol = 1;
private int oldCursorPos = 1;
private int newCursorPos = 1;
private boolean fullUpdate = true;
private boolean cursorVisible = true;
private int ringCounter = 0;
private ECLPSUpdate psUpdate = null;
private haus.nightmare.lib3270j.ecl.ECLPSEvent delegate = null;
public ECLPSEvent(Object source) {
super(source);
if (source instanceof ECLPS) {
ECLPS ps = (ECLPS) source;
this.end = ps.GetSize();
this.endRow = ps.GetRows();
this.endCol = ps.GetCols();
this.newCursorPos = ps.GetCursorPos();
} else if (source instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
haus.nightmare.lib3270j.ecl.ECLPS ps = (haus.nightmare.lib3270j.ecl.ECLPS) source;
this.end = ps.GetSize();
this.endRow = ps.GetRows();
this.endCol = ps.GetCols();
this.newCursorPos = ps.GetCursorPos();
}
}
public ECLPSEvent(Object source, int type) {
this(source);
this.type = type;
}
public ECLPSEvent(haus.nightmare.lib3270j.ecl.ECLPSEvent delegate) {
super(delegate != null ? delegate.getSource() : null);
this.delegate = delegate;
if (delegate != null) {
this.type = delegate.getType();
this.start = delegate.getStart();
this.end = delegate.getEnd();
this.startRow = delegate.getStartRow();
this.startCol = delegate.getStartCol();
this.endRow = delegate.getEndRow();
this.endCol = delegate.getEndCol();
this.oldCursorPos = delegate.getOldCursorAddress() + 1;
this.newCursorPos = delegate.getNewCursorAddress() + 1;
this.fullUpdate = delegate.isFullUpdate();
this.cursorVisible = delegate.isCursorVisible();
this.ringCounter = delegate.getRingCounter();
if (delegate.getPSUpdate() != null) {
this.psUpdate = new ECLPSUpdate(delegate.getPSUpdate());
}
}
}
public int GetType() { return type; }
public int getType() { return type; }
public ECLPS GetPS() {
Object src = GetSource();
if (src instanceof ECLPS) {
return (ECLPS) src;
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
return new ECLPS((haus.nightmare.lib3270j.ecl.ECLPS) src);
}
return null;
}
public void SetStart(int start) { this.start = start; }
public int GetStart() { return start; }
public int getStart() { return start; }
public int GetStartRow() { return startRow; }
public int getStartRow() { return startRow; }
public int GetStartCol() { return startCol; }
public int getStartCol() { return startCol; }
public void SetEnd(int end) { this.end = end; }
public int GetEnd() { return end; }
public int getEnd() { return end; }
public int GetEndRow() { return endRow; }
public int getEndRow() { return endRow; }
public int GetEndCol() { return endCol; }
public int getEndCol() { return endCol; }
public int GetOldCursorPos() { return oldCursorPos; }
public int getOldCursorPos() { return oldCursorPos; }
public int GetNewCursorPos() { return newCursorPos; }
public int getNewCursorPos() { return newCursorPos; }
public boolean IsFullUpdate() { return fullUpdate; }
public boolean isFullUpdate() { return fullUpdate; }
public boolean IsCursorVisible() { return cursorVisible; }
public boolean isCursorVisible() { return cursorVisible; }
public int GetRingCounter() { return ringCounter; }
public int getRingCounter() { return ringCounter; }
public ECLPSUpdate GetPSUpdate() { return psUpdate; }
public ECLPSUpdate getPSUpdate() { return psUpdate; }
public haus.nightmare.lib3270j.ecl.ECLPSEvent getDelegate() { return delegate; }
}
@@ -0,0 +1,13 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLErr;
import com.ibm.eNetwork.ECL.ECLPS;
/**
* Presentation Space listener interface matching IBM Host On-Demand ECLPSListener.
*/
public interface ECLPSListener {
void PSNotifyEvent(ECLPSEvent event);
default void PSNotifyStop(ECLPS ps, int reason) {}
default void PSNotifyError(ECLPS ps, ECLErr err) {}
}
@@ -0,0 +1,48 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLXfer;
/**
* File transfer event matching IBM Host On-Demand ECLXferEvent.
*/
public class ECLXferEvent extends ECLEvent {
private final String sourceFile;
private final String targetFile;
private final boolean receive;
private final long unitsDone;
private final long unitsToDo;
public ECLXferEvent(Object source, long unitsDone, String sourceFile, String targetFile, boolean receive) {
this(source, unitsDone, -1L, sourceFile, targetFile, receive);
}
public ECLXferEvent(Object source, long unitsDone, long unitsToDo, String sourceFile, String targetFile, boolean receive) {
super(source);
this.unitsDone = unitsDone;
this.unitsToDo = unitsToDo;
this.sourceFile = sourceFile;
this.targetFile = targetFile;
this.receive = receive;
}
public ECLXfer GetECLXfer() {
Object src = GetSource();
return (src instanceof ECLXfer) ? (ECLXfer) src : null;
}
public String GetSourceFile() { return sourceFile; }
public String getSourceFile() { return sourceFile; }
public String GetTargetFile() { return targetFile; }
public String getTargetFile() { return targetFile; }
public long GetBytesSent() { return unitsDone; }
public long getBytesSent() { return unitsDone; }
public long GetBytesToSend() { return unitsToDo; }
public long getBytesToSend() { return unitsToDo; }
public boolean IsReceive() { return receive; }
public boolean isReceive() { return receive; }
}
@@ -0,0 +1,13 @@
package com.ibm.eNetwork.ECL.event;
import com.ibm.eNetwork.ECL.ECLErr;
import com.ibm.eNetwork.ECL.ECLXfer;
/**
* File transfer listener interface matching IBM Host On-Demand ECLXferListener.
*/
public interface ECLXferListener {
void XferNotifyEvent(ECLXferEvent event);
default void XferNotifyStop(ECLXfer xfer, int reason) {}
default void XferNotifyError(ECLXfer xfer, ECLErr err) {}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.ECL.macrovariable;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroExpressionParser.
*/
public class MacroExpressionParser extends haus.nightmare.lib3270j.macro.variable.MacroExpressionParser implements Serializable {
private static final long serialVersionUID = 1L;
public MacroExpressionParser() {
super();
}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.ECL.macrovariable;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroVariables.
*/
public class MacroVariables extends haus.nightmare.lib3270j.macro.variable.MacroVariables implements Serializable {
private static final long serialVersionUID = 1L;
public MacroVariables() {
super();
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDAttrib drop-in facade.
*/
public class ECLSDAttrib extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDAttrib {
private static final long serialVersionUID = 1L;
public ECLSDAttrib() {
super();
}
public ECLSDAttrib(char attrib, int row, int col, int plane) {
super(attrib, row, col, plane);
}
public ECLSDAttrib(char attrib, int row, int col, int plane, boolean optional) {
super(attrib, row, col, plane, optional);
}
}
@@ -0,0 +1,24 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDBlock drop-in facade.
*/
public class ECLSDBlock extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlock {
private static final long serialVersionUID = 1L;
public ECLSDBlock() {
super();
}
public ECLSDBlock(int srow, int scol, boolean caseSense, boolean optional) {
super(srow, scol, caseSense, optional);
}
public ECLSDBlock(int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
super(srow, scol, erow, ecol, caseSense, optional);
}
public ECLSDBlock(String[] textLines, int srow, int scol, boolean caseSense) {
super(textLines, srow, scol, caseSense);
}
}
@@ -0,0 +1,16 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDBlockString drop-in facade.
*/
public class ECLSDBlockString extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlockString {
private static final long serialVersionUID = 1L;
public ECLSDBlockString() {
super();
}
public ECLSDBlockString(String text) {
super(text);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDChecksum drop-in facade.
*/
public class ECLSDChecksum extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDChecksum {
private static final long serialVersionUID = 1L;
public ECLSDChecksum() {
super();
}
public ECLSDChecksum(int value) {
super(value);
}
public ECLSDChecksum(int value, boolean optional) {
super(value, optional);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDCursor drop-in facade.
*/
public class ECLSDCursor extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDCursor {
private static final long serialVersionUID = 1L;
public ECLSDCursor() {
super();
}
public ECLSDCursor(int row, int col) {
super(row, col);
}
public ECLSDCursor(int row, int col, boolean optional) {
super(row, col, optional);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDFields drop-in facade.
*/
public class ECLSDFields extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDFields {
private static final long serialVersionUID = 1L;
public ECLSDFields() {
super();
}
public ECLSDFields(int num) {
super(num);
}
public ECLSDFields(int num, boolean optional) {
super(num, optional);
}
}
@@ -0,0 +1,20 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDInputFields drop-in facade.
*/
public class ECLSDInputFields extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDInputFields {
private static final long serialVersionUID = 1L;
public ECLSDInputFields() {
super();
}
public ECLSDInputFields(int num) {
super(num);
}
public ECLSDInputFields(int num, boolean optional) {
super(num, optional);
}
}
@@ -0,0 +1,24 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDOIA drop-in facade.
*/
public class ECLSDOIA extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDOIA {
private static final long serialVersionUID = 1L;
public ECLSDOIA() {
super();
}
public ECLSDOIA(int type) {
super(type);
}
public ECLSDOIA(int type, boolean optional) {
super(type, optional);
}
public ECLSDOIA(String status, boolean optional) {
super(status, optional);
}
}
@@ -0,0 +1,36 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLSDString drop-in facade.
*/
public class ECLSDString extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDString {
private static final long serialVersionUID = 1L;
public ECLSDString() {
super();
}
public ECLSDString(String text) {
super(text);
}
public ECLSDString(String text, int row, int col) {
super(text, row, col);
}
public ECLSDString(String text, int row, int col, boolean caseSense) {
super(text, row, col, caseSense);
}
public ECLSDString(String text, int row, int col, boolean caseSense, boolean optional) {
super(text, row, col, caseSense, optional);
}
public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
super(text, srow, scol, erow, ecol, caseSense, optional);
}
public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional, boolean wrap) {
super(text, srow, scol, erow, ecol, caseSense, optional, wrap);
}
}
@@ -0,0 +1,16 @@
package com.ibm.eNetwork.ECL.screenreco;
/**
* IBM Host On-Demand ECLScreenDescriptor drop-in facade.
*/
public abstract class ECLScreenDescriptor extends haus.nightmare.lib3270j.ecl.screenreco.ECLScreenDescriptor {
private static final long serialVersionUID = 1L;
protected ECLScreenDescriptor() {
super();
}
protected ECLScreenDescriptor(boolean optional) {
super(optional);
}
}
@@ -0,0 +1,23 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
import com.ibm.eNetwork.ECL.ECLSession;
/**
* Compatibility facade for IBM Host On-Demand Macro bean.
*/
public class Macro extends haus.nightmare.lib3270j.macro.Macro implements Serializable {
private static final long serialVersionUID = 1L;
public Macro() {
super();
}
public Macro(ECLSession session) {
super(session != null ? session.getDelegate() : null);
}
public Macro(haus.nightmare.lib3270j.ecl.ECLSession session) {
super(session);
}
}
@@ -0,0 +1,10 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroAction.
*/
public abstract class MacroAction extends haus.nightmare.lib3270j.macro.action.MacroAction implements Serializable {
private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroActions.
*/
public class MacroActions extends haus.nightmare.lib3270j.macro.MacroActions implements Serializable {
private static final long serialVersionUID = 1L;
public MacroActions() {
super();
}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroNextScreens.
*/
public class MacroNextScreens extends haus.nightmare.lib3270j.macro.MacroNextScreens implements Serializable {
private static final long serialVersionUID = 1L;
public MacroNextScreens() {
super();
}
}
@@ -0,0 +1,18 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroScreen.
*/
public class MacroScreen extends haus.nightmare.lib3270j.macro.MacroScreen implements Serializable {
private static final long serialVersionUID = 1L;
public MacroScreen() {
super();
}
public MacroScreen(String name) {
super(name);
}
}
@@ -0,0 +1,14 @@
package com.ibm.eNetwork.beans.HOD;
import java.io.Serializable;
/**
* Compatibility facade for IBM Host On-Demand MacroScreens.
*/
public class MacroScreens extends haus.nightmare.lib3270j.macro.MacroScreens implements Serializable {
private static final long serialVersionUID = 1L;
public MacroScreens() {
super();
}
}
@@ -92,6 +92,9 @@ public class Telnet3270Client {
ps.notifyAlarm();
}
@Override public void onKeyboardUnlocked() {
if (screenBuffer != null) {
screenBuffer.getHistoryBuffer().snapshot(screenBuffer);
}
ps.notifyKeyUnlocked();
}
});
@@ -658,6 +661,9 @@ public class Telnet3270Client {
public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); }
public void toggleWordWrap() { setWordWrap(!isWordWrap()); }
public boolean isPasteWordBreak() { return inputProcessor != null && inputProcessor.isPasteWordBreak(); }
public void setPasteWordBreak(boolean b) { if (inputProcessor != null) inputProcessor.setPasteWordBreak(b); }
/** APL Keyboard Mode operations. */
public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); }
public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); }
@@ -705,4 +711,8 @@ public class Telnet3270Client {
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
return dsProcessor.getGocaDecoder();
}
public haus.nightmare.lib3270j.screen.ScreenHistoryBuffer getScreenHistoryBuffer() {
return screenBuffer != null ? screenBuffer.getHistoryBuffer() : null;
}
}
@@ -59,6 +59,7 @@ public class DataStreamProcessor {
private byte currentBg = 0;
private byte currentGr = 0;
private byte currentCs = 0;
private byte currentOl = 0;
/** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface
@@ -250,6 +251,7 @@ public class DataStreamProcessor {
currentBg = 0;
currentGr = 0;
currentCs = 0;
currentOl = 0;
break;
case CMD_WSF:
case SNA_CMD_WSF:
@@ -392,6 +394,7 @@ public class DataStreamProcessor {
currentBg = 0;
currentGr = 0;
currentCs = 0;
currentOl = 0;
}
if (eraseFirst) {
@@ -401,6 +404,7 @@ public class DataStreamProcessor {
currentBg = 0;
currentGr = 0;
currentCs = 0;
currentOl = 0;
}
// Process orders and data starting at byte 2
@@ -497,6 +501,7 @@ public class DataStreamProcessor {
currentBg = 0;
currentGr = 0;
currentCs = 0;
currentOl = 0;
break;
case XA_FOREGROUND:
currentFg = (byte) attrValue;
@@ -521,6 +526,10 @@ public class DataStreamProcessor {
case XA_CHARSET:
currentCs = (byte) attrValue;
break;
case XA_OUTLINING:
case 0x84:
currentOl = (byte) attrValue;
break;
}
pos += 3;
lastWasOrder = true;
@@ -548,6 +557,8 @@ public class DataStreamProcessor {
applyExtendedAttribute(ea, attrType, attrValue);
}
}
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 2 + nPairs * 2;
lastWasOrder = true;
break;
@@ -603,6 +614,7 @@ public class DataStreamProcessor {
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.cs = fillCs;
ea.ol = currentOl;
baddr = (baddr + 1) % size;
} while (baddr != toAddr);
screen.setBufferAddress(baddr);
@@ -631,6 +643,7 @@ public class DataStreamProcessor {
ea.bg = 0;
ea.gr = 0;
ea.cs = 0;
ea.ol = 0;
}
}
baddr = (baddr + 1) % size;
@@ -655,6 +668,7 @@ public class DataStreamProcessor {
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.ol = currentOl;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 2;
@@ -681,6 +695,7 @@ public class DataStreamProcessor {
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.cs = currentCs;
ea.ol = currentOl;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
@@ -993,6 +1008,7 @@ public class DataStreamProcessor {
byte[] psData = new byte[fieldLen - 3];
System.arraycopy(data, pos + 3, psData, 0, psData.length);
programSymbolManager.loadps(psData);
notifyScreenUpdated();
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
@@ -1303,6 +1319,7 @@ public class DataStreamProcessor {
byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, offset + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData);
notifyScreenUpdated();
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
@@ -482,8 +482,8 @@ public class QueryReplyBuilder {
cpgid = screen.getTranslator().getCpgid();
}
if (graphicsMode == GraphicsMode.PROGRAMMED_SYMBOLS) {
// Programmed Symbols only mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
if (graphicsMode.isProgrammedSymbolsEnabled()) {
// Programmed Symbols mode (3279 PS / 3179G with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
out.write(0x00); // more flags
@@ -681,16 +681,18 @@ public class QueryReplyBuilder {
public byte[] buildGrSymbolSet() {
int charW = getCharWidth();
int charH = getCharHeight();
int cgcsgid = 0x02B9;
int cpgid = 0x0025;
if (screen != null && screen.getTranslator() != null) {
cgcsgid = screen.getTranslator().getCgcsgid();
cpgid = screen.getTranslator().getCpgid();
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return new byte[]{
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00, (byte) charW, (byte) charH,
0x00, 0x01, 0x4C, 0x01, 0x22, 0x00, 0x00, 0x12, 0x01, 0x00, 0x20, (byte) 0xF8, (byte) 0xC1, (byte) 0xC1,
0x00, 0x00, 0x18, 0x18, 0x00, 0x01, 0x72, 0x01, 0x2C, 0x00, (byte) 0xFF
};
}
return new byte[]{
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
0x01, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00,
(byte) charW, (byte) charH, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
(byte) charW, (byte) charH, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
}
@@ -77,6 +77,34 @@ public class ECLConnection {
if (props != null) {
this.properties.putAll(props);
convertData(this.properties);
String h = this.properties.getProperty(ECLSession.SESSION_HOST);
if (h == null) h = this.properties.getProperty("host");
if (h == null) h = this.properties.getProperty("Host");
if (h == null) h = this.properties.getProperty("SESSION_PRINT_SESSION_HOST");
if (h != null && !h.trim().isEmpty()) this.host = h.trim();
String p = this.properties.getProperty(ECLSession.SESSION_PORT);
if (p == null) p = this.properties.getProperty("port");
if (p == null) p = this.properties.getProperty("Port");
if (p == null) p = this.properties.getProperty("SESSION_HOST_PORT");
if (p == null) p = this.properties.getProperty("SESSION_PRINT_SESSION_HOST_PORT");
if (p != null && !p.trim().isEmpty()) {
try { this.port = Integer.parseInt(p.trim()); } catch (NumberFormatException ignored) {}
}
String cp = this.properties.getProperty(ECLSession.SESSION_CODE_PAGE);
if (cp == null) cp = this.properties.getProperty("code_page");
if (cp == null) cp = this.properties.getProperty("codepage");
if (cp == null) cp = this.properties.getProperty("CodePage");
if (cp != null && !cp.trim().isEmpty()) this.codePage = cp.trim();
String lu = this.properties.getProperty(ECLSession.SESSION_LU_NAME);
if (lu == null) lu = this.properties.getProperty("lu_name");
if (lu == null) lu = this.properties.getProperty("luname");
if (lu == null) lu = this.properties.getProperty("SESSION_PRINT_SESSION_LU_NAME");
if (lu != null && !lu.trim().isEmpty()) this.luName = lu.trim();
String asu = this.properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK);
if (asu != null) {
this.autoSysUnlock = "true".equalsIgnoreCase(asu) || "1".equals(asu);
@@ -11,6 +11,8 @@ public class ECLErr extends Exception {
private String id = "";
private String text = "";
private int reasonCode = 0;
public ECLErr() {
super();
}
@@ -20,6 +22,21 @@ public class ECLErr extends Exception {
this.text = text;
}
public ECLErr(String location, String msgNum, int reasonCode) {
super(formatMsg(location, msgNum, "Reason " + reasonCode));
this.tag = location;
this.id = msgNum;
this.text = "Reason " + reasonCode;
this.reasonCode = reasonCode;
}
public ECLErr(String location, String msgNum) {
super(formatMsg(location, msgNum, ""));
this.tag = location;
this.id = msgNum;
this.text = "";
}
public ECLErr(String tag, String id, String text) {
super(formatMsg(tag, id, text));
this.tag = tag;
@@ -27,11 +44,30 @@ public class ECLErr extends Exception {
this.text = text;
}
public ECLErr(String tag, String id, String text, String extra) {
super(formatMsg(tag, id, text + " " + extra));
this.tag = tag;
this.id = id;
this.text = text + " " + extra;
public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
super(formatMsg(location, msgNum, insert1 + " (Reason " + reasonCode + ")"));
this.tag = location;
this.id = msgNum;
this.text = insert1 + " (Reason " + reasonCode + ")";
this.reasonCode = reasonCode;
}
public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) {
super(formatMsg(location, msgNum, insert1 + " " + insert2));
this.tag = location;
this.id = msgNum;
this.text = insert1 + " " + insert2;
}
public ECLErr(String location, String msgNum, String insert1, String insert2) {
this(location, msgNum, insert1, insert2, false);
}
public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) {
super(formatMsg(location, msgNum, insert1 + " " + insert2 + " " + insert3));
this.tag = location;
this.id = msgNum;
this.text = insert1 + " " + insert2 + " " + insert3;
}
public ECLErr(Throwable cause) {
@@ -57,14 +93,33 @@ public class ECLErr extends Exception {
public String getID() { return id; }
public String GetID() { return id; }
public String getLocation() { return tag; }
public String GetLocation() { return tag; }
public int getReasonCode() { return reasonCode; }
public int GetReasonCode() { return reasonCode; }
public int getMsgNumber() {
if (id != null && id.length() >= 7 && id.startsWith("ECL")) {
try {
return Integer.parseInt(id.substring(3, 7));
} catch (NumberFormatException ignored) {}
}
return 0;
}
public int GetMsgNumber() { return getMsgNumber(); }
public String getText() { return text; }
public String GetText() { return text; }
public String getMsgText() { return getMessage(); }
public String GetMsgText() { return getMessage(); }
public String getErrorText() { return getMessage(); }
public String GetErrorText() { return getMessage(); }
@Override
public String toString() {
return "ECLErr[tag=" + tag + ", id=" + id + ", text=" + text + "]";
return "ECLErr[tag=" + tag + ", id=" + id + ", text=" + text + ", reasonCode=" + reasonCode + "]";
}
}
@@ -26,6 +26,10 @@ public class ECLField {
this.attribute = attribute;
}
public ECLPS getPS() { return ps; }
public ECLPS GetPS() { return ps; }
public byte getAttributeByte() { return attribute; }
/** Buffer address of the field attribute character. */
public int getStart() { return startPos; }
public int GetStart() { return getStart(); }
@@ -154,6 +158,8 @@ public class ECLField {
return ps.getString(dataStart, length);
}
public String GetText() { return getText(); }
public String getString() { return getText(); }
public String GetString() { return getText(); }
/**
* Set the text contents of this field.
@@ -162,7 +168,13 @@ public class ECLField {
if (isProtected() || length <= 0) return;
ps.setText(text, dataStart);
}
public void SetText(String text) { setText(text); }
public void SetText(String text) throws ECLErr {
if (isProtected()) {
throw new ECLErr("ECLField:SetText", "ECL0015", "Field is protected");
}
setText(text);
}
public void SetString(String text) throws ECLErr { SetText(text); }
/**
* Get selector light pen type.
@@ -7,15 +7,17 @@ import java.util.List;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import java.util.Vector;
/**
* Manages the collection of all 3270 fields currently present in the Presentation Space.
* Conforms to IBM ECL ECLFieldList specification.
*/
public class ECLFieldList {
public class ECLFieldList extends Vector<ECLField> {
private static final long serialVersionUID = 1L;
private final ECLPS ps;
private final ScreenBuffer screen;
private final List<ECLField> fields = new ArrayList<>();
public ECLFieldList(ECLPS ps, ScreenBuffer screen) {
this.ps = ps;
@@ -23,11 +25,14 @@ public class ECLFieldList {
refresh();
}
public ECLPS getPS() { return ps; }
public ScreenBuffer getScreenBuffer() { return screen; }
/**
* Refresh the field list by scanning the presentation space buffer.
*/
public synchronized void refresh() {
fields.clear();
clear();
if (screen == null || !screen.isFormatted()) {
return;
}
@@ -66,35 +71,35 @@ public class ECLFieldList {
}
byte faVal = screen.getCell(startPos).fa;
fields.add(new ECLField(ps, startPos, dataStart, endPos, len, faVal));
add(new ECLField(ps, startPos, dataStart, endPos, len, faVal));
}
}
public void Refresh() { refresh(); }
public synchronized int getFieldCount() {
return fields.size();
return size();
}
public int GetFieldCount() { return getFieldCount(); }
public synchronized List<ECLField> getFields() {
return Collections.unmodifiableList(new ArrayList<>(fields));
return Collections.unmodifiableList(new ArrayList<>(this));
}
public List<ECLField> GetFields() { return getFields(); }
public synchronized ECLField getFirstField() {
if (fields.isEmpty()) return null;
return fields.get(0);
if (isEmpty()) return null;
return get(0);
}
public ECLField GetFirstField() { return getFirstField(); }
public synchronized ECLField getNextField(ECLField prev) {
if (prev == null || fields.isEmpty()) return getFirstField();
for (int i = 0; i < fields.size(); i++) {
ECLField f = fields.get(i);
if (prev == null || isEmpty()) return getFirstField();
for (int i = 0; i < size(); i++) {
ECLField f = get(i);
if (f.equals(prev) || f.getStart() == prev.getStart()) {
if (i + 1 < fields.size()) {
return fields.get(i + 1);
if (i + 1 < size()) {
return get(i + 1);
}
return null;
}
@@ -107,12 +112,12 @@ public class ECLFieldList {
* Find the field that contains the specified buffer position.
*/
public synchronized ECLField findField(int pos) {
if (fields.isEmpty() || screen == null) return null;
if (isEmpty() || screen == null) return null;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return null;
pos = ((pos % size) + size) % size;
for (ECLField f : fields) {
for (ECLField f : this) {
int start = f.getStart();
int end = f.getEnd();
if (start <= end) {
@@ -140,14 +145,14 @@ public class ECLFieldList {
* Get the field preceding the given field in the field list.
*/
public synchronized ECLField getPreviousField(ECLField next) {
if (next == null || fields.isEmpty()) return null;
for (int i = 0; i < fields.size(); i++) {
ECLField f = fields.get(i);
if (next == null || isEmpty()) return null;
for (int i = 0; i < size(); i++) {
ECLField f = get(i);
if (f.equals(next) || f.getStart() == next.getStart()) {
if (i > 0) {
return fields.get(i - 1);
return get(i - 1);
} else {
return fields.get(fields.size() - 1);
return get(size() - 1);
}
}
}
@@ -195,29 +200,29 @@ public class ECLFieldList {
* Find field containing the given text string.
*/
public synchronized ECLField findField(String text, int startPos) {
if (text == null || text.isEmpty() || fields.isEmpty()) return null;
if (text == null || text.isEmpty() || isEmpty()) return null;
int size = screen.getRows() * screen.getCols();
if (size <= 0) return null;
startPos = ((startPos % size) + size) % size;
// Find starting index in field list
int startIdx = 0;
for (int i = 0; i < fields.size(); i++) {
if (fields.get(i).getStart() >= startPos) {
for (int i = 0; i < size(); i++) {
if (get(i).getStart() >= startPos) {
startIdx = i;
break;
}
}
for (int i = startIdx; i < fields.size(); i++) {
ECLField f = fields.get(i);
for (int i = startIdx; i < size(); i++) {
ECLField f = get(i);
if (f.getText().contains(text)) {
return f;
}
}
// Wrap around search to beginning of field list
for (int i = 0; i < startIdx; i++) {
ECLField f = fields.get(i);
ECLField f = get(i);
if (f.getText().contains(text)) {
return f;
}
@@ -265,18 +270,18 @@ public class ECLFieldList {
public boolean MatchAttributes(ECLField f, int mask) { return matchAttributes(f, mask); }
public synchronized ECLField locateField(int attrMask, ECLField prev) {
if (fields.isEmpty()) return null;
if (isEmpty()) return null;
int startIndex = 0;
if (prev != null) {
int idx = fields.indexOf(prev);
if (idx >= 0 && idx + 1 < fields.size()) {
int idx = indexOf(prev);
if (idx >= 0 && idx + 1 < size()) {
startIndex = idx + 1;
} else {
return null;
}
}
for (int i = startIndex; i < fields.size(); i++) {
ECLField f = fields.get(i);
for (int i = startIndex; i < size(); i++) {
ECLField f = get(i);
if (matchAttributes(f, attrMask)) {
return f;
}
@@ -2,6 +2,7 @@ package haus.nightmare.lib3270j.ecl;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@@ -109,6 +110,8 @@ public class ECLOIA implements ECLConstants {
session != null && session.getClient() != null ? session.getClient().getTelnetFSM() : null);
}
private final List<com.ibm.eNetwork.ECL.ECLOIANotify> hodOiaNotifies = new CopyOnWriteArrayList<>();
public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIANotify listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
@@ -132,6 +135,29 @@ public class ECLOIA implements ECLConstants {
UnregisterOIAEvent(listener);
}
public void RegisterOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) {
if (notify != null && !hodOiaNotifies.contains(notify)) {
hodOiaNotifies.add(notify);
}
}
public void registerOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) {
RegisterOIAEvent(notify);
}
public void UnregisterOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) {
if (notify != null) {
hodOiaNotifies.remove(notify);
try {
notify.NotifyStop(new com.ibm.eNetwork.ECL.ECLOIA(this), STOP_UNREGISTER);
} catch (Exception ignored) {}
}
}
public void unregisterOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) {
UnregisterOIAEvent(notify);
}
public void RegisterOIAEvent(ECLOIAListener listener) {
if (listener != null && !oiaListeners.contains(listener)) {
oiaListeners.add(listener);
@@ -166,6 +192,14 @@ public class ECLOIA implements ECLConstants {
l.OIANotifyError(this, err);
} catch (Exception ignored) {}
}
for (com.ibm.eNetwork.ECL.ECLOIANotify l : hodOiaNotifies) {
try {
com.ibm.eNetwork.ECL.ECLErr ferr = (err instanceof com.ibm.eNetwork.ECL.ECLErr)
? (com.ibm.eNetwork.ECL.ECLErr) err
: new com.ibm.eNetwork.ECL.ECLErr(err.getMessage());
l.NotifyError(new com.ibm.eNetwork.ECL.ECLOIA(this), ferr);
} catch (Exception ignored) {}
}
}
public void notifyOIAStop(int reason) {
@@ -179,6 +213,11 @@ public class ECLOIA implements ECLConstants {
l.OIANotifyStop(this, reason);
} catch (Exception ignored) {}
}
for (com.ibm.eNetwork.ECL.ECLOIANotify l : hodOiaNotifies) {
try {
l.NotifyStop(new com.ibm.eNetwork.ECL.ECLOIA(this), reason);
} catch (Exception ignored) {}
}
}
private final ReentrantLock oiaLock = new ReentrantLock();
@@ -210,6 +249,11 @@ public class ECLOIA implements ECLConstants {
}
} catch (Exception ignored) {}
}
for (com.ibm.eNetwork.ECL.ECLOIANotify l : hodOiaNotifies) {
try {
l.OIANotifyEvent(new com.ibm.eNetwork.ECL.ECLOIAEvent(event));
} catch (Exception ignored) {}
}
}
public boolean isInsertMode() {
@@ -267,6 +311,11 @@ public class ECLOIA implements ECLConstants {
public void setInputInhibited(int reason) {
if (this.inhibitOverride != reason) {
this.inhibitOverride = reason;
if (reason == INHIBIT_NOT_INHIBITED && inputProcessor != null && inputProcessor.isKeyboardLocked()) {
inputProcessor.setKeyboardLocked(false);
} else if (reason == INHIBIT_SYSTEM_LOCK && inputProcessor != null && !inputProcessor.isKeyboardLocked()) {
inputProcessor.setKeyboardLocked(true);
}
notifyOIAChanged();
}
}
@@ -424,6 +473,14 @@ public class ECLOIA implements ECLConstants {
return getInputInhibited();
}
public boolean isKeyLocked() {
return getInputInhibited() != INHIBIT_NOT_INHIBITED;
}
public boolean IsKeyLocked() {
return isKeyLocked();
}
/**
* Block until the keyboard becomes unlocked and ready for user input, or timeout expires.
* @return true if keyboard unlocked, false if timeout occurred.
@@ -5,6 +5,7 @@ import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@@ -155,20 +156,94 @@ public class ECLPS implements ECLConstants {
public ECLPSTHAIServices GetECLPSTHAIServices() { return thaiServices; }
public ECLPSTHAIServices getTHAIServices() { return thaiServices; }
public int getSize() { return screen.getRows() * screen.getCols(); }
public int getRows() { return screen.getRows(); }
public int getCols() { return screen.getCols(); }
public int getCursorPos(){ return screen.getCursorAddress(); }
public int getCursorRow(){ return screen.getCursorRow(); }
public int getCursorCol(){ return screen.getCursorCol(); }
public int getSize() { return screen != null ? screen.getRows() * screen.getCols() : 0; }
public int getRows() { return screen != null ? screen.getRows() : 0; }
public int getCols() { return screen != null ? screen.getCols() : 0; }
public int GetSize() { return getSize(); }
public int GetRows() { return getRows(); }
public int GetCols() { return getCols(); }
public int GetSizeRows() { return getRows(); }
public int GetSizeCols() { return getCols(); }
public int getCursorPos(){ return screen != null ? screen.getCursorAddress() : 0; }
public int getCursorRow(){ return screen != null ? screen.getCursorRow() : 0; }
public int getCursorCol(){ return screen != null ? screen.getCursorCol() : 0; }
public synchronized int GetCursorPos() {
return (screen != null) ? screen.getCursorAddress() + 1 : 1;
}
public synchronized int GetCursorRow() {
return (screen != null) ? screen.getCursorRow() + 1 : 1;
}
public synchronized int GetCursorCol() {
return (screen != null) ? screen.getCursorCol() + 1 : 1;
}
public void setCursorPos(int pos) {
screen.setCursorAddress(pos);
if (screen != null) {
screen.setCursorAddress(pos);
}
}
public void setCursorPos(int row, int col) {
int pos = screen.rowColToAddress(row, col);
screen.setCursorAddress(pos);
if (screen != null) {
int pos = screen.rowColToAddress(row, col);
screen.setCursorAddress(pos);
}
}
public synchronized void SetCursorPos(int pos) throws com.ibm.eNetwork.ECL.ECLErr {
if (pos < 1 || pos > getSize()) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetCursorPos", "ECL0010", "1", String.valueOf(pos));
}
setCursorPos(pos - 1);
notifyCursorMoved(0, pos - 1);
}
public synchronized void SetCursorPos(int row, int col) throws com.ibm.eNetwork.ECL.ECLErr {
SetCursorPos(ConvertRowColToPos(row, col));
}
public synchronized int ConvertPosToRow(int pos) throws com.ibm.eNetwork.ECL.ECLErr {
if (pos < 1 || pos > getSize()) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertPosToRow", "ECL0010", "1", String.valueOf(pos));
}
int cols = getCols();
return (cols > 0) ? ((pos - 1) / cols + 1) : 1;
}
public synchronized int ConvertPosToCol(int pos) throws com.ibm.eNetwork.ECL.ECLErr {
if (pos < 1 || pos > getSize()) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertPosToCol", "ECL0010", "1", String.valueOf(pos));
}
int cols = getCols();
return (cols > 0) ? ((pos - 1) % cols + 1) : 1;
}
public synchronized int ConvertRowColToPos(int row, int col) throws com.ibm.eNetwork.ECL.ECLErr {
if (row < 1 || row > getRows()) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertRowColToPos", "ECL0010", "\"row\"", String.valueOf(row));
}
if (col < 1 || col > getCols()) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertRowColToPos", "ECL0010", "\"col\"", String.valueOf(col));
}
return (row - 1) * getCols() + col;
}
public synchronized void ConvertPosToRowCol(int pos, int[] rowCol) throws com.ibm.eNetwork.ECL.ECLErr {
if (rowCol == null || rowCol.length < 2) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertPosToRowCol", "ECL0010", "\"rowCol\"", "invalid array");
}
rowCol[0] = ConvertPosToRow(pos);
rowCol[1] = ConvertPosToCol(pos);
}
public synchronized int[] ConvertPosToRowCol(int pos) throws com.ibm.eNetwork.ECL.ECLErr {
int[] rc = new int[2];
ConvertPosToRowCol(pos, rc);
return rc;
}
/**
@@ -277,6 +352,60 @@ public class ECLPS implements ECLConstants {
return getString(pos, length);
}
public synchronized int GetString(char[] buffer, int len, int pos) throws com.ibm.eNetwork.ECL.ECLErr {
return GetString(buffer, len, pos, len);
}
public synchronized int GetString(char[] buffer, int len, int pos, int length) throws com.ibm.eNetwork.ECL.ECLErr {
if (buffer == null) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0011", "1");
}
int size = getSize();
if (pos < 1 || pos > size) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"position\"", String.valueOf(pos));
}
if (len <= 0) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"bufferLength\"", String.valueOf(len));
}
if (length <= 0) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"length\"", String.valueOf(length));
}
int copyCount = Math.min(Math.min(buffer.length, len), Math.min(length, size - pos + 1));
char[] temp = new char[copyCount];
getPlane(PLANE_TEXT, temp, pos - 1, copyCount);
System.arraycopy(temp, 0, buffer, 0, copyCount);
return copyCount;
}
public synchronized int GetString(char[] buffer, int len, int row, int col, int length) throws com.ibm.eNetwork.ECL.ECLErr {
return GetString(buffer, len, ConvertRowColToPos(row, col), length);
}
public synchronized String GetString(int pos, int length) throws com.ibm.eNetwork.ECL.ECLErr {
int size = getSize();
if (pos < 1 || pos > size) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"pos\"", String.valueOf(pos));
}
return getString(pos - 1, length);
}
public synchronized String GetString(int row, int col, int length) throws com.ibm.eNetwork.ECL.ECLErr {
return GetString(ConvertRowColToPos(row, col), length);
}
public synchronized int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
if (buffer == null || len <= 0 || length <= 0 || screen == null) return 0;
int r = (row > 0) ? row - 1 : 0;
int c = (col > 0) ? col - 1 : 0;
int pos = screen.rowColToAddress(r, c);
int copyCount = Math.min(Math.min(buffer.length, len), length);
return getPlane(plane, buffer, pos, copyCount);
}
public int getScreen(char[] buffer, int len, int row, int col, int length, int plane) {
return GetScreen(buffer, len, row, col, length, plane);
}
/**
* Insert text directly into unprotected fields in the presentation space starting at pos.
*/
@@ -319,6 +448,34 @@ public class ECLPS implements ECLConstants {
setText(text, p);
}
/**
* Insert text at current cursor position.
*/
public void SetText(String text) {
SetText(text, GetCursorPos());
}
public void setText(String text) {
SetText(text);
}
public synchronized void SetString(String text, int pos) throws com.ibm.eNetwork.ECL.ECLErr {
if (text == null) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetString", "ECL0010", "\"text\"", "null");
}
if (pos < 1 || pos > getSize()) {
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetString", "ECL0010", "\"pos\"", String.valueOf(pos));
}
SetText(text, pos);
}
public synchronized void SetString(String text, int row, int col) throws com.ibm.eNetwork.ECL.ECLErr {
SetString(text, ConvertRowColToPos(row, col));
}
public synchronized void SetString(String text) throws com.ibm.eNetwork.ECL.ECLErr {
SetString(text, GetCursorPos());
}
/**
* Search for a string in the presentation space (0-based indexing).
* Returns 0-based position, or -1 if not found.
@@ -500,6 +657,86 @@ public class ECLPS implements ECLConstants {
return SearchPSExt(text, startRow, startCol, endRow, endCol, dir, ignoreCase, wrap);
}
// ========== IBM HoD SearchText / SearchString (1-based API) ==========
public synchronized int SearchText(String text) {
return SearchText(text, 1, SEARCH_FORWARD);
}
public synchronized int searchText(String text) {
return SearchText(text);
}
public synchronized int SearchText(String text, int dir) {
return dir == SEARCH_BACKWARD ? SearchText(text, getSize(), dir) : SearchText(text, 1, dir);
}
public synchronized int searchText(String text, int dir) {
return SearchText(text, dir);
}
public synchronized int SearchText(String text, int arg1, int arg2) {
int startPos;
int dir;
if (arg1 > 2) {
startPos = arg1;
dir = arg2;
} else if (arg2 > 2) {
dir = arg1;
startPos = arg2;
} else {
dir = arg1;
startPos = arg2;
}
if (text == null || text.isEmpty() || screen == null) return 0;
int size = getSize();
if (size <= 0) return 0;
if (startPos < 1) startPos = 1;
if (startPos > size) startPos = size;
char[] fullScreen = new char[size];
getPlane(PLANE_TEXT, fullScreen, 0, size);
for (int i = 0; i < size; i++) {
if (fullScreen[i] == '\0') fullScreen[i] = ' ';
}
String screenText = new String(fullScreen);
int idx = (dir == SEARCH_BACKWARD) ? screenText.lastIndexOf(text, startPos - 1)
: screenText.indexOf(text, startPos - 1);
return idx == -1 ? 0 : idx + 1;
}
public synchronized int searchText(String text, int arg1, int arg2) {
return SearchText(text, arg1, arg2);
}
public synchronized int SearchText(String text, int row, int col, int dir) throws com.ibm.eNetwork.ECL.ECLErr {
return SearchText(text, ConvertRowColToPos(row, col), dir);
}
public synchronized int searchText(String text, int row, int col, int dir) throws com.ibm.eNetwork.ECL.ECLErr {
return SearchText(text, row, col, dir);
}
public synchronized int SearchString(String text) {
return SearchText(text);
}
public synchronized int searchString(String text, int dir) {
return SearchText(text, dir);
}
public synchronized int SearchString(String text, int dir) {
return SearchText(text, dir);
}
public synchronized int SearchString(String text, int arg1, int arg2) {
return SearchText(text, arg1, arg2);
}
public synchronized int SearchString(String text, int row, int col, int dir) throws com.ibm.eNetwork.ECL.ECLErr {
return SearchText(text, row, col, dir);
}
private boolean matchesAt(String screenText, String target, int pos, int size) {
int len = target.length();
for (int j = 0; j < len; j++) {
@@ -608,13 +845,37 @@ public class ECLPS implements ECLConstants {
public void SetPasteStopAtProtectedLine(boolean val) { setPasteStopAtProtectedLine(val); }
private boolean pasteWordBreak = true;
public boolean isPasteWordBreak() {
if (session != null && session.getProperties() != null) {
String p = session.getProperties().getProperty(ECLSession.PASTE_WORD_BREAK);
if (p != null) return Boolean.parseBoolean(p);
}
return inputProcessor != null ? inputProcessor.isPasteWordBreak() : pasteWordBreak;
}
public boolean IsPasteWordBreak() { return isPasteWordBreak(); }
public void setPasteWordBreak(boolean val) {
this.pasteWordBreak = val;
if (session != null && session.getProperties() != null) {
session.getProperties().setProperty(ECLSession.PASTE_WORD_BREAK, String.valueOf(val));
}
if (inputProcessor != null) {
inputProcessor.setPasteWordBreak(val);
}
}
public void SetPasteWordBreak(boolean val) { setPasteWordBreak(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(), isBlockPaste());
return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine(), isBlockPaste(), isPasteWordBreak());
}
return 0;
}
@@ -630,7 +891,7 @@ public class ECLPS implements ECLConstants {
if (text == null || text.isEmpty() || screen == null) return 0;
if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine() || isBlockPaste())) {
setCursorPos(row, col);
return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine(), isBlockPaste());
return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine(), isBlockPaste(), isPasteWordBreak());
}
int rows = screen.getRows();
int cols = screen.getCols();
@@ -920,6 +1181,10 @@ public class ECLPS implements ECLConstants {
// ========== ECLPS Event Listener Management ==========
private final java.util.List<ECLPSListener> psListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
private final java.util.List<com.ibm.eNetwork.ECL.ECLPSNotify> psNotifies = new java.util.concurrent.CopyOnWriteArrayList<>();
private final java.util.Map<com.ibm.eNetwork.ECL.ECLPSNotify, Integer> notifyEventTypes = new java.util.concurrent.ConcurrentHashMap<>();
private final java.util.Map<com.ibm.eNetwork.ECL.ECLPSNotify, Object> notifyDescriptors = new java.util.concurrent.ConcurrentHashMap<>();
private final java.util.List<com.ibm.eNetwork.ECL.event.ECLPSListener> hodPsListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
public boolean isCursorVisible() { return cursorVisible; }
public boolean GetCursorVisible() { return cursorVisible; }
@@ -1007,6 +1272,111 @@ public class ECLPS implements ECLConstants {
UnregisterPSEvent(listener);
}
public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) {
RegisterPSEvent(notify, ALL_EVENTS);
}
public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) {
RegisterPSEvent(notify);
}
public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, int eventType) {
if (notify != null) {
notifyEventTypes.put(notify, eventType);
if (!psNotifies.contains(notify)) {
psNotifies.add(notify);
}
}
}
public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, int eventType) {
RegisterPSEvent(notify, eventType);
}
public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc) {
RegisterPSEvent(notify, desc, ALL_EVENTS);
}
public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc) {
RegisterPSEvent(notify, desc);
}
public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
if (notify != null) {
if (desc != null) {
notifyDescriptors.put(notify, desc);
} else {
notifyDescriptors.remove(notify);
}
notifyEventTypes.put(notify, eventType);
if (!psNotifies.contains(notify)) {
psNotifies.add(notify);
}
}
}
public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
RegisterPSEvent(notify, desc, eventType);
}
public void UnregisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) {
if (notify != null) {
psNotifies.remove(notify);
notifyDescriptors.remove(notify);
notifyEventTypes.remove(notify);
try {
notify.NotifyStop(new com.ibm.eNetwork.ECL.ECLPS(this), STOP_UNREGISTER);
} catch (Exception ignored) {}
}
}
public void unregisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) {
UnregisterPSEvent(notify);
}
public void RegisterPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) {
if (listener != null && !hodPsListeners.contains(listener)) {
hodPsListeners.add(listener);
}
}
public void registerPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) {
RegisterPSEvent(listener);
}
public void UnregisterPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) {
if (listener != null) {
hodPsListeners.remove(listener);
}
}
public void unregisterPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) {
UnregisterPSEvent(listener);
}
private static final String[] HACL_MNEMONICS = new String[]{
"[attn]", "[backspace]", "[backtab]", "[clear]", "[down]", "[left]", "[right]", "[up]",
"[delete]", "[enter]", "[tab]", "[eof]", "[eraseeof]", "[erasefld]", "[erinp]",
"[pf1]", "[pf2]", "[pf3]", "[pf4]", "[pf5]", "[pf6]", "[pf7]", "[pf8]", "[pf9]",
"[pf10]", "[pf11]", "[pf12]", "[pf13]", "[pf14]", "[pf15]", "[pf16]", "[pf17]",
"[pf18]", "[pf19]", "[pf20]", "[pf21]", "[pf22]", "[pf23]", "[pf24]",
"[newline]", "[pa1]", "[pa2]", "[pa3]", "[pagedn]", "[pageup]", "[sysreq]",
"[wordleft]", "[wordright]", "[backtabword]", "[tabword]", "[previousword]",
"[nextword]", "[deleteword]", "[insert]", "[home]", "[reset]", "[dup]",
"[fieldmark]", "[fldext]", "[field+]", "[field-]", "[test]", "[help]",
"[bof]", "[cursel]", "[selfld]", "[deselfld]", "[+cr]", "[altview]",
"[printhost]", "[screenrev]", "[copy]", "[paste]", "[cut]", "[markleft]",
"[markright]", "[markup]", "[markdown]", "[unmark]", "[enterreset]"
};
public String[] GetSendKeyMnemonics() {
return HACL_MNEMONICS.clone();
}
public String[] getSendKeyMnemonics() {
return GetSendKeyMnemonics();
}
public void dispatchEvent(ECLPSEvent event) {
notifyPSEvent(event);
}
@@ -1045,6 +1415,26 @@ public class ECLPS implements ECLConstants {
}
} catch (Exception ignored) {}
}
for (com.ibm.eNetwork.ECL.ECLPSNotify n : psNotifies) {
Object d = notifyDescriptors.get(n);
if (d instanceof ECLScreenDesc && !((ECLScreenDesc) d).Matches(this, session != null ? session.GetOIA() : null)) {
continue;
}
int filter = notifyEventTypes.getOrDefault(n, ALL_EVENTS);
int evtCategory = event.GetType();
if (filter != ALL_EVENTS && (filter & evtCategory) == 0) {
continue;
}
try {
n.NotifyEvent(new com.ibm.eNetwork.ECL.ECLPS(this));
} catch (Exception ignored) {}
}
for (com.ibm.eNetwork.ECL.event.ECLPSListener hl : hodPsListeners) {
try {
com.ibm.eNetwork.ECL.event.ECLPSEvent facadeEvt = new com.ibm.eNetwork.ECL.event.ECLPSEvent(event);
hl.PSNotifyEvent(facadeEvt);
} catch (Exception ignored) {}
}
}
public void notifyPSError(ECLErr err) {
@@ -1053,6 +1443,19 @@ public class ECLPS implements ECLConstants {
l.PSNotifyError(this, err);
} catch (Exception ignored) {}
}
com.ibm.eNetwork.ECL.ECLErr ferr = (err instanceof com.ibm.eNetwork.ECL.ECLErr)
? (com.ibm.eNetwork.ECL.ECLErr) err
: new com.ibm.eNetwork.ECL.ECLErr(err != null ? err.getMessage() : "");
for (com.ibm.eNetwork.ECL.ECLPSNotify n : psNotifies) {
try {
n.NotifyError(new com.ibm.eNetwork.ECL.ECLPS(this), ferr);
} catch (Exception ignored) {}
}
for (com.ibm.eNetwork.ECL.event.ECLPSListener hl : hodPsListeners) {
try {
hl.PSNotifyError(new com.ibm.eNetwork.ECL.ECLPS(this), ferr);
} catch (Exception ignored) {}
}
}
public void notifyPSStop(int reason) {
@@ -1061,6 +1464,16 @@ public class ECLPS implements ECLConstants {
l.PSNotifyStop(this, reason);
} catch (Exception ignored) {}
}
for (com.ibm.eNetwork.ECL.ECLPSNotify n : psNotifies) {
try {
n.NotifyStop(new com.ibm.eNetwork.ECL.ECLPS(this), reason);
} catch (Exception ignored) {}
}
for (com.ibm.eNetwork.ECL.event.ECLPSListener hl : hodPsListeners) {
try {
hl.PSNotifyStop(new com.ibm.eNetwork.ECL.ECLPS(this), reason);
} catch (Exception ignored) {}
}
}
public void notifyPSUpdate(int startRow, int startCol, int endRow, int endCol, boolean full) {
@@ -1128,6 +1541,25 @@ public class ECLPS implements ECLConstants {
sendKeys(keys);
}
public void sendKeys(String keys, int pos) {
if (pos >= 1 && pos <= getSize()) {
setCursorPos(pos - 1);
}
sendKeys(keys);
}
public void SendKeys(String keys, int pos) {
sendKeys(keys, pos);
}
public void SendKeysNoAid(String string, boolean bl) {
sendKeys(string);
}
public void SendKeysNoAid(String string, int row, int col, boolean bl) {
sendKeys(string, row, col);
}
/**
* Send keystrokes with configurable inter-character or inter-mnemonic delay.
*/
@@ -1154,6 +1586,17 @@ public class ECLPS implements ECLConstants {
sendCharacters(keys, row, col, delayMs);
}
public void sendCharacters(String keys, int pos, int delayMs) {
if (pos >= 1 && pos <= getSize()) {
setCursorPos(pos - 1);
}
sendCharacters(keys, delayMs);
}
public void SendCharacters(String keys, int pos, int delayMs) {
sendCharacters(keys, pos, delayMs);
}
public void sendCharacters(String keys, int delayMs) {
if (keys == null || keys.isEmpty()) return;
if (delayMs <= 0) {
@@ -1161,37 +1604,36 @@ public class ECLPS implements ECLConstants {
return;
}
int i = 0;
int len = keys.length();
while (i < len) {
if (keys.charAt(i) == '[') {
if (i + 1 < len && keys.charAt(i + 1) == '[') {
// Escaped bracket "[["
sendKeys("[");
i += 2;
try {
SendKeyTokenizer tokenizer = new SendKeyTokenizer(keys);
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (token == null) break;
if (token.length() == 1) {
if (inputProcessor != null) inputProcessor.typeCharacter(token.charAt(0));
if (delayMs > 0 && tokenizer.hasMoreTokens()) {
Thread.sleep(delayMs);
}
} else if (token.startsWith("[") && token.endsWith("]")) {
if (inputProcessor != null) inputProcessor.executeMnemonicToken(token);
if (delayMs > 0 && tokenizer.hasMoreTokens()) {
Thread.sleep(delayMs);
}
} else {
int close = keys.indexOf(']', i);
if (close > i) {
String mnemonic = keys.substring(i, close + 1);
sendKeys(mnemonic);
i = close + 1;
} else {
sendKeys(keys.substring(i, i + 1));
i++;
for (int i = 0; i < token.length(); i++) {
if (inputProcessor != null) inputProcessor.typeCharacter(token.charAt(i));
if (delayMs > 0 && (i < token.length() - 1 || tokenizer.hasMoreTokens())) {
Thread.sleep(delayMs);
}
}
}
} else {
sendKeys(keys.substring(i, i + 1));
i++;
}
if (delayMs > 0 && i < len) {
try {
Thread.sleep(delayMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ECLErr e) {
if (inputProcessor != null) {
sendKeys(keys);
}
}
}
@@ -196,6 +196,14 @@ public class ECLPSEvent extends EventObject {
return getECLPSUpdate();
}
public ECLPSUpdate getPSUpdate() {
return getECLPSUpdate();
}
public ECLPSUpdate GetPSUpdate() {
return getECLPSUpdate();
}
public ECLPS getPS() {
return (getSource() instanceof ECLPS) ? (ECLPS) getSource() : null;
}
@@ -1,279 +1,487 @@
package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import java.io.Serializable;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDAttrib;
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlock;
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDChecksum;
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDCursor;
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDFields;
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDInputFields;
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDOIA;
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDString;
import haus.nightmare.lib3270j.ecl.screenreco.ECLScreenDescriptor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
/**
* Screen descriptor matching engine for IBM Host On-Demand ECL automation.
* Screen descriptor composite matching engine for IBM Host On-Demand ECL automation.
* Encapsulates criteria for matching host screens (strings, rectangular regions, cursor positions,
* field counts, and OIA status).
* field counts, OIA status, checksums, and attributes) and supports HACL XML schema serialization.
*/
public class ECLScreenDesc {
public class ECLScreenDesc implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private final List<ScreenCondition> conditions = new ArrayList<>();
private interface ScreenCondition {
boolean matches(ECLPS ps, ECLOIA oia);
}
private final List<ECLScreenDescriptor> descriptors = new CopyOnWriteArrayList<>();
private String name = "";
private boolean active = true;
private boolean match = false;
public boolean useOIAInhibitStatus = false;
public ECLScreenDesc() {}
/**
* Clear all conditions from this descriptor.
*/
public synchronized void Clear() {
conditions.clear();
public ECLScreenDesc(String xml) {
if (xml != null && !xml.trim().isEmpty()) {
setXML(xml);
}
}
public synchronized void clear() {
Clear();
}
// ========== Descriptor Management ==========
/**
* Add a condition that the given string must appear anywhere on the presentation space.
*/
public synchronized void AddString(String text) {
public void AddDescriptor(ECLScreenDescriptor descriptor) {
if (descriptor != null) {
descriptors.add(descriptor);
}
}
public void addDescriptor(ECLScreenDescriptor descriptor) { AddDescriptor(descriptor); }
public void RemoveDescriptor(ECLScreenDescriptor descriptor) {
if (descriptor != null) {
descriptors.remove(descriptor);
}
}
public void removeDescriptor(ECLScreenDescriptor descriptor) { RemoveDescriptor(descriptor); }
public List<ECLScreenDescriptor> GetDescriptors() {
return Collections.unmodifiableList(new ArrayList<>(descriptors));
}
public List<ECLScreenDescriptor> getDescriptors() { return GetDescriptors(); }
public ECLScreenDescriptor Get(int index) {
return descriptors.get(index);
}
public ECLScreenDescriptor get(int index) { return Get(index); }
public int Size() { return descriptors.size(); }
public int size() { return Size(); }
public int getConditionCount() { return Size(); }
public void Clear() { descriptors.clear(); }
public void clear() { Clear(); }
public String GetName() { return name; }
public String getName() { return name; }
public void SetName(String name) { this.name = (name != null) ? name : ""; }
public void setName(String name) { SetName(name); }
public boolean IsActive() { return active; }
public boolean isActive() { return active; }
public void SetActive(boolean active) { this.active = active; }
public void setActive(boolean active) { SetActive(active); }
public boolean IsMatch() { return match; }
public boolean isMatch() { return match; }
public void SetMatch(boolean match) { this.match = match; }
public void setMatch(boolean match) { SetMatch(match); }
// ========== Programmatic Add* Methods ==========
public void AddString(String text) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
return ps.searchString(text) >= 0;
});
AddDescriptor(new ECLSDString(text));
}
public void addString(String text) { AddString(text); }
public synchronized void addString(String text) {
AddString(text);
}
/**
* Add a condition that text must appear at 1-based (row, col) position (case-insensitive by default).
*/
public synchronized void AddString(String text, int row, int col) {
public void AddString(String text, int row, int col) {
AddString(text, row, col, true);
}
public void addString(String text, int row, int col) { AddString(text, row, col); }
public synchronized void addString(String text, int row, int col) {
AddString(text, row, col, true);
}
/**
* Add a condition that text must appear at (row, col) with case sensitivity control.
* Note: 1-based indexing conforming to IBM ECL specification (row 1..rows, col 1..cols).
*/
public synchronized void AddString(String text, int row, int col, boolean caseSense) {
public void AddString(String text, int row, int col, boolean caseSense) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
int r = (row > 0) ? row - 1 : 0;
int c = (col > 0) ? col - 1 : 0;
String onScreen = ps.getString(r, c, text.length());
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
});
AddDescriptor(new ECLSDString(text, row, col, caseSense));
}
public void addString(String text, int row, int col, boolean caseSense) { AddString(text, row, col, caseSense); }
public synchronized void addString(String text, int row, int col, boolean caseSense) {
AddString(text, row, col, caseSense);
}
/**
* Add a condition that text must appear at a 1-based linear buffer position.
*/
public synchronized void AddString(String text, int pos, boolean caseSense) {
public void AddString(String text, int pos, boolean caseSense) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
int p0 = (pos > 0) ? pos - 1 : 0;
String onScreen = ps.getString(p0, text.length());
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
});
AddDescriptor(new ECLSDString(text, pos, caseSense));
}
public void addString(String text, int pos, boolean caseSense) { AddString(text, pos, caseSense); }
public synchronized void addString(String text, int pos, boolean caseSense) {
AddString(text, pos, caseSense);
}
/**
* Add a condition that text must appear within a rectangular region.
*/
public synchronized void AddStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
public void AddStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> {
if (ps == null) return false;
int sr = (sRow > 0) ? sRow - 1 : 0;
int sc = (sCol > 0) ? sCol - 1 : 0;
int er = (eRow > 0) ? eRow - 1 : 0;
int ec = (eCol > 0) ? eCol - 1 : 0;
String block = ps.copyString(sr, sc, er, ec);
if (block == null) return false;
return caseSense ? block.contains(text) : block.toLowerCase().contains(text.toLowerCase());
});
AddDescriptor(new ECLSDString(text, sRow, sCol, eRow, eCol, caseSense, false, false));
}
public synchronized void addStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
public void addStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
AddStringInRect(text, sRow, sCol, eRow, eCol, caseSense);
}
/**
* Add a condition that the cursor must be located at 1-based (row, col).
*/
public synchronized void AddCursorPos(int row, int col) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int r = (row > 0) ? row - 1 : 0;
int c = (col > 0) ? col - 1 : 0;
return ps.getCursorRow() == r && ps.getCursorCol() == c;
});
public void AddCursorPos(int row, int col) {
AddDescriptor(new ECLSDCursor(row, col));
}
public void addCursorPos(int row, int col) { AddCursorPos(row, col); }
public synchronized void addCursorPos(int row, int col) {
AddCursorPos(row, col);
public void AddCursorPos(int pos) {
AddDescriptor(new ECLSDCursor(pos));
}
public void addCursorPos(int pos) { AddCursorPos(pos); }
/**
* Add a condition that the cursor must be located at 1-based linear buffer position.
*/
public synchronized void AddCursorPos(int pos) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int p0 = (pos > 0) ? pos - 1 : 0;
return ps.getCursorPos() == p0;
});
public void AddNumFields(int count) {
AddDescriptor(new ECLSDFields(count));
}
public void addNumFields(int count) { AddNumFields(count); }
public synchronized void addCursorPos(int pos) {
AddCursorPos(pos);
public void AddNumInputFields(int count) {
AddDescriptor(new ECLSDInputFields(count));
}
public void addNumInputFields(int count) { AddNumInputFields(count); }
/**
* Add a condition on total field count on the formatted screen.
*/
public synchronized void AddNumFields(int count) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
return ps.getFieldList().getFieldCount() == count;
});
public void AddOIAStatus(int status) {
ECLSDOIA oia = new ECLSDOIA();
oia.setExplicitInhibitStatus(status);
AddDescriptor(oia);
}
public void addOIAStatus(int status) { AddOIAStatus(status); }
public synchronized void addNumFields(int count) {
AddNumFields(count);
}
/**
* Add a condition on number of unprotected input fields on the screen.
*/
public synchronized void AddNumInputFields(int count) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int inputCount = 0;
for (ECLField f : ps.getFieldList().getFields()) {
if (!f.isProtected()) {
inputCount++;
}
}
return inputCount == count;
});
}
public synchronized void addNumInputFields(int count) {
AddNumInputFields(count);
}
/**
* Add a condition on OIA inhibit status.
*/
public synchronized void AddOIAStatus(int status) {
conditions.add((ps, oia) -> {
if (oia == null) return true;
return oia.getInputInhibited() == status;
});
}
public synchronized void addOIAStatus(int status) {
public void AddOIAInhibitStatus(int status) {
AddOIAStatus(status);
}
public void addOIAInhibitStatus(int status) { AddOIAInhibitStatus(status); }
public void AddAttrib(char c, int row, int col, int plane) {
AddDescriptor(new ECLSDAttrib(c, row, col, plane));
}
public void addAttrib(char c, int row, int col, int plane) { AddAttrib(c, row, col, plane); }
public void AddAttrib(char c, int row, int col, int plane, boolean optional) {
AddDescriptor(new ECLSDAttrib(c, row, col, plane, optional));
}
public void addAttrib(char c, int row, int col, int plane, boolean optional) {
AddAttrib(c, row, col, plane, optional);
}
public void AddFieldAttr(int pos, int attr) {
AddDescriptor(new ECLScreenDescriptor() {
private static final long serialVersionUID = 1L;
@Override
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
if (ps == null) return false;
int p0 = (pos > 0) ? pos - 1 : 0;
ECLField f = ps.getField(p0);
if (f == null) return false;
return ps.getFieldList().matchAttributes(f, attr);
}
@Override
public String format(int indent, boolean full) {
return "<attrib pos=\"" + pos + "\" attr=\"" + attr + "\" />";
}
});
}
public void addFieldAttr(int pos, int attr) { AddFieldAttr(pos, attr); }
public void AddFieldAttr(int row, int col, int attr) {
AddDescriptor(new ECLScreenDescriptor() {
private static final long serialVersionUID = 1L;
@Override
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
if (ps == null) return false;
int r = (row > 0) ? row - 1 : 0;
int c = (col > 0) ? col - 1 : 0;
ECLField f = ps.getField(r, c);
if (f == null) return false;
return ps.getFieldList().matchAttributes(f, attr);
}
@Override
public String format(int indent, boolean full) {
return "<attrib row=\"" + row + "\" col=\"" + col + "\" attr=\"" + attr + "\" />";
}
});
}
public void addFieldAttr(int row, int col, int attr) { AddFieldAttr(row, col, attr); }
public void AddChecksum(int checksum) {
AddDescriptor(new ECLSDChecksum(checksum));
}
public void addChecksum(int checksum) { AddChecksum(checksum); }
public void AddChecksum(int checksum, boolean optional) {
AddDescriptor(new ECLSDChecksum(checksum, optional));
}
public void addChecksum(int checksum, boolean optional) { AddChecksum(checksum, optional); }
public void AddBlock(String[] strings, int sRow, int sCol, boolean caseSense) {
AddDescriptor(new ECLSDBlock(strings, sRow, sCol, caseSense));
}
public void addBlock(String[] strings, int sRow, int sCol, boolean caseSense) {
AddBlock(strings, sRow, sCol, caseSense);
}
public void AddBlock(ECLSDBlock block) {
AddDescriptor(block);
}
public void addBlock(ECLSDBlock block) { AddBlock(block); }
// ========== Matching Evaluation ==========
/**
* Check if the current screen state matches all conditions in this descriptor.
*/
public synchronized boolean Matches(ECLPS ps, ECLOIA oia) {
if (conditions.isEmpty()) {
if (descriptors.isEmpty()) {
this.match = true;
return true;
}
for (ScreenCondition cond : conditions) {
if (!cond.matches(ps, oia)) {
return false;
if (ps == null) {
this.match = false;
return false;
}
boolean hasNonOptional = false;
boolean allNonOptionalMatch = true;
boolean hasOptional = false;
boolean anyOptionalMatch = false;
for (ECLScreenDescriptor desc : descriptors) {
boolean m = desc.matches(ps, oia);
if (desc.isOptional()) {
hasOptional = true;
if (m) anyOptionalMatch = true;
} else {
hasNonOptional = true;
if (!m) allNonOptionalMatch = false;
}
}
return true;
boolean res;
if (hasNonOptional) {
res = allNonOptionalMatch;
} else if (hasOptional) {
res = anyOptionalMatch;
} else {
res = true;
}
this.match = res;
return res;
}
public boolean matches(ECLPS ps, ECLOIA oia) {
return Matches(ps, oia);
}
public boolean matches(ECLPS ps, ECLOIA oia) { return Matches(ps, oia); }
public boolean Matches(ECLSession session) {
if (session == null) return false;
return Matches(session.GetPS(), session.GetOIA());
}
public boolean matches(ECLSession session) {
return Matches(session);
}
public boolean matches(ECLSession session) { return Matches(session); }
public boolean Matches(Telnet3270Client client) {
if (client == null) return false;
return Matches(client.getPS(), client.getOIA());
}
public boolean matches(Telnet3270Client client) { return Matches(client); }
public boolean matches(Telnet3270Client client) {
return Matches(client);
}
/**
* Add a condition on field attributes at a 1-based buffer position.
*/
public synchronized void AddFieldAttr(int pos, int attr) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int p0 = (pos > 0) ? pos - 1 : 0;
ECLField f = ps.getField(p0);
if (f == null) return false;
return ps.getFieldList().matchAttributes(f, attr);
});
}
public synchronized void addFieldAttr(int pos, int attr) {
AddFieldAttr(pos, attr);
}
public synchronized void AddFieldAttr(int row, int col, int attr) {
conditions.add((ps, oia) -> {
if (ps == null) return false;
int r = (row > 0) ? row - 1 : 0;
int c = (col > 0) ? col - 1 : 0;
ECLField f = ps.getField(r, c);
if (f == null) return false;
return ps.getFieldList().matchAttributes(f, attr);
});
}
public synchronized void addFieldAttr(int row, int col, int attr) {
AddFieldAttr(row, col, attr);
}
public boolean useOIAInhibitStatus = false;
/**
* Check if a raw ScreenBuffer matches all criteria in this screen descriptor.
*/
public boolean Matches(ScreenBuffer buf) {
if (buf == null) return false;
return Matches(new ECLPS(buf, null, null), null);
}
public boolean matches(ScreenBuffer buf) {
return Matches(buf);
public boolean matches(ScreenBuffer buf) { return Matches(buf); }
// ========== XML Serialization & Parsing ==========
public String getXML() {
StringBuilder sb = new StringBuilder();
sb.append("<description>\n");
for (ECLScreenDescriptor desc : descriptors) {
sb.append(" ").append(desc.format(0, false)).append("\n");
}
sb.append("</description>");
return sb.toString();
}
public String GetXML() { return getXML(); }
public void setXML(String xml) {
Clear();
if (xml == null || xml.trim().isEmpty()) {
return;
}
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
} catch (Throwable ignored) {}
try {
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
} catch (Throwable ignored) {}
try {
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
} catch (Throwable ignored) {}
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xml.trim())));
Element root = doc.getDocumentElement();
Element descElement = root;
if (!root.getTagName().equalsIgnoreCase("description")) {
NodeList list = root.getElementsByTagName("description");
if (list.getLength() > 0) {
descElement = (Element) list.item(0);
}
}
NodeList children = descElement.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) continue;
Element elem = (Element) node;
String tag = elem.getTagName().toLowerCase();
boolean optional = "true".equalsIgnoreCase(elem.getAttribute("optional"));
boolean invertmatch = "true".equalsIgnoreCase(elem.getAttribute("invertmatch"));
String hostid = elem.getAttribute("hostid");
if (tag.equals("string")) {
String value = elem.getAttribute("value");
int row = parseIntAttr(elem, "row", 0);
int col = parseIntAttr(elem, "col", 0);
int erow = parseIntAttr(elem, "erow", -1);
int ecol = parseIntAttr(elem, "ecol", -1);
boolean casesense = "true".equalsIgnoreCase(elem.getAttribute("casesense"));
boolean wrap = "true".equalsIgnoreCase(elem.getAttribute("wrap"));
ECLSDString s;
if (row > 0 && col > 0) {
if (erow > 0 || ecol > 0) {
s = new ECLSDString(value, row, col, erow, ecol, casesense, optional, wrap);
} else {
s = new ECLSDString(value, row, col, casesense, optional);
}
} else {
s = new ECLSDString(value);
s.setCaseSense(casesense);
s.setOptional(optional);
}
s.setInvertMatch(invertmatch);
s.setHostid(hostid);
AddDescriptor(s);
} else if (tag.equals("cursor")) {
int row = parseIntAttr(elem, "row", 1);
int col = parseIntAttr(elem, "col", 1);
int pos = parseIntAttr(elem, "position", -1);
ECLSDCursor c;
if (pos > 0) {
c = new ECLSDCursor(pos, optional);
} else {
c = new ECLSDCursor(row, col, optional);
}
c.setInvertMatch(invertmatch);
c.setHostid(hostid);
AddDescriptor(c);
} else if (tag.equals("numfields") || tag.equals("fields")) {
int num = parseIntAttr(elem, elem.hasAttribute("number") ? "number" : "value", 0);
ECLSDFields f = new ECLSDFields(num, optional);
f.setInvertMatch(invertmatch);
f.setHostid(hostid);
AddDescriptor(f);
} else if (tag.equals("numinputfields") || tag.equals("inputfields")) {
int num = parseIntAttr(elem, elem.hasAttribute("number") ? "number" : "value", 0);
ECLSDInputFields inf = new ECLSDInputFields(num, optional);
inf.setInvertMatch(invertmatch);
inf.setHostid(hostid);
AddDescriptor(inf);
} else if (tag.equals("oia")) {
String status = elem.hasAttribute("status") ? elem.getAttribute("status") :
(elem.hasAttribute("input") ? elem.getAttribute("input") : "NOTINHIBITED");
ECLSDOIA oia = new ECLSDOIA(status, optional);
oia.setInvertMatch(invertmatch);
oia.setHostid(hostid);
AddDescriptor(oia);
} else if (tag.equals("checksum")) {
int value = parseIntAttr(elem, "value", 0);
ECLSDChecksum cs = new ECLSDChecksum(value, optional);
cs.setInvertMatch(invertmatch);
cs.setHostid(hostid);
AddDescriptor(cs);
} else if (tag.equals("attrib")) {
String valStr = elem.getAttribute("value");
char val = ECLSDAttrib.StringToAttrib(valStr);
int row = parseIntAttr(elem, "row", 1);
int col = parseIntAttr(elem, "col", 1);
int plane = ECLSDAttrib.StringToPlane(elem.getAttribute("plane"));
ECLSDAttrib attr = new ECLSDAttrib(val, row, col, plane, optional);
attr.setInvertMatch(invertmatch);
attr.setHostid(hostid);
AddDescriptor(attr);
} else if (tag.equals("block")) {
int row = parseIntAttr(elem, "row", 1);
int col = parseIntAttr(elem, "col", 1);
int erow = parseIntAttr(elem, "erow", -1);
int ecol = parseIntAttr(elem, "ecol", -1);
boolean casesense = "true".equalsIgnoreCase(elem.getAttribute("casesense"));
ECLSDBlock blk;
if (erow > 0 || ecol > 0) {
blk = new ECLSDBlock(row, col, erow, ecol, casesense, optional);
} else {
blk = new ECLSDBlock(row, col, casesense, optional);
}
blk.setInvertMatch(invertmatch);
blk.setHostid(hostid);
NodeList subList = elem.getElementsByTagName("string");
for (int j = 0; j < subList.getLength(); j++) {
Element strElem = (Element) subList.item(j);
blk.addString(strElem.getAttribute("value"));
}
AddDescriptor(blk);
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to parse ECLScreenDesc XML: " + e.getMessage(), e);
}
}
public void SetXML(String xml) { setXML(xml); }
private static int parseIntAttr(Element elem, String attrName, int defaultVal) {
if (!elem.hasAttribute(attrName)) return defaultVal;
try {
return Integer.parseInt(elem.getAttribute(attrName).trim());
} catch (NumberFormatException e) {
return defaultVal;
}
}
public synchronized int getConditionCount() {
return conditions.size();
@Override
public Object clone() {
try {
ECLScreenDesc copy = (ECLScreenDesc) super.clone();
copy.descriptors.clear();
for (ECLScreenDescriptor d : descriptors) {
copy.descriptors.add((ECLScreenDescriptor) d.clone());
}
return copy;
} catch (CloneNotSupportedException e) {
ECLScreenDesc copy = new ECLScreenDesc();
copy.SetName(this.name);
copy.SetActive(this.active);
for (ECLScreenDescriptor d : descriptors) {
copy.AddDescriptor((ECLScreenDescriptor) d.clone());
}
return copy;
}
}
@Override
public String toString() {
return getXML();
}
}
@@ -11,21 +11,28 @@ public class ECLScreenReco {
private final ECLSession session;
private final ECLPS ps;
private final ECLOIA oia;
private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>();
private final Map<ECLScreenDesc, ECLPSListener> listenerMap = new ConcurrentHashMap<>();
private final ECLPSListener internalListener;
private final ECLOIANotify internalOiaListener;
public ECLScreenReco() {
this((ECLPS) null);
}
public ECLScreenReco(ECLSession session) {
this(session != null ? session.GetPS() : null);
this(session != null ? session.GetPS() : null, session != null ? session.GetOIA() : null);
}
public ECLScreenReco(ECLPS ps) {
this(ps, (ECLOIA) null);
}
public ECLScreenReco(ECLPS ps, ECLOIA oia) {
this.ps = ps;
this.session = (ps != null) ? ps.GetParent() : null;
this.oia = (oia != null) ? oia : ((session != null) ? session.GetOIA() : null);
this.internalListener = new ECLPSListener() {
@Override
@@ -34,9 +41,14 @@ public class ECLScreenReco {
}
};
this.internalOiaListener = event -> checkAllScreens();
if (this.ps != null) {
this.ps.RegisterPSEvent(this.internalListener);
}
if (this.oia != null) {
this.oia.RegisterOIAEvent(this.internalOiaListener);
}
}
// ========== Static Recognition Helpers ==========
@@ -95,6 +107,16 @@ public class ECLScreenReco {
UnregisterScreen(desc, notify);
}
public void UnregisterScreen(ECLScreenDesc desc) {
if (desc != null) {
notifyMap.remove(desc);
listenerMap.remove(desc);
}
}
public void unregisterScreen(ECLScreenDesc desc) {
UnregisterScreen(desc);
}
public void RegisterScreen(ECLScreenDesc desc, ECLPSListener listener) {
if (desc != null && listener != null) {
listenerMap.put(desc, listener);
@@ -120,7 +142,10 @@ public class ECLScreenReco {
}
public boolean IsMatch(ECLScreenDesc desc) {
return IsMatch(ps, desc);
if (desc == null) return true;
if (ps == null) return false;
ECLOIA activeOia = (oia != null) ? oia : ((ps.GetParent() != null) ? ps.GetParent().GetOIA() : null);
return desc.Matches(ps, activeOia);
}
public boolean isMatch(ECLScreenDesc desc) {
return IsMatch(desc);
@@ -142,6 +167,9 @@ public class ECLScreenReco {
if (ps != null && internalListener != null) {
ps.UnregisterPSEvent(internalListener);
}
if (oia != null && internalOiaListener != null) {
oia.UnregisterOIAEvent(internalOiaListener);
}
notifyMap.clear();
listenerMap.clear();
}
@@ -41,6 +41,7 @@ public class ECLSession {
public static final String BLOCK_PASTE = "blockPaste";
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
public static final String PASTE_WORD_BREAK = "pasteWordBreak";
public static final String ENTRYASSIST_DOCMODE = "EntryAssist_DOCmode";
public static final String ENTRYASSIST_DOCWORDWRAP = "EntryAssist_DOCwordWrap";
public static final String ENTRYASSIST_STARTCOL = "EntryAssist_startCol";
@@ -456,11 +456,26 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
this.bytesTransferred = 0;
this.totalBytes = (config.isSend() && localFile.exists()) ? localFile.length() : 0;
// Auto-detect DBCS from translator codepage if not explicitly set
if (!config.isDbcs() && translator != null && translator.getCodePage() != null && translator.getCodePage().isDBCS()) {
config.setDbcs(true);
}
// Wait for keyboard unlock before typing command
waitForKeyboardUnlock(2000);
// If clear is requested, send Clear AID first to wipe intermediate screen status
if (config.isClear() && input != null && !input.isNvtMode()) {
input.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
try { Thread.sleep(150); } catch (InterruptedException ignored) {}
waitForKeyboardUnlock(2000);
}
String command = config.buildCommand();
log.info("ECLXfer: initiating transfer: " + command);
int capacity = input.kybdPrime();
if (capacity < command.length()) {
int capacity = input != null ? input.kybdPrime() : 0;
if (input != null && capacity < command.length()) {
String err = "Input field capacity insufficient for command (" + capacity + " chars)";
fireEvent(ECLXferEvent.XFER_ABORTED, FTConstants.ECL_ERR_XFER_ABORT, err);
return FTConstants.ECL_ERR_XFER_ABORT;
@@ -469,10 +484,25 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
setState(FTState.AWAIT_ACK);
fireEvent(ECLXferEvent.XFER_STARTED, 0, "Transfer initiated");
input.emulateInput(command + "\n");
if (input != null) {
input.emulateInput(command + "\n");
}
return 0;
}
private void waitForKeyboardUnlock(long timeoutMs) {
if (input == null) return;
long start = System.currentTimeMillis();
while (input.isKeyboardLocked() && (System.currentTimeMillis() - start) < timeoutMs) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
private void parseOptionsIntoConfig(FTConfig config, String options) {
if (options == null || options.trim().isEmpty()) return;
config.parseOptions(options);
@@ -0,0 +1,126 @@
package haus.nightmare.lib3270j.ecl.screenreco;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLPS;
/**
* Screen descriptor matching field or presentation plane attributes matching IBM Host On-Demand ECLSDAttrib.
*/
public class ECLSDAttrib extends ECLScreenDescriptor {
private static final long serialVersionUID = 1L;
public static final int TEXT_PLANE = 1;
public static final int COLOR_PLANE = 2;
public static final int DBCS_PLANE = 4;
public static final int GRID_PLANE = 8;
public static final int FIELD_PLANE = 16;
public static final int EXFIELD_PLANE = 32;
private char attrib = 0x00;
private int row = 1;
private int col = 1;
private int plane = FIELD_PLANE;
public ECLSDAttrib() {
super();
}
public ECLSDAttrib(char attrib, int row, int col, int plane) {
this(attrib, row, col, plane, false);
}
public ECLSDAttrib(char attrib, int row, int col, int plane, boolean optional) {
super(optional);
this.attrib = attrib;
this.row = row;
this.col = col;
this.plane = plane;
}
public char getAttrib() { return attrib; }
public char GetAttrib() { return attrib; }
public void setAttrib(char attrib) { this.attrib = attrib; }
public void SetAttrib(char attrib) { setAttrib(attrib); }
public int getRow() { return row; }
public int GetRow() { return row; }
public void setRow(int row) { this.row = row; }
public void SetRow(int row) { setRow(row); }
public int getCol() { return col; }
public int GetCol() { return col; }
public void setCol(int col) { this.col = col; }
public void SetCol(int col) { setCol(col); }
public int getPlane() { return plane; }
public int GetPlane() { return plane; }
public void setPlane(int plane) { this.plane = plane; }
public void SetPlane(int plane) { setPlane(plane); }
public static String PlaneToString(int plane) {
switch (plane) {
case TEXT_PLANE: return "TEXT_PLANE";
case COLOR_PLANE: return "COLOR_PLANE";
case DBCS_PLANE: return "DBCS_PLANE";
case GRID_PLANE: return "GRID_PLANE";
case FIELD_PLANE: return "FIELD_PLANE";
case EXFIELD_PLANE: return "EXFIELD_PLANE";
default: return "FIELD_PLANE";
}
}
public static int StringToPlane(String str) {
if (str == null) return FIELD_PLANE;
if (str.equalsIgnoreCase("TEXT_PLANE")) return TEXT_PLANE;
if (str.equalsIgnoreCase("COLOR_PLANE")) return COLOR_PLANE;
if (str.equalsIgnoreCase("DBCS_PLANE")) return DBCS_PLANE;
if (str.equalsIgnoreCase("GRID_PLANE")) return GRID_PLANE;
if (str.equalsIgnoreCase("FIELD_PLANE")) return FIELD_PLANE;
if (str.equalsIgnoreCase("EXFIELD_PLANE")) return EXFIELD_PLANE;
try {
return Integer.parseInt(str);
} catch (NumberFormatException e) {
return FIELD_PLANE;
}
}
public static String AttribToString(char c) {
return "0x" + Integer.toHexString(c & 0xFFFF);
}
public static char StringToAttrib(String str) {
if (str == null || str.isEmpty()) return 0;
str = str.trim();
int xIdx = str.toLowerCase().indexOf('x');
if (xIdx != -1) {
return (char) Integer.parseInt(str.substring(xIdx + 1), 16);
}
return (char) Integer.parseInt(str);
}
@Override
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
if (ps == null) return false;
int totalRows = ps.getRows();
int totalCols = ps.getCols();
if (row < 1 || row > totalRows || col < 1 || col > totalCols) {
return false;
}
int addr = (row - 1) * totalCols + (col - 1);
char[] buf = new char[1];
ps.getPlane(plane, buf, addr, 1);
return buf[0] == attrib;
}
@Override
public String format(int indent, boolean full) {
StringBuilder sb = new StringBuilder("<attrib ");
sb.append("value=\"").append(AttribToString(attrib)).append("\" ");
sb.append("row=\"").append(row).append("\" ");
sb.append("col=\"").append(col).append("\" ");
sb.append("plane=\"").append(PlaneToString(plane)).append("\" ");
return formatAttributes(sb.toString(), full);
}
}
@@ -0,0 +1,204 @@
package haus.nightmare.lib3270j.ecl.screenreco;
import java.util.ArrayList;
import java.util.List;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLPS;
/**
* Screen descriptor matching a rectangular block of text matching IBM Host On-Demand ECLSDBlock.
*/
public class ECLSDBlock extends ECLScreenDescriptor {
private static final long serialVersionUID = 1L;
public static final int BLOCK_ABSOLUTE = 1;
public static final int BLOCK_INRECT = 2;
private int blockType = BLOCK_INRECT;
private int srow = 1;
private int scol = 1;
private int erow = -1;
private int ecol = -1;
private boolean caseSense = false;
private ECLSDBlockString[] strings = new ECLSDBlockString[0];
public ECLSDBlock() {
super();
}
public ECLSDBlock(int srow, int scol, boolean caseSense, boolean optional) {
super(optional);
this.blockType = BLOCK_ABSOLUTE;
this.srow = srow;
this.scol = scol;
this.caseSense = caseSense;
}
public ECLSDBlock(int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
super(optional);
this.blockType = BLOCK_INRECT;
this.srow = srow;
this.scol = scol;
this.erow = erow;
this.ecol = ecol;
this.caseSense = caseSense;
}
public ECLSDBlock(String[] textLines, int srow, int scol, boolean caseSense) {
super(false);
this.blockType = BLOCK_ABSOLUTE;
this.srow = srow;
this.scol = scol;
this.caseSense = caseSense;
setStrings(textLines);
}
public int getBlockType() { return blockType; }
public int GetBlockType() { return blockType; }
public void setBlockType(int blockType) { this.blockType = blockType; }
public void SetBlockType(int blockType) { setBlockType(blockType); }
public int getSRow() { return srow; }
public int GetSRow() { return srow; }
public void setSRow(int srow) { this.srow = srow; }
public void SetSRow(int srow) { setSRow(srow); }
public int getSCol() { return scol; }
public int GetSCol() { return scol; }
public void setSCol(int scol) { this.scol = scol; }
public void SetSCol(int scol) { setSCol(scol); }
public int getERow() { return erow; }
public int GetERow() { return erow; }
public void setERow(int erow) { this.erow = erow; }
public void SetERow(int erow) { setERow(erow); }
public int getECol() { return ecol; }
public int GetECol() { return ecol; }
public void setECol(int ecol) { this.ecol = ecol; }
public void SetECol(int ecol) { setECol(ecol); }
public boolean isCaseSense() { return caseSense; }
public boolean IsCaseSense() { return caseSense; }
public void setCaseSense(boolean caseSense) { this.caseSense = caseSense; }
public void SetCaseSense(boolean caseSense) { setCaseSense(caseSense); }
public String[] getStrings() {
String[] arr = new String[strings.length];
for (int i = 0; i < strings.length; i++) {
arr[i] = (strings[i] != null) ? strings[i].getText() : "";
}
return arr;
}
public String[] GetStrings() { return getStrings(); }
public void setStrings(String[] textLines) {
if (textLines == null) {
this.strings = new ECLSDBlockString[0];
return;
}
this.strings = new ECLSDBlockString[textLines.length];
for (int i = 0; i < textLines.length; i++) {
this.strings[i] = new ECLSDBlockString(textLines[i]);
}
}
public void SetStrings(String[] textLines) { setStrings(textLines); }
public void setBlockStrings(ECLSDBlockString[] blockStrings) {
this.strings = (blockStrings != null) ? blockStrings : new ECLSDBlockString[0];
}
public void addString(String text) {
ECLSDBlockString[] newArr = new ECLSDBlockString[strings.length + 1];
System.arraycopy(strings, 0, newArr, 0, strings.length);
newArr[strings.length] = new ECLSDBlockString(text);
this.strings = newArr;
}
@Override
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
if (ps == null) return false;
if (strings == null || strings.length == 0) return true;
int totalRows = ps.getRows();
int totalCols = ps.getCols();
String[] lines = getStrings();
// 1. Absolute matching at (srow, scol)
if (blockType == BLOCK_ABSOLUTE || (erow <= 0 && ecol <= 0 && srow > 0 && scol > 0)) {
for (int i = 0; i < lines.length; i++) {
int r0 = srow + i - 1;
int c0 = scol - 1;
if (r0 < 0 || r0 >= totalRows || c0 < 0 || c0 >= totalCols) return false;
String target = lines[i];
String onScreen = ps.getString(r0, c0, target.length());
if (onScreen == null) return false;
boolean eq = caseSense ? target.equals(onScreen) : target.equalsIgnoreCase(onScreen);
if (!eq) return false;
}
return true;
}
// 2. Rectangular region matching
int sr = (srow > 0) ? srow : 1;
int sc = (scol > 0) ? scol : 1;
int er = (erow > 0) ? Math.min(erow, totalRows) : totalRows;
int ec = (ecol > 0) ? Math.min(ecol, totalCols) : totalCols;
if (sr > er || sc > ec) return false;
int numLines = lines.length;
if (er - sr + 1 < numLines) return false;
int rectWidth = ec - sc + 1;
List<String> screenLines = new ArrayList<>();
for (int r = sr; r <= er; r++) {
String s = ps.getString(r - 1, sc - 1, rectWidth);
screenLines.add(s != null ? s : "");
}
String firstTarget = caseSense ? lines[0] : lines[0].toLowerCase();
for (int i = 0; i <= screenLines.size() - numLines; i++) {
String rowStr = caseSense ? screenLines.get(i) : screenLines.get(i).toLowerCase();
int matchIdx = rowStr.indexOf(firstTarget);
while (matchIdx != -1) {
boolean allMatch = true;
for (int j = 1; j < numLines; j++) {
String nextRowStr = caseSense ? screenLines.get(i + j) : screenLines.get(i + j).toLowerCase();
String nextTarget = caseSense ? lines[j] : lines[j].toLowerCase();
if (matchIdx + nextTarget.length() > nextRowStr.length() ||
!nextRowStr.startsWith(nextTarget, matchIdx)) {
allMatch = false;
break;
}
}
if (allMatch) {
return true;
}
matchIdx = rowStr.indexOf(firstTarget, matchIdx + 1);
}
}
return false;
}
@Override
public String format(int indent, boolean full) {
StringBuilder sb = new StringBuilder("<block ");
if (srow > 0 || full) sb.append("row=\"").append(srow).append("\" ");
if (scol > 0 || full) sb.append("col=\"").append(scol).append("\" ");
if (blockType == BLOCK_INRECT && (erow > 0 || ecol > 0 || full)) {
if (erow > 0 || full) sb.append("erow=\"").append(erow).append("\" ");
if (ecol > 0 || full) sb.append("ecol=\"").append(ecol).append("\" ");
}
if (caseSense || full) sb.append("casesense=\"").append(caseSense).append("\" ");
if (optional || full) sb.append("optional=\"").append(optional).append("\" ");
if (invert || full) sb.append("invertmatch=\"").append(invert).append("\" ");
if (hostid != null && !hostid.isEmpty()) sb.append("hostid=\"").append(escapeChars(hostid)).append("\" ");
sb.append(">\n");
for (ECLSDBlockString s : strings) {
sb.append(" ").append(s.format(indent + 1, full)).append("\n");
}
sb.append("</block>");
return sb.toString();
}
}
@@ -0,0 +1,40 @@
package haus.nightmare.lib3270j.ecl.screenreco;
import java.io.Serializable;
/**
* Encapsulates an individual string line within an ECLSDBlock matching IBM Host On-Demand.
*/
public class ECLSDBlockString implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private String text = "";
public ECLSDBlockString() {}
public ECLSDBlockString(String text) {
this.text = (text != null) ? text : "";
}
public String getText() { return text; }
public String GetText() { return text; }
public String GetString() { return text; }
public String getString() { return text; }
public void setText(String text) { this.text = (text != null) ? text : ""; }
public void SetText(String text) { setText(text); }
public void SetString(String text) { setText(text); }
public void setString(String text) { setText(text); }
public String format(int indent, boolean full) {
return "<string value=\"" + ECLScreenDescriptor.escapeChars(text) + "\" />";
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,71 @@
package haus.nightmare.lib3270j.ecl.screenreco;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLPS;
/**
* Screen descriptor matching screen buffer checksum matching IBM Host On-Demand ECLSDChecksum.
*/
public class ECLSDChecksum extends ECLScreenDescriptor {
private static final long serialVersionUID = 1L;
private int value = 0;
public ECLSDChecksum() {
super();
}
public ECLSDChecksum(int value) {
this(value, false);
}
public ECLSDChecksum(int value, boolean optional) {
super(optional);
this.value = value;
}
public int getValue() { return value; }
public int GetValue() { return value; }
public void setValue(int value) { this.value = value; }
public void SetValue(int value) { setValue(value); }
public static int CalculateChecksum(ECLFieldList fieldList, int psSize) {
int sum = 0;
if (fieldList != null && fieldList.getFieldCount() > 0) {
int count = fieldList.getFieldCount();
sum = 17;
ECLField current = null;
for (int i = 0; i < count - 1; i++) {
if (i == 0) {
current = fieldList.getFirstField();
}
ECLField next = fieldList.getNextField(current);
if (current != null && next != null) {
int delta = next.getStart() - current.getStart() - 1;
sum = sum * 37 + delta;
}
current = next;
}
ECLField last = (current == null) ? fieldList.getFirstField() : current;
int endDelta = (last != null) ? (psSize - last.getStart()) : 0;
sum = sum * 37 + endDelta;
}
return sum;
}
@Override
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
if (ps == null) return false;
int calc = CalculateChecksum(ps.getFieldList(), ps.getSize());
return calc == value;
}
@Override
public String format(int indent, boolean full) {
StringBuilder sb = new StringBuilder("<checksum ");
sb.append("value=\"").append(value).append("\" ");
return formatAttributes(sb.toString(), full);
}
}
@@ -0,0 +1,77 @@
package haus.nightmare.lib3270j.ecl.screenreco;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLPS;
/**
* Screen descriptor matching cursor position matching IBM Host On-Demand ECLSDCursor.
*/
public class ECLSDCursor extends ECLScreenDescriptor {
private static final long serialVersionUID = 1L;
private int row = 1;
private int col = 1;
private int pos = -1;
public ECLSDCursor() {
super();
}
public ECLSDCursor(int pos) {
this(pos, false);
}
public ECLSDCursor(int pos, boolean optional) {
super(optional);
this.pos = pos;
}
public ECLSDCursor(int row, int col) {
this(row, col, false);
}
public ECLSDCursor(int row, int col, boolean optional) {
super(optional);
this.row = row;
this.col = col;
}
public int getPos() { return pos; }
public int GetPos() { return pos; }
public void setPos(int pos) { this.pos = pos; }
public void SetPos(int pos) { setPos(pos); }
public int getRow() { return row; }
public int GetRow() { return row; }
public void setRow(int row) { this.row = row; }
public void SetRow(int row) { setRow(row); }
public int getCol() { return col; }
public int GetCol() { return col; }
public void setCol(int col) { this.col = col; }
public void SetCol(int col) { setCol(col); }
@Override
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
if (ps == null) return false;
if (pos > 0) {
return (ps.getCursorPos() + 1) == pos;
}
// In ECLPS, getCursorRow() and getCursorCol() return 0-based indices.
int curRow = ps.getCursorRow() + 1;
int curCol = ps.getCursorCol() + 1;
return (curRow == row && curCol == col);
}
@Override
public String format(int indent, boolean full) {
StringBuilder sb = new StringBuilder("<cursor ");
if (pos > 0) {
sb.append("position=\"").append(pos).append("\" ");
} else {
sb.append("row=\"").append(row).append("\" ");
sb.append("col=\"").append(col).append("\" ");
}
return formatAttributes(sb.toString(), full);
}
}
@@ -0,0 +1,47 @@
package haus.nightmare.lib3270j.ecl.screenreco;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLPS;
/**
* Screen descriptor matching total number of fields matching IBM Host On-Demand ECLSDFields.
*/
public class ECLSDFields extends ECLScreenDescriptor {
private static final long serialVersionUID = 1L;
private int num = 0;
public ECLSDFields() {
super();
}
public ECLSDFields(int num) {
this(num, false);
}
public ECLSDFields(int num, boolean optional) {
super(optional);
this.num = num;
}
public int getNum() { return num; }
public int GetNum() { return num; }
public void setNum(int num) { this.num = num; }
public void SetNum(int num) { setNum(num); }
@Override
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
if (ps == null) return false;
ECLFieldList fl = ps.getFieldList();
if (fl == null) return false;
return fl.getFieldCount() == num;
}
@Override
public String format(int indent, boolean full) {
StringBuilder sb = new StringBuilder("<numfields ");
sb.append("number=\"").append(num).append("\" ");
return formatAttributes(sb.toString(), full);
}
}
@@ -0,0 +1,54 @@
package haus.nightmare.lib3270j.ecl.screenreco;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLPS;
/**
* Screen descriptor matching unprotected input field count matching IBM Host On-Demand ECLSDInputFields.
*/
public class ECLSDInputFields extends ECLScreenDescriptor {
private static final long serialVersionUID = 1L;
private int num = 0;
public ECLSDInputFields() {
super();
}
public ECLSDInputFields(int num) {
this(num, false);
}
public ECLSDInputFields(int num, boolean optional) {
super(optional);
this.num = num;
}
public int getNum() { return num; }
public int GetNum() { return num; }
public void setNum(int num) { this.num = num; }
public void SetNum(int num) { setNum(num); }
@Override
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
if (ps == null) return false;
ECLFieldList fl = ps.getFieldList();
if (fl == null) return false;
int inputCount = 0;
for (ECLField f : fl.getFields()) {
if (!f.isProtected()) {
inputCount++;
}
}
return inputCount == num;
}
@Override
public String format(int indent, boolean full) {
StringBuilder sb = new StringBuilder("<numinputfields ");
sb.append("number=\"").append(num).append("\" ");
return formatAttributes(sb.toString(), full);
}
}
@@ -0,0 +1,92 @@
package haus.nightmare.lib3270j.ecl.screenreco;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLPS;
/**
* Screen descriptor matching Operator Information Area (OIA) status matching IBM Host On-Demand ECLSDOIA.
*/
public class ECLSDOIA extends ECLScreenDescriptor {
private static final long serialVersionUID = 1L;
public static final int OIA_DONTCARE = 0;
public static final int OIA_NOTINHIBITED = 1;
private int oiaType = OIA_NOTINHIBITED;
private Integer explicitInhibitStatus = null;
public ECLSDOIA() {
super();
}
public ECLSDOIA(int type) {
this(type, false);
}
public ECLSDOIA(int type, boolean optional) {
super(optional);
this.oiaType = type;
}
public ECLSDOIA(String status, boolean optional) {
super(optional);
setOIAType(status);
}
public int getOIAType() { return oiaType; }
public int GetOIAType() { return oiaType; }
public void setOIAType(int type) { this.oiaType = type; this.explicitInhibitStatus = null; }
public void SetOIAType(int type) { setOIAType(type); }
public void setOIAType(String status) {
if (status == null) {
this.oiaType = OIA_NOTINHIBITED;
return;
}
if (status.equalsIgnoreCase("DONTCARE")) {
this.oiaType = OIA_DONTCARE;
} else if (status.equalsIgnoreCase("NOTINHIBITED") || status.equalsIgnoreCase("true")) {
this.oiaType = OIA_NOTINHIBITED;
} else if (status.equalsIgnoreCase("false")) {
// Inhibited: inverted NOTINHIBITED or explicit
this.oiaType = OIA_NOTINHIBITED;
this.invert = true;
} else {
try {
this.explicitInhibitStatus = Integer.parseInt(status);
} catch (NumberFormatException e) {
this.oiaType = OIA_NOTINHIBITED;
}
}
}
public void SetOIAType(String status) { setOIAType(status); }
public Integer getExplicitInhibitStatus() { return explicitInhibitStatus; }
public void setExplicitInhibitStatus(Integer status) { this.explicitInhibitStatus = status; }
@Override
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
if (oia == null) return true;
if (oiaType == OIA_DONTCARE) return true;
if (explicitInhibitStatus != null) {
return oia.getInputInhibited() == explicitInhibitStatus;
}
// OIA_NOTINHIBITED: input must not be inhibited
return oia.getInputInhibited() == ECLOIA.INHIBIT_NOTINHIBITED;
}
@Override
public String format(int indent, boolean full) {
StringBuilder sb = new StringBuilder("<oia ");
if (explicitInhibitStatus != null) {
sb.append("status=\"").append(explicitInhibitStatus).append("\" ");
} else if (oiaType == OIA_DONTCARE) {
sb.append("status=\"DONTCARE\" ");
} else {
sb.append("status=\"NOTINHIBITED\" input=\"true\" ");
}
return formatAttributes(sb.toString(), full);
}
}
@@ -0,0 +1,222 @@
package haus.nightmare.lib3270j.ecl.screenreco;
import haus.nightmare.lib3270j.ecl.ECLOIA;
import haus.nightmare.lib3270j.ecl.ECLPS;
/**
* Screen descriptor matching text on presentation space matching IBM Host On-Demand ECLSDString.
*/
public class ECLSDString extends ECLScreenDescriptor {
private static final long serialVersionUID = 1L;
public static final int STRING_ABSOLUTE = 1;
public static final int STRING_INRECT = 2;
private int stringType = STRING_INRECT;
private String text = "";
private int srow = 1;
private int scol = 1;
private int erow = -1;
private int ecol = -1;
private boolean caseSense = false;
private int pos = -1;
private boolean wrap = false;
public ECLSDString() {
super();
}
public ECLSDString(String text) {
super(false);
this.text = (text != null) ? text : "";
this.srow = 0;
this.scol = 0;
this.erow = 0;
this.ecol = 0;
this.stringType = STRING_INRECT;
}
public ECLSDString(String text, int pos, boolean caseSense) {
this(text, pos, caseSense, false);
}
public ECLSDString(String text, int pos, boolean caseSense, boolean optional) {
super(optional);
this.text = (text != null) ? text : "";
this.pos = pos;
this.caseSense = caseSense;
this.stringType = STRING_ABSOLUTE;
}
public ECLSDString(String text, int row, int col) {
this(text, row, col, false, false);
}
public ECLSDString(String text, int row, int col, boolean caseSense) {
this(text, row, col, caseSense, false);
}
public ECLSDString(String text, int row, int col, boolean caseSense, boolean optional) {
super(optional);
this.text = (text != null) ? text : "";
this.srow = row;
this.scol = col;
this.erow = -1;
this.ecol = -1;
this.caseSense = caseSense;
this.stringType = STRING_ABSOLUTE;
}
public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
this(text, srow, scol, erow, ecol, caseSense, optional, false);
}
public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional, boolean wrap) {
super(optional);
this.text = (text != null) ? text : "";
this.srow = srow;
this.scol = scol;
this.erow = erow;
this.ecol = ecol;
this.caseSense = caseSense;
this.wrap = wrap;
this.stringType = STRING_INRECT;
}
public String getText() { return text; }
public String GetText() { return text; }
public String GetString() { return text; }
public String getString() { return text; }
public void setText(String text) { this.text = (text != null) ? text : ""; }
public void SetText(String text) { setText(text); }
public void SetString(String text) { setText(text); }
public void setString(String text) { setText(text); }
public int getRow() { return srow; }
public int GetRow() { return srow; }
public int getSRow() { return srow; }
public int GetSRow() { return srow; }
public void setRow(int row) { this.srow = row; }
public void SetRow(int row) { setRow(row); }
public void setSRow(int row) { setRow(row); }
public void SetSRow(int row) { setRow(row); }
public int getCol() { return scol; }
public int GetCol() { return scol; }
public int getSCol() { return scol; }
public int GetSCol() { return scol; }
public void setCol(int col) { this.scol = col; }
public void SetCol(int col) { setCol(col); }
public void setSCol(int col) { setCol(col); }
public void SetSCol(int col) { setCol(col); }
public int getERow() { return erow; }
public int GetERow() { return erow; }
public void setERow(int erow) { this.erow = erow; }
public void SetERow(int erow) { setERow(erow); }
public int getECol() { return ecol; }
public int GetECol() { return ecol; }
public void setECol(int ecol) { this.ecol = ecol; }
public void SetECol(int ecol) { setECol(ecol); }
public boolean isCaseSense() { return caseSense; }
public boolean IsCaseSense() { return caseSense; }
public void setCaseSense(boolean caseSense) { this.caseSense = caseSense; }
public void SetCaseSense(boolean caseSense) { setCaseSense(caseSense); }
public boolean isWrap() { return wrap; }
public boolean IsWrap() { return wrap; }
public void setWrap(boolean wrap) { this.wrap = wrap; }
public void SetWrap(boolean wrap) { setWrap(wrap); }
public int getStringType() { return stringType; }
public int GetStringType() { return stringType; }
public void setStringType(int stringType) { this.stringType = stringType; }
public void SetStringType(int stringType) { setStringType(stringType); }
@Override
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
if (ps == null || text == null) return false;
if (text.isEmpty()) return true;
int totalRows = ps.getRows();
int totalCols = ps.getCols();
if (pos > 0) {
int p0 = pos - 1;
String onScreen = ps.getString(p0, text.length());
if (onScreen == null) return false;
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
}
// 1. Search anywhere on screen if row and col are not set (<= 0)
if (srow <= 0 && scol <= 0 && erow <= 0 && ecol <= 0) {
String fullScreen = ps.getString(0, totalRows * totalCols);
if (fullScreen == null) return false;
return caseSense ? fullScreen.contains(text) : fullScreen.toLowerCase().contains(text.toLowerCase());
}
// 2. Absolute match at (srow, scol)
if (stringType == STRING_ABSOLUTE || (erow <= 0 && ecol <= 0 && srow > 0 && scol > 0)) {
int r0 = srow - 1;
int c0 = scol - 1;
if (r0 < 0 || r0 >= totalRows || c0 < 0 || c0 >= totalCols) return false;
String onScreen = ps.getString(r0, c0, text.length());
if (onScreen == null) return false;
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
}
// 3. Rectangular region match (srow, scol, erow, ecol)
int sr = (srow > 0) ? srow : 1;
int sc = (scol > 0) ? scol : 1;
int er = (erow > 0) ? Math.min(erow, totalRows) : totalRows;
int ec = (ecol > 0) ? Math.min(ecol, totalCols) : totalCols;
if (sr > er || sc > ec) return false;
if (!wrap) {
int len = ec - sc + 1;
for (int r = sr; r <= er; r++) {
String line = ps.getString(r - 1, sc - 1, len);
if (line != null) {
boolean found = caseSense ? line.contains(text) : line.toLowerCase().contains(text.toLowerCase());
if (found) return true;
}
}
return false;
} else {
StringBuilder sb = new StringBuilder();
int len = ec - sc + 1;
for (int r = sr; r <= er; r++) {
String line = ps.getString(r - 1, sc - 1, len);
if (line != null) sb.append(line);
}
String block = sb.toString();
return caseSense ? block.contains(text) : block.toLowerCase().contains(text.toLowerCase());
}
}
@Override
public String format(int indent, boolean full) {
StringBuilder sb = new StringBuilder("<string ");
sb.append("value=\"").append(escapeChars(text)).append("\" ");
if (srow > 0 || full) {
sb.append("row=\"").append(srow).append("\" ");
}
if (scol > 0 || full) {
sb.append("col=\"").append(scol).append("\" ");
}
if (stringType == STRING_INRECT && (erow > 0 || ecol > 0 || full)) {
if (erow > 0 || full) sb.append("erow=\"").append(erow).append("\" ");
if (ecol > 0 || full) sb.append("ecol=\"").append(ecol).append("\" ");
}
if (caseSense || full) {
sb.append("casesense=\"").append(caseSense).append("\" ");
}
if (wrap || full) {
sb.append("wrap=\"").append(wrap).append("\" ");
}
return formatAttributes(sb.toString(), full);
}
}
@@ -0,0 +1,110 @@
package haus.nightmare.lib3270j.ecl.screenreco;
import java.io.Serializable;
import haus.nightmare.lib3270j.ecl.ECLPS;
import haus.nightmare.lib3270j.ecl.ECLOIA;
/**
* Abstract base class for composite screen recognition descriptors matching IBM Host On-Demand.
*/
public abstract class ECLScreenDescriptor implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
protected boolean optional = false;
protected boolean match = false;
protected boolean invert = false;
protected String hostid = "";
protected ECLScreenDescriptor() {}
protected ECLScreenDescriptor(boolean optional) {
this.optional = optional;
}
public boolean isOptional() { return optional; }
public boolean IsOptional() { return optional; }
public void setOptional(boolean optional) { this.optional = optional; }
public void SetOptional(boolean optional) { setOptional(optional); }
public boolean isMatch() { return match; }
public boolean IsMatch() { return match; }
public void setMatch(boolean match) { this.match = match; }
public void SetMatch(boolean match) { setMatch(match); }
public boolean isInvertMatch() { return invert; }
public boolean IsInvertMatch() { return invert; }
public void setInvertMatch(boolean invert) { this.invert = invert; }
public void SetInvertMatch(boolean invert) { setInvertMatch(invert); }
public String getHostid() { return hostid; }
public String GetHostid() { return hostid; }
public void setHostid(String hostid) { this.hostid = (hostid != null) ? hostid : ""; }
public void SetHostid(String hostid) { setHostid(hostid); }
/**
* Checks whether this descriptor matches against presentation space and/or OIA.
* Takes invertmatch into account.
*/
public boolean matches(ECLPS ps, ECLOIA oia) {
boolean m = checkMatch(ps, oia);
boolean res = invert ? !m : m;
setMatch(res);
return res;
}
public boolean Matches(ECLPS ps, ECLOIA oia) {
return matches(ps, oia);
}
/**
* Internal implementation of descriptor check without inversion.
*/
protected abstract boolean checkMatch(ECLPS ps, ECLOIA oia);
/**
* Format this descriptor as an XML string.
*/
public abstract String format(int indent, boolean full);
public String Format(int indent, boolean full) { return format(indent, full); }
protected String formatAttributes(String tagAndSpecificAttrs, boolean full) {
StringBuilder sb = new StringBuilder(tagAndSpecificAttrs);
if (optional || full) {
sb.append("optional=\"").append(optional).append("\" ");
}
if (invert || full) {
sb.append("invertmatch=\"").append(invert).append("\" ");
}
if (hostid != null && !hostid.isEmpty()) {
sb.append("hostid=\"").append(escapeChars(hostid)).append("\" ");
}
sb.append("/>");
return sb.toString();
}
public static String escapeChars(String string) {
if (string == null) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < string.length(); i++) {
char c = string.charAt(i);
switch (c) {
case '&': sb.append("&amp;"); break;
case '<': sb.append("&lt;"); break;
case '>': sb.append("&gt;"); break;
case '"': sb.append("&quot;"); break;
case '\'': sb.append("&apos;"); break;
default: sb.append(c); break;
}
}
return sb.toString();
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,156 @@
package haus.nightmare.lib3270j.ecl.util;
import haus.nightmare.lib3270j.ecl.ECLErr;
/**
* Parses SendKeys strings into keystroke and mnemonic tokens.
* Conforms 1:1 to IBM Host On-Demand (HoD) com.ibm.eNetwork.ECL.util.SendKeyTokenizer.
*/
public class SendKeyTokenizer {
private String sendString;
private int stringIndex;
private int stringLength;
protected String className = this.getClass().getName();
private boolean chunkPlainText = true;
public SendKeyTokenizer() {
this.setSendKeyString(null);
}
public SendKeyTokenizer(String string) {
this.setSendKeyString(string);
}
public SendKeyTokenizer(String string, boolean chunkPlainText) {
this.setSendKeyString(string);
this.chunkPlainText = chunkPlainText;
}
public void setSendKeyString(String string) {
this.stringLength = string != null ? string.length() : 0;
this.sendString = string;
this.stringIndex = 0;
}
public String remainder() {
if (this.stringIndex >= this.stringLength) {
return "";
}
return this.sendString.substring(this.stringIndex);
}
public boolean hasMoreTokens() {
return this.stringLength > this.stringIndex;
}
public boolean isChunkPlainText() {
return chunkPlainText;
}
public void setChunkPlainText(boolean chunkPlainText) {
this.chunkPlainText = chunkPlainText;
}
/**
* Retrieves the next keystroke or mnemonic token.
*
* Escaped brackets:
* "[[" returns "["
* "]] " returns "]"
*
* Unterminated or unmatched brackets throw ECLErr("ECL0012").
*/
public String nextToken() throws ECLErr {
if (this.stringIndex >= this.stringLength) {
return null;
}
char c = this.sendString.charAt(this.stringIndex);
switch (c) {
case '[': {
this.stringIndex++;
if (this.stringIndex >= this.stringLength) {
throw new ECLErr(this.className + ":1", "ECL0012", 1);
}
char next = this.sendString.charAt(this.stringIndex);
if (next == '[') {
this.stringIndex++;
return "[";
}
StringBuilder sb = new StringBuilder();
sb.append('[');
boolean foundClose = false;
while (this.stringIndex < this.stringLength) {
char ch = this.sendString.charAt(this.stringIndex);
if (ch == ']') {
sb.append(']');
this.stringIndex++;
foundClose = true;
break;
}
sb.append(ch);
this.stringIndex++;
}
if (!foundClose) {
throw new ECLErr(this.className + ":2", "ECL0012", 1);
}
String token = sb.toString();
// HoD numeric keypad single digit shortcut [0]..[9] -> "0".."9"
if (token.length() == 3 && token.charAt(0) == '[' && token.charAt(2) == ']') {
char digit = token.charAt(1);
if (digit >= '0' && digit <= '9') {
return String.valueOf(digit);
}
}
return token;
}
case ']': {
this.stringIndex++;
if (this.stringIndex >= this.stringLength) {
throw new ECLErr(this.className + ":3", "ECL0012", 1);
}
char next = this.sendString.charAt(this.stringIndex);
if (next == ']') {
this.stringIndex++;
return "]";
}
throw new ECLErr(this.className + ":4", "ECL0012", 1);
}
default: {
if (!this.chunkPlainText) {
this.stringIndex++;
return String.valueOf(c);
}
StringBuilder sb = new StringBuilder();
while (this.stringIndex < this.stringLength) {
char ch = this.sendString.charAt(this.stringIndex);
if (ch == '[' || ch == ']') {
break;
}
sb.append(ch);
this.stringIndex++;
}
return sb.toString();
}
}
}
/**
* Fetch the next character token without AID key translations.
*/
public String nextTokenNoAid() throws ECLErr {
if (this.stringIndex >= this.stringLength) {
throw new ECLErr(this.className + ":1", "ECL0012", 1);
}
char c = this.sendString.charAt(this.stringIndex);
this.stringIndex++;
return String.valueOf(c);
}
}
@@ -74,6 +74,11 @@ public class FTConfig {
private String codePage = null;
private boolean unicode = false;
private int unicodeType = 0; // 0 = UCS2, 1 = UTF8
private String programName = null;
private boolean dbcs = false;
private boolean optionNew = false;
private boolean clear = true;
private boolean optionNoso = false;
// ========== Derived convenience getters ==========
@@ -91,6 +96,37 @@ public class FTConfig {
}
public boolean isAppend() { return existAction == ExistAction.APPEND; }
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
public boolean isOptionNew() { return optionNew; }
public void setOptionNew(boolean optionNew) { this.optionNew = optionNew; }
public boolean isOptionNoso() { return optionNoso; }
public void setOptionNoso(boolean optionNoso) { this.optionNoso = optionNoso; }
public boolean isDbcs() { return dbcs; }
public void setDbcs(boolean dbcs) { this.dbcs = dbcs; }
public boolean isClear() { return clear; }
public void setClear(boolean clear) { this.clear = clear; }
public String getProgramName() {
if (programName != null && !programName.trim().isEmpty()) {
return programName.trim();
}
if (dbcs || isDbcsCodePage(codePage)) {
return "APVUFILE";
}
return "IND$FILE";
}
public void setProgramName(String programName) {
this.programName = programName;
}
private static boolean isDbcsCodePage(String cp) {
if (cp == null || cp.trim().isEmpty()) return false;
String clean = cp.trim().toUpperCase();
if (clean.startsWith("CP")) clean = clean.substring(2);
return clean.equals("930") || clean.equals("939") || clean.equals("935") ||
clean.equals("1388") || clean.equals("933") || clean.equals("937") ||
clean.equals("1371");
}
// ========== Standard getters and setters ==========
@@ -303,10 +339,22 @@ public class FTConfig {
if (upper.contains("APPEND")) {
this.existAction = ExistAction.APPEND;
} else if (upper.contains("NEW") && !upper.contains("NONEW")) {
this.optionNew = true;
} else if (upper.contains("REPLACE") || upper.contains("OVERWRITE")) {
this.existAction = ExistAction.REPLACE;
}
if (upper.contains("NOSO")) {
this.optionNoso = true;
}
if (upper.contains("APVUFILE")) {
this.programName = "APVUFILE";
} else if (upper.contains("IND$FILE")) {
this.programName = "IND$FILE";
}
if (upper.contains("CICS")) {
this.hostType = HostType.CICS;
} else if (upper.contains("CMS") || upper.contains("VM")) {
@@ -332,6 +380,10 @@ public class FTConfig {
String hostTrimmed = hostFilename.trim();
if (hostType == HostType.TSO && hostTrimmed.contains(" ") && validateCmsFilename(hostTrimmed) == null) {
hostType = HostType.CMS;
}
if (hostType == HostType.TSO) {
String tsoError = validateTsoDatasetName(hostTrimmed);
if (tsoError != null) return tsoError;
@@ -448,8 +500,8 @@ public class FTConfig {
public String buildCommand() {
StringBuilder cmd = new StringBuilder();
// IND$FILE GET/PUT hostfile
cmd.append("IND$FILE ");
// Host program selection: IND$FILE or APVUFILE
cmd.append(getProgramName()).append(" ");
cmd.append(isReceive() ? "GET " : "PUT ");
cmd.append(hostFilename != null ? hostFilename.trim() : "");
@@ -477,16 +529,23 @@ public class FTConfig {
opts.append("NOCRLF");
}
// Append (send only)
if (isAppend() && isSend()) {
if (isOptionNoso()) {
if (opts.length() > 0) opts.append(" ");
opts.append("APPEND");
opts.append("NOSO");
}
// Overwrite / Replace (if send or explicitly requested)
if (isOverwrite() && isSend()) {
if (opts.length() > 0) opts.append(" ");
opts.append("REPLACE");
// Append / New / Replace (send only)
if (isSend()) {
if (isAppend()) {
if (opts.length() > 0) opts.append(" ");
opts.append("APPEND");
} else if (isOptionNew()) {
if (opts.length() > 0) opts.append(" ");
opts.append("NEW");
} else if (isOverwrite()) {
if (opts.length() > 0) opts.append(" ");
opts.append("REPLACE");
}
}
// Host-specific send options
@@ -123,15 +123,69 @@ public final class FTConstants {
// Error codes
public static final int TR_ERR_EOF = 0x2200;
public static final int TR_ERR_CMDFAIL = 0x0100;
public static final int TR_ERR_OPEN_FAILED = 0x0100;
public static final int TR_ERR_SEQ_NOT_ALLOWED = 0x0200;
public static final int TR_ERR_NOT_AUTHORIZED = 0x3E00;
public static final int TR_ERR_SYNTAX_ERROR = 0x6000;
public static final int TR_ERR_GET_PAST_EOF = 0x2200;
// DFT buffer size limits
// DFT buffer size limits (Default matching IBM HoD bmtuSize = 2500)
public static final int DFT_MIN_BUF = 256;
public static final int DFT_MAX_BUF = 32768;
public static final int DFT_BUF = 4096;
public static final int DFT_BUF = 2500;
// AID for structured field response
public static final int AID_SF = 0x88;
// DDM Stream Names (ASCII and EBCDIC)
public static final String STREAM_NAME_FT_DATA = "FT:DATA";
public static final String STREAM_NAME_FT_MSG = "FT:MSG";
public static final byte[] STREAM_NAME_FT_DATA_BYTES_ASCII = {
0x46, 0x54, 0x3A, 0x44, 0x41, 0x54, 0x41 // 'F','T',':','D','A','T','A'
};
public static final byte[] STREAM_NAME_FT_DATA_BYTES_EBCDIC = {
(byte) 0xC6, (byte) 0xE3, 0x7A, (byte) 0xC4, (byte) 0xC1, (byte) 0xE3, (byte) 0xC1
};
public static final byte[] STREAM_NAME_FT_MSG_BYTES_ASCII = {
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20 // 'F','T',':','M','S','G',' '
};
public static final byte[] STREAM_NAME_FT_MSG_BYTES_EBCDIC = {
(byte) 0xC6, (byte) 0xE3, 0x7A, (byte) 0xD4, (byte) 0xE2, (byte) 0xC7, 0x40
};
// HoD Binary Packet Templates (Xfer3270.java)
public static final byte[] RB_OPEN_ACK = {
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x00, 0x09, (byte) 0xFF, (byte) 0xEF
};
public static final byte[] RB_CLOSE_ACK = {
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x41, 0x09, (byte) 0xFF, (byte) 0xEF
};
public static final byte[] RB_OPEN_FAILED = {
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x01, 0x00, (byte) 0xFF, (byte) 0xEF
};
public static final byte[] RB_GET_PAST_EOF = {
(byte) 0x88, 0x00, 0x09, (byte) 0xD0, 0x46, 0x08, 0x69, 0x04, 0x22, 0x00, (byte) 0xFF, (byte) 0xEF
};
public static final byte[] RB_UPLOAD_DATA_HEADER = {
0x11, 0x11, (byte) 0xD0, 0x46, 0x05, 0x63, 0x06, 0x00, 0x00, 0x00, 0x00,
(byte) 0xC0, (byte) 0x80, 0x61, 0x00, 0x00
};
/**
* Builds exact 14-byte Data Acknowledgement matching HoD Xfer3270:
* [0x88, 0x00, 0x0B, 0xD0, 0x47, 0x05, 0x63, 0x06, rec_b0, rec_b1, rec_b2, rec_b3, 0xFF, 0xEF]
*/
public static byte[] createDataAck(long recnum) {
return new byte[] {
(byte) 0x88, 0x00, 0x0B, (byte) 0xD0, 0x47, 0x05, 0x63, 0x06,
(byte) ((recnum >> 24) & 0xFF),
(byte) ((recnum >> 16) & 0xFF),
(byte) ((recnum >> 8) & 0xFF),
(byte) (recnum & 0xFF),
(byte) 0xFF, (byte) 0xEF
};
}
// ========== Transfer State ==========
public enum FTState {
@@ -215,11 +215,13 @@ public class FTCut {
// CUT frames MUST have a skip field attribute at O_SF (1919)
byte sfAttr = screen.getCellFAByte(O_SF);
if (sfAttr == 0 || !isSkip(sfAttr)) {
// Not a CUT frame (likely local echo, menu return, or intermediate screen)
// Not a CUT frame (likely local echo, menu return, or host error screen)
if (xferInProgress) {
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
xferInProgress = false;
listener.onTransferAborted("Host aborted transfer (returned to menu)");
String hostErr = scanScreenForErrors();
String abortMsg = hostErr != null ? hostErr : "Host aborted transfer (returned to menu)";
listener.onTransferAborted(abortMsg);
}
return;
}
@@ -245,6 +247,38 @@ public class FTCut {
}
}
/**
* Scans rows 1-24 of the 3270 screen buffer for host error indicators.
* Checks for TSO (IKJ*), CMS (DMS*), CICS (DFH*), and generic transfer abort messages.
*/
public String scanScreenForErrors() {
if (screen == null) return null;
int rows = screen.getRows();
int cols = screen.getCols();
String[] errorPrefixes = {
"IKJ", "DMS", "DFH", "TRANS", "NOT FOUND", "ERROR", "REJECTED", "INVALID", "COMMAND NOT FOUND", "FILE NOT FOUND"
};
for (int r = 0; r < rows; r++) {
StringBuilder lineSb = new StringBuilder();
for (int c = 0; c < cols; c++) {
int pos = r * cols + c;
int ec = screen.getCellEC(pos);
if (ec != 0) {
lineSb.append(translator.ebcdicToUnicode(ec));
}
}
String line = lineSb.toString().trim();
if (line.isEmpty()) continue;
String lineUpper = line.toUpperCase();
for (String prefix : errorPrefixes) {
if (lineUpper.contains(prefix)) {
return line;
}
}
}
return null;
}
private void checkForHostPrompts() {
if (screen == null) return;
int size = screen.getRows() * screen.getCols();
@@ -299,6 +333,10 @@ public class FTCut {
cutAck();
String msg = extractHostMessage();
if ("Host cancelled transfer".equals(msg)) {
String hostErr = scanScreenForErrors();
if (hostErr != null) msg = hostErr;
}
listener.onTransferAborted(msg);
break;
@@ -388,7 +426,7 @@ public class FTCut {
log.fine("CUT: > DATA seq=" + seq + " len=" + count);
expandedLength += count;
listener.onBytesTransferred(expandedLength);
input.sendAidForFT(AID_ENTER);
paceAndSendAid(AID_ENTER);
}
// ========== Data (Download) ==========
@@ -467,11 +505,11 @@ public class FTCut {
screen.setCellFA(O_DR_SF, attr);
log.fine("CUT: Retransmitting last upload frame (len=" + lastUploadCount + ")");
input.sendAidForFT(AID_ENTER);
paceAndSendAid(AID_ENTER);
} else {
// In download mode, send ACK_RETRANSMIT (PF1)
log.fine("CUT: Requesting frame retransmit from host via PF1");
input.sendAidForFT(ACK_RETRANSMIT);
paceAndSendAid(ACK_RETRANSMIT);
}
}
@@ -479,7 +517,7 @@ public class FTCut {
private void cutAck() {
log.fine("CUT: > ACK (Enter)");
input.sendAidForFT(AID_ENTER);
paceAndSendAid(AID_ENTER);
}
// ========== Abort ==========
@@ -492,10 +530,24 @@ public class FTCut {
screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF);
screen.setCell(RO_REASON_CODE + 1, reason & 0xFF);
input.sendAidForFT(AID_PF2);
paceAndSendAid(AID_PF2);
listener.onTransferAborted(message);
}
private void paceAndSendAid(int aid) {
if (input == null) return;
long start = System.currentTimeMillis();
while (input.isKeyboardLocked() && System.currentTimeMillis() - start < 1000) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
input.sendAidForFT((byte) aid);
}
// ========== Character Translation ==========
/**
@@ -49,6 +49,14 @@ public class FTDft {
private int customMtuSize = 0;
private int pendingByte = -1;
// Record formatting buffers and SO/SI tracking
private final ByteArrayOutputStream downloadRecordBuffer = new ByteArrayOutputStream();
private boolean dbcsMode = false;
private boolean hasPendingByteInDBCS = false;
private byte pendingByteDBCS = 0;
private byte[] pendingUploadRecord = null;
private int pendingUploadRecordOffset = 0;
// Savebuf for Read Modified retransmit
private byte[] dftSaveBuf = null;
private int dftSaveBufLen = 0;
@@ -159,6 +167,12 @@ public class FTDft {
dftSaveBufLen = 0;
lastCr = false;
pendingByte = -1;
downloadRecordBuffer.reset();
dbcsMode = false;
hasPendingByteInDBCS = false;
pendingByteDBCS = 0;
pendingUploadRecord = null;
pendingUploadRecordOffset = 0;
}
/**
@@ -201,6 +215,12 @@ public class FTDft {
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
// Check if this is a host error reply (e.g. TR_ERROR_REPLY 0x08, or contains TR_ERROR_HDR 0x6904)
if ((requestCode & 0xFF) == TR_ERROR_REPLY || isErrorField(data, payloadStart, offset + length - payloadStart)) {
handleHostErrorReply(data, payloadStart, offset + length - payloadStart);
return;
}
switch (requestCode) {
case TR_OPEN_REQ:
dftOpenRequest(data, offset, length);
@@ -228,8 +248,74 @@ public class FTDft {
}
}
private boolean isErrorField(byte[] data, int start, int length) {
int end = Math.min(start + length, data.length);
for (int i = start; i + 3 < end; i++) {
int hdr = ((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF);
if (hdr == TR_ERROR_HDR) return true;
}
return false;
}
private void handleHostErrorReply(byte[] data, int payloadStart, int length) {
int end = Math.min(payloadStart + length, data.length);
int errCode = 0;
for (int i = payloadStart; i + 3 < end; i++) {
int hdr = ((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF);
if (hdr == TR_ERROR_HDR) {
errCode = ((data[i + 2] & 0xFF) << 8) | (data[i + 3] & 0xFF);
break;
}
}
String message;
switch (errCode) {
case TR_ERR_OPEN_FAILED:
message = "Host file open failed (file not found, cannot open, or dataset does not exist)";
break;
case TR_ERR_SEQ_NOT_ALLOWED:
message = "Host transfer sequence not allowed (state violation)";
break;
case TR_ERR_GET_PAST_EOF:
if (listener != null && listener.getCurrentState() == FTState.RUNNING) {
log.info("DFT: Host reported end of file (0x2200)");
listener.onTransferComplete(null);
return;
}
message = "Host reached end of file";
break;
case TR_ERR_NOT_AUTHORIZED:
message = "Host file access not authorized (security violation)";
break;
case TR_ERR_SYNTAX_ERROR:
message = "Host command syntax error or invalid transfer options";
break;
default:
message = "Host transfer error (code 0x" + Integer.toHexString(errCode) + ")";
break;
}
log.warning("DFT host error reply: " + message + " (errCode=0x" + Integer.toHexString(errCode) + ")");
dftAbort(message, TR_DATA_INSERT);
}
// ========== Open Request ==========
private static boolean containsBytes(byte[] data, int start, int end, byte[] pattern) {
if (data == null || pattern == null || pattern.length == 0) return false;
int max = Math.min(end, data.length) - pattern.length;
for (int i = Math.max(0, start); i <= max; i++) {
boolean match = true;
for (int j = 0; j < pattern.length; j++) {
if (data[i + j] != pattern[j]) {
match = false;
break;
}
}
if (match) return true;
}
return false;
}
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
log.fine("DFT: Open request");
@@ -247,7 +333,10 @@ public class FTDft {
// Check for host DDM file attributes embedded in open request
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
if (isMessageStream(nameBuf)) {
int sfEnd = Math.min(sfOffset + sfLength, data.length);
if (containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_ASCII) ||
containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_EBCDIC) ||
isMessageStream(nameBuf)) {
messageFlag = true;
log.info("DFT: Open request for message stream (" + nameBuf + ")");
} else {
@@ -258,7 +347,7 @@ public class FTDft {
dftEof = false;
recnum = 1;
// Acknowledge Open matching x3270 (SF_TRANSFER_DATA + 0x0009)
// Acknowledge Open matching IBM Host On-Demand
dftOpenAck();
}
@@ -322,12 +411,7 @@ public class FTDft {
}
private void dftOpenAck() {
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
out.write(0); out.write(5); // SF length
out.write(SF_TRANSFER_DATA);
out.write(0); out.write(9); // OpenAck response code 0x0009
input.sendStructuredFieldData(out.toByteArray());
input.sendStructuredFieldData(RB_OPEN_ACK.clone());
}
// ========== Insert Request (host sending data for download) ==========
@@ -441,31 +525,125 @@ public class FTDft {
}
/**
* Write download data to the local file, handling ASCII conversion.
* Matching x3270 upload_convert logic.
* Write download data to the local file, handling ASCII conversion, RECFM F/V formatting, and EOF stripping.
* Conforms to IBM Host On-Demand specification.
*/
private void writeDownloadData(byte[] data, int offset, int length,
FTConfig config) throws IOException {
if (outputStream == null) return;
if (outputStream == null || length <= 0) return;
if (config != null && config.isUnicode()) {
outputStream.write(data, offset, length);
return;
}
if (!config.isAscii()) {
if (config != null && !config.isAscii()) {
outputStream.write(data, offset, length);
return;
}
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
"V".equalsIgnoreCase(hostRecfm);
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
"F".equalsIgnoreCase(hostRecfm);
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
if (isRecfmV) {
downloadRecordBuffer.write(data, offset, length);
processVariableDownloadRecords(config);
} else if (isRecfmF && lrecl > 0) {
downloadRecordBuffer.write(data, offset, length);
processFixedDownloadRecords(lrecl, config);
} else {
writeConvertedChunk(data, offset, length, config);
}
}
private void processVariableDownloadRecords(FTConfig config) throws IOException {
while (downloadRecordBuffer.size() >= 4) {
byte[] buf = downloadRecordBuffer.toByteArray();
int rdwLen = ((buf[0] & 0xFF) << 8) | (buf[1] & 0xFF);
if (rdwLen < 4) {
downloadRecordBuffer.reset();
if (buf.length > 1) {
downloadRecordBuffer.write(buf, 1, buf.length - 1);
}
continue;
}
if (buf.length < rdwLen) {
break;
}
int recDataLen = rdwLen - 4;
writeConvertedChunk(buf, 4, recDataLen, config);
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
downloadRecordBuffer.reset();
if (buf.length > rdwLen) {
downloadRecordBuffer.write(buf, rdwLen, buf.length - rdwLen);
}
}
}
private void processFixedDownloadRecords(int lrecl, FTConfig config) throws IOException {
while (downloadRecordBuffer.size() >= lrecl) {
byte[] buf = downloadRecordBuffer.toByteArray();
int end = lrecl;
while (end > 0) {
int b = buf[end - 1] & 0xFF;
if (b == 0x20 || b == 0x40 || (config != null && config.isRemapFlag() && b == FT2ASC[0x40])) {
end--;
} else {
break;
}
}
writeConvertedChunk(buf, 0, end, config);
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
downloadRecordBuffer.reset();
if (buf.length > lrecl) {
downloadRecordBuffer.write(buf, lrecl, buf.length - lrecl);
}
}
}
private void flushRemainingDownloadRecords() throws IOException {
if (outputStream == null || downloadRecordBuffer.size() == 0) return;
FTConfig config = listener != null ? listener.getConfig() : null;
byte[] remaining = downloadRecordBuffer.toByteArray();
downloadRecordBuffer.reset();
if (config != null && config.isAscii()) {
int end = remaining.length;
while (end > 0) {
int b = remaining[end - 1] & 0xFF;
if (b == 0x20 || b == 0x40 || (config.isRemapFlag() && b == FT2ASC[0x40])) {
end--;
} else {
break;
}
}
if (end > 0) {
writeConvertedChunk(remaining, 0, end, config);
}
} else {
outputStream.write(remaining);
}
outputStream.flush();
}
private void writeConvertedChunk(byte[] data, int offset, int length, FTConfig config) throws IOException {
if (outputStream == null || length <= 0) return;
for (int i = 0; i < length; i++) {
int b = data[offset + i] & 0xFF;
if (config.isCrFlag() && (b == '\r' || b == EOF_CTRL_Z || b == EOF_CTRL_D)) {
continue; // Strip CR and EOF (^Z / ^D)
if (b == EOF_CTRL_Z || b == EOF_CTRL_D) {
continue; // Strip EOF (^Z / ^D)
}
if (!config.isRemapFlag()) {
if (config != null && config.isCrFlag() && b == '\r') {
continue; // Strip CR
}
if (config == null || !config.isRemapFlag()) {
outputStream.write(b);
continue;
}
@@ -506,30 +684,42 @@ public class FTDft {
byte[] readBuf = new byte[numbytes];
int totalRead = 0;
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
"F".equalsIgnoreCase(hostRecfm);
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
"V".equalsIgnoreCase(hostRecfm);
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
try {
while (!dftEof && totalRead < numbytes) {
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
if (config != null && config.isAscii() && isRecfmF && lrecl > 0) {
totalRead = readFixedRecordUpload(readBuf, numbytes, lrecl, config);
} else if (config != null && config.isAscii() && isRecfmV) {
totalRead = readVariableRecordUpload(readBuf, numbytes, config);
} else {
while (!dftEof && totalRead < numbytes) {
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
}
totalRead += n;
} else if (config != null && config.isAscii()) {
int b = dftAsciiRead(config);
if (b == -1) {
dftEof = true;
break;
}
readBuf[totalRead++] = (byte) b;
} else {
if (inputStream == null) { dftEof = true; break; }
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
}
totalRead += n;
}
totalRead += n;
} else if (config.isAscii()) {
int b = dftAsciiRead(config);
if (b == -1) {
dftEof = true;
break;
}
readBuf[totalRead++] = (byte) b;
} else {
if (inputStream == null) { dftEof = true; break; }
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
}
totalRead += n;
}
}
} catch (IOException e) {
@@ -537,17 +727,17 @@ public class FTDft {
return;
}
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
out.write(AID_SF);
int sfLenPos = out.size();
out.write(0); out.write(0);
out.write(SF_TRANSFER_DATA);
if (totalRead > 0) {
log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes");
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
out.write(AID_SF);
int sfLenPos = out.size();
out.write(0); out.write(0);
out.write(SF_TRANSFER_DATA);
out.write((TR_GET_REPLY >> 8) & 0xFF);
out.write(TR_GET_REPLY & 0xFF);
@@ -570,31 +760,167 @@ public class FTDft {
out.write(readBuf, 0, totalRead);
bytesTransferred += totalRead;
byte[] result = out.toByteArray();
int sfLen = result.length - 1;
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
dftSaveBuf = result.clone();
dftSaveBufLen = result.length;
input.sendStructuredFieldData(result);
listener.onBytesTransferred(bytesTransferred);
} else {
log.fine("DFT: > GetReply EOF");
out.write((TR_GET_REQ >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
out.write((TR_ERR_EOF >> 8) & 0xFF);
out.write(TR_ERR_EOF & 0xFF);
dftEof = true;
dftSaveBuf = RB_GET_PAST_EOF.clone();
dftSaveBufLen = RB_GET_PAST_EOF.length;
input.sendStructuredFieldData(RB_GET_PAST_EOF.clone());
listener.onBytesTransferred(bytesTransferred);
}
}
private byte[] readNextUploadLineConverted(FTConfig config) throws IOException {
if (inputStream == null || dftEof) return null;
ByteArrayOutputStream lineBytes = new ByteArrayOutputStream();
int c;
boolean hasData = false;
while ((c = inputStream.read()) != -1) {
hasData = true;
if (c == '\r') {
continue; // ignore CR
}
if (c == '\n') {
break;
}
if (c == EOF_CTRL_Z || c == EOF_CTRL_D) {
break;
}
lineBytes.write(c);
}
if (!hasData && c == -1) {
return null;
}
byte[] result = out.toByteArray();
int sfLen = result.length - 1;
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
byte[] raw = lineBytes.toByteArray();
return convertLineForUpload(raw, config);
}
dftSaveBuf = result.clone();
dftSaveBufLen = result.length;
private byte[] convertLineForUpload(byte[] raw, FTConfig config) {
String lineStr = new String(raw, StandardCharsets.UTF_8);
ByteArrayOutputStream converted = new ByteArrayOutputStream(raw.length + 8);
boolean isDbcs = (config != null && config.isDbcs()) || (translator != null && translator.getCodePage() != null && translator.getCodePage().isDBCS());
boolean noSo = config != null && config.isOptionNoso();
boolean inDbcsState = false;
input.sendStructuredFieldData(result);
listener.onBytesTransferred(bytesTransferred);
for (int i = 0; i < lineStr.length(); i++) {
char ch = lineStr.charAt(i);
int ebc = translator != null ? translator.unicodeToEbcdic(ch) : -1;
boolean charIsDbcs = isDbcs && (ch > 0xFF || ebc > 0xFF);
if (charIsDbcs) {
if (!inDbcsState && !noSo) {
converted.write(0x0E); // Shift-Out
inDbcsState = true;
}
converted.write((ebc >> 8) & 0xFF);
converted.write(ebc & 0xFF);
} else {
if (inDbcsState && !noSo) {
converted.write(0x0F); // Shift-In
inDbcsState = false;
}
converted.write(convertAsciiByte(ch, config));
}
}
if (inDbcsState && !noSo) {
converted.write(0x0F); // Shift-In at end of record
}
return converted.toByteArray();
}
private int readFixedRecordUpload(byte[] outBuf, int maxBytes, int lrecl, FTConfig config) throws IOException {
int totalRead = 0;
int padByte = (config != null && config.isRemapFlag()) ? FT2ASC[0x40] : 0x20;
while (totalRead + lrecl <= maxBytes && !dftEof) {
byte[] rec;
if (pendingUploadRecord != null) {
rec = pendingUploadRecord;
pendingUploadRecord = null;
} else {
rec = readNextUploadLineConverted(config);
if (rec == null) {
dftEof = true;
break;
}
}
if (rec.length <= lrecl) {
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
totalRead += rec.length;
int padCount = lrecl - rec.length;
for (int p = 0; p < padCount; p++) {
outBuf[totalRead++] = (byte) padByte;
}
} else {
// Longer than lrecl: split
System.arraycopy(rec, 0, outBuf, totalRead, lrecl);
totalRead += lrecl;
int rem = rec.length - lrecl;
pendingUploadRecord = new byte[rem];
System.arraycopy(rec, lrecl, pendingUploadRecord, 0, rem);
}
}
return totalRead;
}
private int readVariableRecordUpload(byte[] outBuf, int maxBytes, FTConfig config) throws IOException {
int totalRead = 0;
while (totalRead + 4 < maxBytes && !dftEof) {
byte[] rec;
if (pendingUploadRecord != null) {
rec = pendingUploadRecord;
pendingUploadRecord = null;
} else {
rec = readNextUploadLineConverted(config);
if (rec == null) {
dftEof = true;
break;
}
}
int needed = rec.length + 4;
if (totalRead + needed <= maxBytes) {
outBuf[totalRead++] = (byte) ((needed >> 8) & 0xFF);
outBuf[totalRead++] = (byte) (needed & 0xFF);
outBuf[totalRead++] = 0;
outBuf[totalRead++] = 0;
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
totalRead += rec.length;
} else {
if (totalRead > 0) {
// Save for next MTU buffer
pendingUploadRecord = rec;
break;
} else {
// Single record exceeds entire MTU: split
int chunk = maxBytes - 4;
int rdwLen = chunk + 4;
outBuf[totalRead++] = (byte) ((rdwLen >> 8) & 0xFF);
outBuf[totalRead++] = (byte) (rdwLen & 0xFF);
outBuf[totalRead++] = 0;
outBuf[totalRead++] = 0;
System.arraycopy(rec, 0, outBuf, totalRead, chunk);
totalRead += chunk;
int rem = rec.length - chunk;
pendingUploadRecord = new byte[rem];
System.arraycopy(rec, chunk, pendingUploadRecord, 0, rem);
break;
}
}
}
return totalRead;
}
/**
@@ -717,14 +1043,13 @@ public class FTDft {
private void dftCloseRequest() {
log.fine("DFT: Close");
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
out.write(0); out.write(5);
out.write(SF_TRANSFER_DATA);
out.write((TR_CLOSE_REPLY >> 8) & 0xFF);
out.write(TR_CLOSE_REPLY & 0xFF);
try {
flushRemainingDownloadRecords();
} catch (IOException e) {
log.warning("DFT: Error flushing download buffer on close: " + e.getMessage());
}
input.sendStructuredFieldData(out.toByteArray());
input.sendStructuredFieldData(RB_CLOSE_ACK.clone());
if (listener.getCurrentState() != FTState.NONE) {
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
@@ -735,21 +1060,7 @@ public class FTDft {
// ========== Data Acknowledgement ==========
private void dftDataAck() {
ByteArrayOutputStream out = new ByteArrayOutputStream(12);
out.write(AID_SF);
out.write(0); out.write(11); // SF length = 11 (0x000B)
out.write(SF_TRANSFER_DATA);
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
out.write(TR_NORMAL_REPLY & 0xFF);
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
out.write(TR_RECNUM_HDR & 0xFF);
out.write((int) ((recnum >> 24) & 0xFF));
out.write((int) ((recnum >> 16) & 0xFF));
out.write((int) ((recnum >> 8) & 0xFF));
out.write((int) (recnum & 0xFF));
recnum++;
input.sendStructuredFieldData(out.toByteArray());
input.sendStructuredFieldData(createDataAck(recnum++));
}
// ========== Abort ==========
@@ -757,18 +1068,22 @@ public class FTDft {
private void dftAbort(String message, int code) {
log.warning("DFT: ABORT — " + message);
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
out.write(AID_SF);
out.write(0); out.write(9);
out.write(SF_TRANSFER_DATA);
out.write((code >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
out.write(TR_ERR_CMDFAIL & 0xFF);
if (code == TR_OPEN_REQ) {
input.sendStructuredFieldData(RB_OPEN_FAILED.clone());
} else {
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
out.write(AID_SF);
out.write(0); out.write(9);
out.write(SF_TRANSFER_DATA);
out.write((code >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
out.write(TR_ERR_CMDFAIL & 0xFF);
input.sendStructuredFieldData(out.toByteArray());
input.sendStructuredFieldData(out.toByteArray());
}
listener.onTransferAborted(message);
}
@@ -10,10 +10,14 @@ import java.util.List;
* <p>A GOCA fillet curve passes from the initial point P_0 to the terminal point P_{N-1},
* bending tangentially toward each intermediate control point P_i.
*/
public class FilletPts {
public class FilletPts extends HODEllipse {
public static final int DEFAULT_STEPS_PER_SEGMENT = 24;
public FilletPts() {
super(0.0, 0.0, 0.0, 0.0);
}
/**
* Calculates interpolated spline vertices across control points using double precision.
*
@@ -116,17 +120,115 @@ public class FilletPts {
}
/**
* Interpolates fillet points into provided output buffers.
* Interpolates fillet points into provided output buffers using HoD's exact rational quadratic
* polynomial spline algorithm.
* Matches IBM Host On-Demand FilletPts.getFilletPoints.
*/
public int getFilletPoints(int[] xIn, int[] yIn, int n, int[] xOut, int[] yOut) {
if (xIn == null || yIn == null || n <= 0 || xOut == null || yOut == null) {
public int getFilletPoints(int[] nArray, int[] nArray2, int n, int[] nArray3, int[] nArray4) {
if (nArray == null || nArray2 == null || n <= 0 || nArray3 == null || nArray4 == null) {
return 0;
}
int[][] pts = calculate(xIn, yIn, n, 16);
int count = Math.min(pts[0].length, Math.min(xOut.length, yOut.length));
System.arraycopy(pts[0], 0, xOut, 0, count);
System.arraycopy(pts[1], 0, yOut, 0, count);
return count;
int n2;
int n3 = 0;
int n4 = 0;
boolean bl = false;
int n5 = 0;
for (n2 = 1; n2 < n; ++n2) {
if (nArray[n2] != nArray[n5] || nArray2[n2] != nArray2[n5]) continue;
bl = true;
}
if (bl) {
int[] nArray5 = new int[n];
int[] nArray6 = new int[n];
nArray5[0] = nArray[0];
nArray6[0] = nArray2[0];
n5 = 0;
for (n2 = 1; n2 < n; ++n2) {
if (nArray[n2] == nArray[n5] && nArray2[n2] == nArray2[n5]) continue;
nArray5[++n5] = nArray[n2];
nArray6[n5] = nArray2[n2];
}
nArray = nArray5;
nArray2 = nArray6;
n = n5 + 1;
}
nArray3[n3] = nArray[0];
nArray4[n3] = nArray2[0];
int n6 = nArray[0];
int n9 = nArray2[0];
n3++;
if (n == 1) {
nArray3[n3] = nArray3[0];
nArray4[n3++] = nArray4[0];
return n3;
}
if (n == 2) {
nArray3[n3] = nArray[1];
nArray4[n3++] = nArray2[1];
return n3;
}
int[] nArray7 = new int[3];
int[] nArray8 = new int[3];
nArray7[0] = nArray[n4];
nArray8[0] = nArray2[n4++];
--n;
nArray7[1] = nArray[n4];
nArray8[1] = nArray2[n4++];
--n;
while (true) {
if (n == 0) {
nArray3[n3] = nArray7[1];
nArray4[n3++] = nArray8[1];
return n3;
}
nArray7[2] = nArray[n4];
nArray8[2] = nArray2[n4];
if (n > 1) {
nArray7[2] = (nArray7[2] + nArray7[1]) / 2;
nArray8[2] = (nArray8[2] + nArray8[1]) / 2;
}
--n;
this.dCenterX = nArray7[0] + nArray7[2] - nArray7[1];
this.dCenterY = nArray8[0] + nArray8[2] - nArray8[1];
this.dP = (double) nArray7[0] - this.dCenterX;
this.dQ = (double) nArray8[2] - this.dCenterY;
this.dR = (double) nArray7[2] - this.dCenterX;
this.dS = (double) nArray8[0] - this.dCenterY;
int n10 = (int) this.dP;
int n11 = (int) this.dQ;
int n12 = (int) this.dR;
int n13 = (int) this.dS;
int n14 = Math.max(Math.abs(n10) + Math.abs(n13), Math.abs(n12) + Math.abs(n11));
this.dRad = n14;
if (n14 == 0) {
return n3;
}
int n15 = this.bres(this.dRad);
for (n2 = 0; n2 <= n15; ++n2) {
this.filletHODSegment2(nArray3, nArray4, n3, this.dXq[n2], this.dYq[n2]);
if (n6 == nArray3[n3] && n9 == nArray4[n3]) continue;
n6 = nArray3[n3];
n9 = nArray4[n3++];
}
for (n2 = n15; n2 >= 0; --n2) {
this.filletHODSegment2(nArray3, nArray4, n3, this.dYq[n2], this.dXq[n2]);
if (n6 == nArray3[n3] && n9 == nArray4[n3]) continue;
n6 = nArray3[n3];
n9 = nArray4[n3++];
}
if (n6 != nArray7[2] || n9 != nArray8[2]) {
nArray3[n3] = nArray7[2];
nArray4[n3++] = nArray8[2];
}
if (n == 0) {
return n3;
}
nArray7[0] = n6 = nArray7[2];
nArray8[0] = n9 = nArray8[2];
nArray7[1] = nArray[n4];
nArray8[1] = nArray2[n4++];
}
}
}
@@ -50,6 +50,19 @@ public class GddmCoordinateTransform {
private double transformX = 1.0;
private double transformY = 1.0;
// Aspect Ratio parameters (P, Q, R, S) per IBM Host On-Demand (HODDecoder / GSAP)
public static final int DEFAULT_P = 1;
public static final int DEFAULT_Q = 1;
public static final int DEFAULT_R = 0;
public static final int DEFAULT_S = 0;
private int paramP = DEFAULT_P;
private int paramQ = DEFAULT_Q;
private int paramR = DEFAULT_R;
private int paramS = DEFAULT_S;
private double scaleX = 1.0;
private double scaleY = 1.0;
public GddmCoordinateTransform() {
this(80, 24, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
}
@@ -270,4 +283,68 @@ public class GddmCoordinateTransform {
public int convertYtoRow(int y) {
return (defaultCharHeight > 0) ? (y / defaultCharHeight) + 1 : 1;
}
// =========================================================================
// Aspect Ratio & Coordinate Inversion (Phase 17.2)
// =========================================================================
/**
* Sets GOCA aspect ratio parameters P, Q, R, S and updates scaling factors.
* ScaleX = P/Q, ScaleY = R/S. Defaults: P=1, Q=1, R=0, S=0.
*/
public void setAspectRatio(int p, int q, int r, int s) {
this.paramP = p;
this.paramQ = q;
this.paramR = r;
this.paramS = s;
this.scaleX = (q != 0) ? ((double) p / (double) q) : 1.0;
this.scaleY = (s != 0) ? ((double) r / (double) s) : 1.0;
if (this.scaleX <= 0.0) this.scaleX = 1.0;
if (this.scaleY <= 0.0) this.scaleY = 1.0;
}
/**
* Resets aspect ratio parameters to defaults: P=1, Q=1, R=0, S=0.
*/
public void resetAspectRatio() {
setAspectRatio(DEFAULT_P, DEFAULT_Q, DEFAULT_R, DEFAULT_S);
}
public int getParamP() { return paramP; }
public int getParamQ() { return paramQ; }
public int getParamR() { return paramR; }
public int getParamS() { return paramS; }
public double getScaleX() { return scaleX; }
public double getScaleY() { return scaleY; }
/**
* Inverts GOCA Cartesian Y (positive upward from bottom-left) to raster Y (top-down):
* Y_screen = (originY - gocaY) * ScaleY.
*/
public int mapGocaYToScreen(int originY, int gocaY) {
return (int) Math.round((originY - gocaY) * scaleY);
}
/**
* Inverts GOCA Cartesian Y to raster screen Y using the transform's default origin YMax.
*/
public int mapGocaYToScreen(int gocaY) {
return mapGocaYToScreen(getYMax(), gocaY);
}
/**
* Inverts raster Y (top-down) back to GOCA Cartesian Y (positive upward from bottom-left):
* Y_goca = originY - (screenY / ScaleY).
*/
public int mapScreenYToGoca(int originY, int screenY) {
return originY - (scaleY > 0 ? (int) Math.round(screenY / scaleY) : screenY);
}
/**
* Inverts raster screen Y back to GOCA Cartesian Y using the transform's default origin YMax.
*/
public int mapScreenYToGoca(int screenY) {
return mapScreenYToGoca(getYMax(), screenY);
}
}
@@ -40,10 +40,15 @@ public class GocaDecoder {
private double charHeight = 16.0;
private int charSet = 0;
private int charPrecision = GocaConstants.CP_STRING;
private int arcParamP = 1;
private int arcParamQ = 0;
private int arcParamR = 0;
private int arcParamS = 1;
public static final int DEFAULT_ARC_P = 1;
public static final int DEFAULT_ARC_Q = 1;
public static final int DEFAULT_ARC_R = 0;
public static final int DEFAULT_ARC_S = 0;
private int arcParamP = DEFAULT_ARC_P;
private int arcParamQ = DEFAULT_ARC_Q;
private int arcParamR = DEFAULT_ARC_R;
private int arcParamS = DEFAULT_ARC_S;
private boolean segChained = false;
private boolean segDynamic = false;
@@ -77,6 +82,7 @@ public class GocaDecoder {
private int imgHeight = 0;
private int imgBitDepth = GocaConstants.BPP_1;
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
private int imgMultiplier = 1;
private final List<Byte> imgBuffer = new ArrayList<>();
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
@@ -368,7 +374,15 @@ public class GocaDecoder {
inImage = false;
imgBitDepth = GocaConstants.BPP_1;
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
imgMultiplier = 1;
imgBuffer.clear();
arcParamP = DEFAULT_ARC_P;
arcParamQ = DEFAULT_ARC_Q;
arcParamR = DEFAULT_ARC_R;
arcParamS = DEFAULT_ARC_S;
if (plane != null && plane.getTransform() != null) {
plane.getTransform().setAspectRatio(arcParamP, arcParamQ, arcParamR, arcParamS);
}
}
private byte[] partialOrderBuffer = new byte[0];
@@ -567,7 +581,10 @@ public class GocaDecoder {
int w = 0, h = 0;
int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED;
int imult = 1;
if (payloadLen >= 6 && idx + 2 + payloadLen <= end) {
imult = readCoord(inputData, idx + 2);
if (imult <= 0) imult = 1;
w = readCoord(inputData, idx + 4);
h = readCoord(inputData, idx + 6);
if (payloadLen >= 7) {
@@ -592,7 +609,7 @@ public class GocaDecoder {
compression = inputData[idx + 7] & 0xFF;
}
}
beginImage(curX, curY, w, h, bitDepth, compression);
beginImage(curX, curY, w, h, bitDepth, compression, imult);
idx += orderLen;
break;
}
@@ -672,13 +689,25 @@ public class GocaDecoder {
idx += orderLen;
break;
}
case GocaConstants.G_GSAP: { // Arc Parameters (0x22)
case GocaConstants.G_GSAP: { // Arc Parameters / Set Aspect Ratio (0x22)
if (idx + 9 < end) {
arcParamP = readCoord(inputData, idx + 2);
arcParamQ = readCoord(inputData, idx + 4);
arcParamR = readCoord(inputData, idx + 6);
arcParamS = readCoord(inputData, idx + 8);
} else {
arcParamP = DEFAULT_ARC_P;
arcParamQ = DEFAULT_ARC_Q;
arcParamR = DEFAULT_ARC_R;
arcParamS = DEFAULT_ARC_S;
}
if (plane != null && plane.getTransform() != null) {
plane.getTransform().setAspectRatio(arcParamP, arcParamQ, arcParamR, arcParamS);
}
logger.info(String.format("GOCA GSAP: P=%d Q=%d R=%d S=%d (ScaleX=%.2f ScaleY=%.2f)",
arcParamP, arcParamQ, arcParamR, arcParamS,
(arcParamQ != 0 ? (double) arcParamP / arcParamQ : 1.0),
(arcParamS != 0 ? (double) arcParamR / arcParamS : 1.0)));
idx += orderLen;
break;
}
@@ -1012,7 +1041,10 @@ public class GocaDecoder {
int w, h;
int bitDepth = GocaConstants.BPP_1;
int compression = GocaConstants.IMG_UNCOMPRESSED;
int imult = 1;
if (payloadLen >= 10) {
imult = readCoord(inputData, idx + 6);
if (imult <= 0) imult = 1;
w = readCoord(inputData, idx + 8);
h = readCoord(inputData, idx + 10);
if (payloadLen >= 11) {
@@ -1037,7 +1069,7 @@ public class GocaDecoder {
compression = inputData[idx + 11] & 0xFF;
}
}
beginImage(x, y, w, h, bitDepth, compression);
beginImage(x, y, w, h, bitDepth, compression, imult);
}
idx += orderLen;
break;
@@ -1309,10 +1341,14 @@ public class GocaDecoder {
}
private void beginImage(int x, int y, int w, int h) {
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED, 1);
}
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression) {
beginImage(x, y, w, h, bitDepth, compression, 1);
}
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression, int multiplier) {
this.inImage = true;
this.imgX = x;
this.imgY = y;
@@ -1320,6 +1356,7 @@ public class GocaDecoder {
this.imgHeight = h;
this.imgBitDepth = bitDepth;
this.imgCompression = compression;
this.imgMultiplier = (multiplier > 0) ? multiplier : 1;
this.imgBuffer.clear();
}
@@ -1335,7 +1372,7 @@ public class GocaDecoder {
int px = plane.mapX(imgX);
int py = plane.mapY(imgY);
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression);
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression, imgMultiplier);
inImage = false;
imgBuffer.clear();
}
@@ -1429,38 +1466,191 @@ public class GocaDecoder {
private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) {
int pos = off;
int end = off + len;
if (isFull) {
// GFARC (0xC7) or GCFARC (0x87) Full Arc / Ellipse
int centerX = curX;
int centerY = curY;
if (!fromCurPos && pos + 4 <= end) {
centerX = readCoord(data, pos);
centerY = readCoord(data, pos + 2);
pos += 4;
}
double multiplier = 1.0;
if (pos + 2 <= end) {
int intPart = data[pos];
int fracPart = data[pos + 1] & 0xFF;
multiplier = intPart + fracPart / 256.0;
if (multiplier <= 0.0) multiplier = 1.0;
pos += 2;
}
double dP = arcParamP * multiplier;
double dQ = arcParamQ * multiplier;
double dR = arcParamR * multiplier;
double dS = arcParamS * multiplier;
int[] arcX = new int[256];
int[] arcY = new int[256];
int nPts = HODEllipse.getHODEllipse(dP, dQ, dR, dS, arcX, arcY, centerX, centerY);
for (int i = 0; i < nPts; i++) {
trackPoint(arcX[i], arcY[i]);
}
if (inArea) {
for (int i = 0; i < nPts; i++) {
if (i == 0 && currentPolyPts == 0) {
addAreaLineStart(arcX[i], arcY[i]);
} else {
addAreaPoint(arcX[i], arcY[i]);
}
}
} else {
for (int i = 0; i < nPts - 1; i++) {
plane.drawLine(plane.mapXDouble(arcX[i]), plane.mapYDouble(arcY[i]),
plane.mapXDouble(arcX[i + 1]), plane.mapYDouble(arcY[i + 1]),
curColor, lineType, lineWidth);
}
}
curX = centerX;
curY = centerY;
return;
}
// Partial Arc: GARC (0xC6) or GCARC (0x86)
// 3-point circular arc definition per GOCA specs: start, intermediate, end
int x0 = curX;
int y0 = curY;
boolean has3Point = false;
int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
if (!fromCurPos && len >= 12 && pos + 12 <= end) {
x0 = readCoord(data, pos);
y0 = readCoord(data, pos + 2);
x1 = readCoord(data, pos + 4);
y1 = readCoord(data, pos + 6);
x2 = readCoord(data, pos + 8);
y2 = readCoord(data, pos + 10);
has3Point = true;
} else if (fromCurPos && len >= 8 && pos + 8 <= end) {
x1 = readCoord(data, pos);
y1 = readCoord(data, pos + 2);
x2 = readCoord(data, pos + 4);
y2 = readCoord(data, pos + 6);
has3Point = true;
} else if (!fromCurPos && len >= 8 && pos + 8 <= end) {
x1 = readCoord(data, pos);
y1 = readCoord(data, pos + 2);
x2 = readCoord(data, pos + 4);
y2 = readCoord(data, pos + 6);
has3Point = true;
}
if (has3Point) {
double d = 2.0 * (x0 * (y1 - y2) + x1 * (y2 - y0) + x2 * (y0 - y1));
if (Math.abs(d) < 1e-6) {
// Collinear points: draw straight line segments P0 -> P1 -> P2
trackPoint(x0, y0);
trackPoint(x1, y1);
trackPoint(x2, y2);
if (inArea) {
if (currentPolyPts == 0) addAreaLineStart(x0, y0);
addAreaPoint(x1, y1);
addAreaPoint(x2, y2);
} else {
plane.drawLine(plane.mapXDouble(x0), plane.mapYDouble(y0),
plane.mapXDouble(x1), plane.mapYDouble(y1),
curColor, lineType, lineWidth);
plane.drawLine(plane.mapXDouble(x1), plane.mapYDouble(y1),
plane.mapXDouble(x2), plane.mapYDouble(y2),
curColor, lineType, lineWidth);
}
curX = x2;
curY = y2;
return;
}
double x0sq = (double) x0 * x0 + (double) y0 * y0;
double x1sq = (double) x1 * x1 + (double) y1 * y1;
double x2sq = (double) x2 * x2 + (double) y2 * y2;
double ccx = (x0sq * (y1 - y2) + x1sq * (y2 - y0) + x2sq * (y0 - y1)) / d;
double ccy = (x0sq * (x2 - x1) + x1sq * (x0 - x2) + x2sq * (x1 - x0)) / d;
double rad = Math.hypot(x0 - ccx, y0 - ccy);
double theta0 = Math.atan2(y0 - ccy, x0 - ccx);
double theta1 = Math.atan2(y1 - ccy, x1 - ccx);
double theta2 = Math.atan2(y2 - ccy, x2 - ccx);
if (theta0 < 0) theta0 += 2 * Math.PI;
if (theta1 < 0) theta1 += 2 * Math.PI;
if (theta2 < 0) theta2 += 2 * Math.PI;
double d01 = theta1 - theta0;
while (d01 < 0) d01 += 2 * Math.PI;
double d02 = theta2 - theta0;
while (d02 < 0) d02 += 2 * Math.PI;
double sweep = (d01 < d02) ? d02 : (d02 - 2 * Math.PI);
int steps = Math.max(8, (int) Math.round(Math.abs(Math.toDegrees(sweep)) / 5.0));
int prevX = x0;
int prevY = y0;
trackPoint(x0, y0);
if (inArea && currentPolyPts == 0) {
addAreaLineStart(x0, y0);
}
for (int s = 1; s <= steps; s++) {
double t = (double) s / steps;
double angle = theta0 + t * sweep;
int px = (s == steps) ? x2 : (int) Math.round(ccx + rad * Math.cos(angle));
int py = (s == steps) ? y2 : (int) Math.round(ccy + rad * Math.sin(angle));
trackPoint(px, py);
if (inArea) {
addAreaPoint(px, py);
} else {
plane.drawLine(plane.mapXDouble(prevX), plane.mapYDouble(prevY),
plane.mapXDouble(px), plane.mapYDouble(py),
curColor, lineType, lineWidth);
}
prevX = px;
prevY = py;
}
curX = x2;
curY = y2;
return;
}
// Fallback: sweep angle parameter decoding
int centerX = curX;
int centerY = curY;
// For absolute variants, read the center point from data
if (!fromCurPos && pos + 4 <= off + len) {
if (!fromCurPos && pos + 4 <= end) {
centerX = readCoord(data, pos);
centerY = readCoord(data, pos + 2);
pos += 4;
}
// Read the multiplier (integer.fraction format)
double multiplier = 1.0;
if (pos + 2 <= off + len) {
int intPart = data[pos]; // signed byte
if (pos + 2 <= end) {
int intPart = data[pos];
int fracPart = data[pos + 1] & 0xFF;
multiplier = intPart + fracPart / 256.0;
if (multiplier == 0.0) multiplier = 1.0;
pos += 2;
}
// Compute semi-axes from arc parameters scaled by multiplier
// P,Q and R,S are conjugate semi-diameter vectors
double pScaled = arcParamP * multiplier;
double qScaled = arcParamQ * multiplier;
double rScaled = arcParamR * multiplier;
double sScaled = arcParamS * multiplier;
// Semi-axis lengths in GOCA coordinate space
double semiAxis1 = Math.sqrt(pScaled * pScaled + qScaled * qScaled);
double semiAxis2 = Math.sqrt(rScaled * rScaled + sScaled * sScaled);
// If arc parameters are at default (1,0,0,1) and curPos != center, use distance to center as radius
double dist = Math.hypot(curX - centerX, curY - centerY);
if (semiAxis1 <= 1.01 && dist > 1.0) {
semiAxis1 = dist * multiplier;
@@ -1470,7 +1660,6 @@ public class GocaDecoder {
if (semiAxis1 < 1.0) semiAxis1 = 1.0;
if (semiAxis2 < 1.0) semiAxis2 = 1.0;
// Map to pixel space with double precision
double cx = plane.mapXDouble(centerX);
double cy = plane.mapYDouble(centerY);
double rx = Math.abs(plane.mapXDouble(semiAxis1) - plane.mapXDouble(0));
@@ -1478,38 +1667,24 @@ public class GocaDecoder {
if (rx <= 0.0) rx = Math.max(1.0, semiAxis1);
if (ry <= 0.0) ry = Math.max(1.0, semiAxis2);
// For partial arcs, determine start angle and sweep angle
double startAngleDeg = 0.0;
double sweepAngleDeg = 360.0;
if (!isFull) {
if (pos + 4 <= off + len) {
// Sweep start: integer.fraction of full revolution
int startInt = data[pos];
int startFrac = data[pos + 1] & 0xFF;
startAngleDeg = (startInt + startFrac / 256.0) * 360.0;
pos += 2;
// Sweep angle: integer.fraction of full revolution
int sweepInt = data[pos];
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
} else if (pos + 2 <= off + len) {
// Single sweep parameter
int sweepInt = data[pos];
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
if (curX != centerX || curY != centerY) {
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
startAngleDeg = Math.toDegrees(startRad);
if (startAngleDeg < 0) startAngleDeg += 360.0;
}
} else {
// No sweep data — if we have a current point, start there and sweep full circle
if (curX != centerX || curY != centerY) {
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
startAngleDeg = Math.toDegrees(startRad);
if (startAngleDeg < 0) startAngleDeg += 360.0;
}
sweepAngleDeg = 360.0;
if (pos + 4 <= end) {
int startInt = data[pos];
int startFrac = data[pos + 1] & 0xFF;
startAngleDeg = (startInt + startFrac / 256.0) * 360.0;
pos += 2;
int sweepInt = data[pos];
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
} else if (pos + 2 <= end) {
int sweepInt = data[pos];
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
if (curX != centerX || curY != centerY) {
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
startAngleDeg = Math.toDegrees(startRad);
if (startAngleDeg < 0) startAngleDeg += 360.0;
}
}
@@ -1533,7 +1708,7 @@ public class GocaDecoder {
}
plane.drawArc(cx, cy, rx, ry, startAngleDeg, sweepAngleDeg,
curColor, lineType, lineWidth, isFull);
curColor, lineType, lineWidth, false);
curX = centerX;
curY = centerY;
@@ -1543,63 +1718,62 @@ public class GocaDecoder {
int pos = off;
int end = off + len;
List<Double> ptsX = new ArrayList<>();
List<Double> ptsY = new ArrayList<>();
List<Integer> gocaPtsX = new ArrayList<>();
List<Integer> gocaPtsY = new ArrayList<>();
if (fromCurPos) {
trackPoint(curX, curY);
ptsX.add(plane.mapXDouble(curX));
ptsY.add(plane.mapYDouble(curY));
gocaPtsX.add(curX);
gocaPtsY.add(curY);
if (inArea && currentPolyPts == 0) {
addAreaLineStart(curX, curY);
}
}
while (pos + 4 <= end) {
int x = readCoord(data, pos);
int y = readCoord(data, pos + 2);
trackPoint(x, y);
ptsX.add(plane.mapXDouble(x));
ptsY.add(plane.mapYDouble(y));
gocaPtsX.add(x);
gocaPtsY.add(y);
if (inArea && ptsX.size() == 1 && currentPolyPts == 0) {
addAreaLineStart(x, y);
}
curX = x;
curY = y;
pos += 4;
}
if (ptsX.size() >= 2) {
int n = ptsX.size();
double[] px = new double[n];
double[] py = new double[n];
for (int i = 0; i < n; i++) {
px[i] = ptsX.get(i);
py[i] = ptsY.get(i);
}
int n = gocaPtsX.size();
if (n < 2) return;
int[] ctrlX = new int[n];
int[] ctrlY = new int[n];
for (int i = 0; i < n; i++) {
ctrlX[i] = gocaPtsX.get(i);
ctrlY[i] = gocaPtsY.get(i);
trackPoint(ctrlX[i], ctrlY[i]);
}
FilletPts fillet = new FilletPts();
int req = fillet.getPointsRequired(n);
int[] outX = new int[Math.max(req, n * 32)];
int[] outY = new int[Math.max(req, n * 32)];
int nPoints = fillet.getFilletPoints(ctrlX, ctrlY, n, outX, outY);
if (nPoints > 0) {
for (int i = 0; i < nPoints; i++) {
trackPoint(outX[i], outY[i]);
}
if (inArea) {
int gn = gocaPtsX.size();
int[] gx = new int[gn];
int[] gy = new int[gn];
for (int i = 0; i < gn; i++) {
gx[i] = gocaPtsX.get(i);
gy[i] = gocaPtsY.get(i);
}
int[][] curve = FilletPts.calculate(gx, gy, gn, 16);
for (int i = 1; i < curve[0].length; i++) {
addAreaPoint(curve[0][i], curve[1][i]);
for (int i = 0; i < nPoints; i++) {
if (i == 0 && currentPolyPts == 0) {
addAreaLineStart(outX[0], outY[0]);
} else {
addAreaPoint(outX[i], outY[i]);
}
}
} else {
plane.drawFillet(px, py, n, curColor, lineType, lineWidth);
for (int i = 0; i < nPoints - 1; i++) {
plane.drawLine(plane.mapXDouble(outX[i]), plane.mapYDouble(outY[i]),
plane.mapXDouble(outX[i + 1]), plane.mapYDouble(outY[i + 1]),
curColor, lineType, lineWidth);
}
}
}
curX = ctrlX[n - 1];
curY = ctrlY[n - 1];
}
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
@@ -1824,6 +1998,15 @@ public class GocaDecoder {
resetDefaults();
}
public synchronized int getArcParamP() { return arcParamP; }
public synchronized int getArcParamQ() { return arcParamQ; }
public synchronized int getArcParamR() { return arcParamR; }
public synchronized int getArcParamS() { return arcParamS; }
public synchronized int getImageMultiplier() { return imgMultiplier; }
public synchronized boolean hasActiveGraphics() {
return graphicsCursorActive || (plane != null && plane.hasContent());
}
private int readCoord(byte[] data, int off) {
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
}
@@ -1159,7 +1159,9 @@ public class GraphicsPlane implements PixelBuffer {
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
setPixelInFill(x, y, fill, isMixOr);
int pixColor = (psSlot.isTriplePlane() && psSlot.getRgbPixels(fill, bg) != null)
? psSlot.getRgbPixels(fill, bg)[pIdx] : fill;
setPixelInFill(x, y, pixColor, isMixOr);
} else if (isOpaqueBg) {
setPixelInFill(x, y, bg, isMixOr);
}
@@ -1192,7 +1194,9 @@ public class GraphicsPlane implements PixelBuffer {
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
setPixelInFill(x, y, fill, isMixOr);
int pixColor = (psSlot.isTriplePlane() && psSlot.getRgbPixels(fill, bg) != null)
? psSlot.getRgbPixels(fill, bg)[pIdx] : fill;
setPixelInFill(x, y, pixColor, isMixOr);
} else if (isOpaqueBg) {
setPixelInFill(x, y, bg, isMixOr);
}
@@ -1221,12 +1225,12 @@ public class GraphicsPlane implements PixelBuffer {
for (int i = 0; i < pLen - 1; i++) {
drawLine((double) px[offset + i], (double) py[offset + i],
(double) px[offset + i + 1], (double) py[offset + i + 1],
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
boundaryColorArgb, lineType, lineWidth);
}
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
(double) px[offset], (double) py[offset],
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
boundaryColorArgb, lineType, lineWidth);
}
}
offset += pLen;
@@ -1417,6 +1421,16 @@ public class GraphicsPlane implements PixelBuffer {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
}
public synchronized void drawVectorString(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, shearAngle);
}
public synchronized void drawVectorString(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
}
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
drawVssChar(x, y, c, color, cw, ch, 0.0, 0.0);
}
@@ -1530,8 +1544,17 @@ public class GraphicsPlane implements PixelBuffer {
*/
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
int bitDepth, int compressionMode) {
drawImage(x, y, width, height, imageData, fgColorArgb, bitDepth, compressionMode, 1);
}
/**
* Draws bitmap image with support for bit depth, RLE decompression, and image multiplier (imult).
*/
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
int bitDepth, int compressionMode, int multiplier) {
if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
int m = (multiplier > 0) ? multiplier : 1;
byte[] rawData = imageData;
if (compressionMode == GocaConstants.IMG_RLE) {
@@ -1549,7 +1572,15 @@ public class GraphicsPlane implements PixelBuffer {
if (byteIdx < rawData.length) {
boolean bit = ((rawData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
if (bit) {
setPixel(x + col, y + row, fgColor);
if (m <= 1) {
setPixel(x + col, y + row, fgColor);
} else {
for (int dy = 0; dy < m; dy++) {
for (int dx = 0; dx < m; dx++) {
setPixel(x + col * m + dx, y + row * m + dy, fgColor);
}
}
}
}
}
}
@@ -1572,7 +1603,15 @@ public class GraphicsPlane implements PixelBuffer {
case 3: pixelColor = GocaConstants.GOCA_COLORS[4]; break; // Green
default: pixelColor = fgColor; break;
}
setPixel(x + col, y + row, pixelColor);
if (m <= 1) {
setPixel(x + col, y + row, pixelColor);
} else {
for (int dy = 0; dy < m; dy++) {
for (int dx = 0; dx < m; dx++) {
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
}
}
}
}
}
}
@@ -1588,7 +1627,16 @@ public class GraphicsPlane implements PixelBuffer {
int shift = (1 - (col % 2)) * 4;
int val = (rawData[byteIdx] >> shift) & 0x0F;
if (val != 0) {
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
int pixelColor = GocaConstants.getGocaColorArgb(val);
if (m <= 1) {
setPixel(x + col, y + row, pixelColor);
} else {
for (int dy = 0; dy < m; dy++) {
for (int dx = 0; dx < m; dx++) {
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
}
}
}
}
}
}
@@ -1602,7 +1650,16 @@ public class GraphicsPlane implements PixelBuffer {
if (byteIdx < rawData.length) {
int val = rawData[byteIdx] & 0xFF;
if (val != 0) {
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
int pixelColor = GocaConstants.getGocaColorArgb(val);
if (m <= 1) {
setPixel(x + col, y + row, pixelColor);
} else {
for (int dy = 0; dy < m; dy++) {
for (int dx = 0; dx < m; dx++) {
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
}
}
}
}
}
}
@@ -0,0 +1,151 @@
package haus.nightmare.lib3270j.graphics;
/**
* Calculates intermediate points for IBM GOCA ellipses and arcs using conjugate semi-axes P, Q, R, S.
* Matches IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODEllipse and HODGraphUtil).
*/
public class HODEllipse {
protected static final double[] dXgxdq = new double[] {
1.0, 0.999847695, 0.999390827, 0.998629534, 0.99756405, 0.996194698,
0.994521895, 0.992546151, 0.990268068, 0.98768834, 0.984807753, 0.981627183,
0.9781476, 0.974370064, 0.970295726, 0.965925826, 0.961261695, 0.956304756,
0.951056516, 0.945518575, 0.93969262, 0.933580426, 0.927183854, 0.920504853,
0.913545457, 0.906307787, 0.898794046, 0.891006524, 0.882947592, 0.874619707,
0.866025403, 0.8571673, 0.848048096, 0.838670567, 0.829037572, 0.819152044,
0.809016994, 0.79863551, 0.788010753, 0.777145961, 0.766044443, 0.75470958,
0.743144825, 0.731353701, 0.7193398, 0.707106781
};
protected static final double[] dXgydq = new double[] {
0.0, 0.017452406, 0.034899496, 0.052335956, 0.069756473, 0.087155742,
0.104528463, 0.121869343, 0.139173101, 0.156434465, 0.173648177, 0.190808995,
0.20791169, 0.224951054, 0.241921895, 0.258819045, 0.275637355, 0.292371704,
0.309016994, 0.325568154, 0.342020143, 0.358367949, 0.374606593, 0.390731128,
0.406736643, 0.422618261, 0.438371146, 0.453990499, 0.469471562, 0.48480962,
0.5, 0.515038074, 0.529919264, 0.544639035, 0.559192903, 0.573576436,
0.587785252, 0.601815023, 0.615661475, 0.629320391, 0.642787609, 0.656059029,
0.669130606, 0.68199836, 0.69465837, 0.707106781
};
protected double dP;
protected double dQ;
protected double dR;
protected double dS;
protected int iP;
protected int iQ;
protected int iR;
protected int iS;
protected double dRad;
protected double dCenterX;
protected double dCenterY;
protected double[] dXq = new double[9];
protected double[] dYq = new double[9];
public HODEllipse(double p, double q, double r, double s) {
this.dP = p;
this.dQ = q;
this.dR = r;
this.dS = s;
this.iP = (int) Math.round(p);
this.iQ = (int) Math.round(q);
this.iR = (int) Math.round(r);
this.iS = (int) Math.round(s);
}
public int getHODPoints(int[] outX, int[] outY, int centerX, int centerY) {
if (outX == null || outY == null || outX.length < 128 || outY.length < 128) {
return 0;
}
int ptIdx = 0;
int n5 = Math.max(Math.abs(this.iP) + Math.abs(this.iS), Math.abs(this.iR) + Math.abs(this.iQ));
this.dRad = n5;
this.dCenterX = centerX;
this.dCenterY = centerY;
if (n5 == 0) {
outX[0] = centerX;
outY[0] = centerY;
return 1;
}
int n6 = this.bres(this.dRad);
int lastX = this.iP + centerX;
int lastY = this.iS + centerY;
for (int i = 0; i <= n6; ++i) {
this.filletHODSegment(outX, outY, ptIdx, this.dXq[i], this.dYq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = n6; i >= 0; --i) {
this.filletHODSegment(outX, outY, ptIdx, this.dYq[i], this.dXq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = 0; i <= n6; ++i) {
this.filletHODSegment(outX, outY, ptIdx, -this.dYq[i], this.dXq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = n6; i >= 0; --i) {
this.filletHODSegment(outX, outY, ptIdx, -this.dXq[i], this.dYq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = 0; i <= n6; ++i) {
this.filletHODSegment(outX, outY, ptIdx, -this.dXq[i], -this.dYq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = n6; i >= 0; --i) {
this.filletHODSegment(outX, outY, ptIdx, -this.dYq[i], -this.dXq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = 0; i <= n6; ++i) {
this.filletHODSegment(outX, outY, ptIdx, this.dYq[i], -this.dXq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
for (int i = n6; i >= 0; --i) {
this.filletHODSegment(outX, outY, ptIdx, this.dXq[i], -this.dYq[i]);
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
lastX = outX[ptIdx];
lastY = outY[ptIdx++];
}
outX[ptIdx] = outX[0];
outY[ptIdx++] = outY[0];
return ptIdx;
}
protected int bres(double rad) {
for (int i = 0; i < 9; ++i) {
this.dXq[i] = rad * dXgxdq[i * 5 + 2];
this.dYq[i] = rad * dXgydq[i * 5 + 2];
}
return 8;
}
protected void filletHODSegment(int[] outX, int[] outY, int idx, double d, double d2) {
outX[idx] = (int) Math.round(this.dP * d / this.dRad + this.dR * d2 / this.dRad + this.dCenterX);
outY[idx] = (int) Math.round(-(this.dS * d / this.dRad + this.dQ * d2 / this.dRad) + this.dCenterY);
}
protected void filletHODSegment2(int[] outX, int[] outY, int idx, double d, double d2) {
outX[idx] = (int) Math.round(this.dP * d / this.dRad + this.dR * d2 / this.dRad + this.dCenterX);
outY[idx] = (int) Math.round(this.dS * d / this.dRad + this.dQ * d2 / this.dRad + this.dCenterY);
}
public static int getHODEllipse(double p, double q, double r, double s, int[] outX, int[] outY, int cx, int cy) {
HODEllipse ellipse = new HODEllipse(p, q, r, s);
return ellipse.getHODPoints(outX, outY, cx, cy);
}
}
@@ -18,11 +18,12 @@ public class ProgramSymbolManager {
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
private int defaultCellWidth = 9;
private int defaultCellHeight = 12; // Standard IBM 3279 PS Slot Default Height (SDH = 0x0C = 12)
private int defaultCellHeight = 16; // Standard IBM 9x16 Programmed Symbol cell matrix
public void setDefaultCellDimensions(int width, int height) {
this.defaultCellWidth = (width > 0) ? width : 9;
this.defaultCellHeight = (height > 0) ? height : 12;
this.defaultCellHeight = (height > 0) ? height : 16;
}
public int getDefaultCellWidth() {
@@ -174,40 +175,56 @@ public class ProgramSymbolManager {
int startCodePoint = data[2] & 0xFF;
int rws = data[3] & 0xFF;
int normRws = rws;
if (normRws >= 0x40 && normRws <= 0x49) {
normRws = normRws - 0x40 + 2; // 0x40 -> 2 (PSA), 0x41 -> 3 (PSB), ..., 0x45 -> 7 (PSF)
} else if ((normRws < 2 || normRws > 11) && (lcid >= 0x40 && lcid <= 0x49)) {
normRws = lcid - 0x40 + 2;
}
int setIndex;
switch (rws) {
case 2: setIndex = 0; break;
case 3: setIndex = 1; break;
case 4: setIndex = 2; break;
case 5: setIndex = 3; break;
case 6: setIndex = 4; break;
case 7: setIndex = 5; break;
switch (normRws) {
case 2: setIndex = 0; break; // PSA (0x40)
case 3: setIndex = 1; break; // PSB (0x41)
case 4: setIndex = 2; break; // PSC (0x42)
case 5: setIndex = 3; break; // PSD (0x43)
case 6: setIndex = 4; break; // PSE (0x44)
case 7: setIndex = 5; break; // PSF (0x45)
case 8: setIndex = 6; break;
case 9: setIndex = 7; break;
case 10: setIndex = 8; break;
case 11: setIndex = 9; break;
default:
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
return;
if (lcid >= 0x40 && lcid < 256) {
setIndex = (lcid - 0x40) % NUMBER_SYMBOL_SETS;
} else {
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
return;
}
break;
}
boolean isTriplePlane = (rws >= 4 && rws <= 7);
ProgramSymbolSet set = sets[setIndex];
boolean isTriplePlane = set.isTriplePlane() || (normRws >= 4 && normRws <= 7);
if (clearAll) {
set.clear();
}
int extHeaderLen = 0;
int cellWidth = defaultCellWidth;
int cellHeight = defaultCellHeight;
int cellWidth = (loadFormat == 1) ? 9 : (defaultCellWidth > 0 ? defaultCellWidth : 9);
int cellHeight = (loadFormat == 1) ? 16 : (defaultCellHeight > 0 ? defaultCellHeight : 16);
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
if (hasExtHeader && data.length > 4) {
extHeaderLen = data[4] & 0xFF;
if (extHeaderLen > 3 && data.length > 6) {
int lw = data[6] & 0xFF;
if (lw > 0) cellWidth = lw;
if (lw > 0 && loadFormat != 1) cellWidth = lw;
}
if (extHeaderLen > 4 && data.length > 7) {
int lh = data[7] & 0xFF;
if (lh > 0) cellHeight = lh;
if (lh > 0 && loadFormat != 1) cellHeight = lh;
}
if (extHeaderLen >= 6 && data.length > 9) {
colorPlane = data[9] & 0xFF;
@@ -223,55 +240,34 @@ public class ProgramSymbolManager {
int remaining = data.length - offset;
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
if (sliceBytes <= 0) sliceBytes = 18;
int bytesPerSymbol;
if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
bytesPerSymbol = sliceBytes * 3;
} else {
bytesPerSymbol = sliceBytes;
}
int bytesPerSymbol = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
if (bytesPerSymbol <= 0) bytesPerSymbol = 18;
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
byte[] pixelData = new byte[cellWidth * cellHeight];
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
if (!clearAll && existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
// If loading a specific color plane (colorPlane != 0), merge with existing ONLY IF existing was not all-planes
if (isTriplePlane && colorPlane != 0 && existing != null && !existing.isAllPlanesLoaded()
&& existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
System.arraycopy(existing.getPixelData(), 0, pixelData, 0,
Math.min(existing.getPixelData().length, pixelData.length));
}
if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
// 3 consecutive slices: Red (plane 1), Green (plane 2), Blue (plane 4)
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, true, 1);
unpackFormat1(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
unpackFormat1(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, true, 1);
unpackFormat3(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
unpackFormat3(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
}
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
} else {
if (loadFormat == 1) {
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
} else {
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
}
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
}
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
boolean allPlanes = (colorPlane == 0);
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane, allPlanes));
codeIndex++;
offset += bytesPerSymbol;
remaining -= bytesPerSymbol;
}
if (clearAll) {
for (int i = codeIndex; i < ProgramSymbolSet.NUM_SLOTS; i++) {
set.clearSlot(i);
}
}
if (logger.isLoggable(Level.FINE)) {
logger.fine(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
@@ -62,6 +62,7 @@ public class ProgramSymbolSet {
private final int height;
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
private final boolean isTriplePlane;
private final boolean allPlanesLoaded;
private int[] cachedRgbArray;
private int cachedFgRgb = -1;
private int cachedBgRgb = -1;
@@ -73,10 +74,19 @@ public class ProgramSymbolSet {
private int cachedScaledBgRgb = -1;
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
this(width, height, pixelData, isTriplePlane, false);
}
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane, boolean allPlanesLoaded) {
this.width = width > 0 ? width : 9;
this.height = height > 0 ? height : 16;
this.pixelData = pixelData;
this.isTriplePlane = isTriplePlane;
this.allPlanesLoaded = allPlanesLoaded;
}
public boolean isAllPlanesLoaded() {
return allPlanesLoaded;
}
public int getWidth() {
@@ -183,18 +193,18 @@ public class ProgramSymbolSet {
} else if (!isTriplePlane) {
rgbArray[idx] = fgArgb;
} else {
// Triple-Plane RGB composite aligned with IBM Host On-Demand 16-color GOCA palette:
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
// Triple-Plane RGB composite aligned with IBM 3179G / HoD GOCA palette:
// val bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
int colorMask = val & 0x07;
int colorArgb;
switch (colorMask) {
case 1: colorArgb = GocaConstants.GOCA_COLORS[2]; break; // Red
case 2: colorArgb = GocaConstants.GOCA_COLORS[4]; break; // Green
case 3: colorArgb = GocaConstants.GOCA_COLORS[6]; break; // Yellow
case 4: colorArgb = GocaConstants.GOCA_COLORS[1]; break; // Blue (CUSTOMBLUE)
case 5: colorArgb = GocaConstants.GOCA_COLORS[3]; break; // Pink
case 6: colorArgb = GocaConstants.GOCA_COLORS[5]; break; // Turquoise
case 7: colorArgb = GocaConstants.GOCA_COLORS[7]; break; // White
case 3: colorArgb = GocaConstants.GOCA_COLORS[6]; break; // Yellow (Red + Green)
case 4: colorArgb = GocaConstants.GOCA_COLORS[1]; break; // Blue
case 5: colorArgb = GocaConstants.GOCA_COLORS[3]; break; // Pink (Red + Blue)
case 6: colorArgb = GocaConstants.GOCA_COLORS[5]; break; // Turquoise (Green + Blue)
case 7: colorArgb = GocaConstants.GOCA_COLORS[7]; break; // White (Red + Green + Blue)
default: colorArgb = bgArgb; break;
}
rgbArray[idx] = colorArgb;
@@ -0,0 +1,588 @@
package haus.nightmare.lib3270j.input;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.*;
/**
* IBM Host On-Demand (HoD) .kmp keyboard map profile parser and serializer.
* Translates HoD hashKey codes (e.g. A67 -> Alt+C, B35 -> End, A82 -> Alt+R)
* and duplicate/numpad key representations.
*
* Implemented in pure Java standard library for lib3270j.
*/
public class HodKeyMap {
public static final int MOD_SHIFT = 1;
public static final int MOD_CTRL = 2;
public static final int MOD_META = 4;
public static final int MOD_ALT = 8;
public static final int LOCATION_STANDARD = 1;
public static final int LOCATION_LEFT = 2;
public static final int LOCATION_RIGHT = 3;
public static final int LOCATION_NUMPAD = 4;
public static final int KEY_LOC_LEFT = 0x1000000; // 16777216
public static final int KEY_LOC_NUMPAD = 0x2000000; // 33554432
// HoD duplicate keycodes for numpad arrows
public static final int NUMPAD_UP_CODE = 224;
public static final int NUMPAD_DOWN_CODE = 225;
public static final int NUMPAD_LEFT_CODE = 226;
public static final int NUMPAD_RIGHT_CODE = 227;
// Standard AWT VK codes
public static final int VK_ENTER = 10;
public static final int VK_CLEAR = 12;
public static final int VK_SHIFT = 16;
public static final int VK_CONTROL = 17;
public static final int VK_ALT = 18;
public static final int VK_PAGE_UP = 33;
public static final int VK_PAGE_DOWN = 34;
public static final int VK_END = 35;
public static final int VK_HOME = 36;
public static final int VK_LEFT = 37;
public static final int VK_UP = 38;
public static final int VK_RIGHT = 39;
public static final int VK_DOWN = 40;
public static final int VK_DELETE = 127;
public static final int VK_INSERT = 155;
public static final int VK_META = 157;
private static final Set<Integer> NUMPAD_DUP_KEYS = new HashSet<>(Arrays.asList(
VK_ENTER, VK_CLEAR, VK_PAGE_UP, VK_PAGE_DOWN, VK_END, VK_HOME, VK_DELETE, VK_INSERT
));
private static final Set<Integer> LEFT_DUP_KEYS = new HashSet<>(Arrays.asList(
VK_SHIFT, VK_CONTROL, VK_ALT, VK_META
));
// Common keycode to standard name mapping
private static final Map<Integer, String> KEYCODE_NAMES = new HashMap<>();
private static final Map<String, Integer> NAME_KEYCODES = new HashMap<>();
static {
// Alphanumeric A-Z (65-90)
for (char c = 'A'; c <= 'Z'; c++) {
KEYCODE_NAMES.put((int) c, String.valueOf(c));
NAME_KEYCODES.put(String.valueOf(c), (int) c);
}
// Digits 0-9 (48-57)
for (char c = '0'; c <= '9'; c++) {
KEYCODE_NAMES.put((int) c, String.valueOf(c));
NAME_KEYCODES.put(String.valueOf(c), (int) c);
}
// Function keys F1-F24
for (int i = 1; i <= 24; i++) {
int vk = (i <= 12) ? (111 + i) : (111 + i); // F1 is 112
KEYCODE_NAMES.put(111 + i, "F" + i);
NAME_KEYCODES.put("F" + i, 111 + i);
}
// Specific keys
registerKey(VK_ENTER, "ENTER");
registerKey(13, "ENTER");
registerKey(9, "TAB");
registerKey(8, "BACK_SPACE");
registerKey(27, "ESCAPE");
registerKey(32, "SPACE");
registerKey(VK_PAGE_UP, "PAGE_UP");
registerKey(VK_PAGE_DOWN, "PAGE_DOWN");
registerKey(VK_END, "END");
registerKey(VK_HOME, "HOME");
registerKey(VK_LEFT, "LEFT");
registerKey(VK_UP, "UP");
registerKey(VK_RIGHT, "RIGHT");
registerKey(VK_DOWN, "DOWN");
registerKey(VK_INSERT, "INSERT");
registerKey(VK_DELETE, "DELETE");
registerKey(VK_CLEAR, "CLEAR");
registerKey(144, "NUM_LOCK");
registerKey(145, "SCROLL_LOCK");
registerKey(19, "PAUSE");
registerKey(44, "PRINTSCREEN");
// Symbols
registerKey(59, "SEMICOLON");
registerKey(61, "EQUALS");
registerKey(44, "COMMA");
registerKey(45, "MINUS");
registerKey(46, "PERIOD");
registerKey(47, "SLASH");
registerKey(192, "BACK_QUOTE");
registerKey(91, "OPEN_BRACKET");
registerKey(92, "BACK_SLASH");
registerKey(93, "CLOSE_BRACKET");
registerKey(222, "QUOTE");
}
private static void registerKey(int code, String name) {
KEYCODE_NAMES.put(code, name);
NAME_KEYCODES.put(name.toUpperCase(), code);
}
public static class KeyEntry {
private final int keyCode;
private final int modifiers;
private final int keyLocation;
private final String value;
public KeyEntry(int keyCode, int modifiers, int keyLocation, String value) {
this.keyCode = keyCode;
this.modifiers = modifiers;
this.keyLocation = keyLocation;
this.value = value;
}
public int getKeyCode() { return keyCode; }
public int getModifiers() { return modifiers; }
public int getKeyLocation() { return keyLocation; }
public String getValue() { return value; }
public boolean isShift() { return (modifiers & MOD_SHIFT) != 0; }
public boolean isCtrl() { return (modifiers & MOD_CTRL) != 0; }
public boolean isMeta() { return (modifiers & MOD_META) != 0; }
public boolean isAlt() { return (modifiers & MOD_ALT) != 0; }
public String toKeyStrokeString() {
StringBuilder sb = new StringBuilder();
if (isCtrl()) sb.append("ctrl ");
if (isAlt()) sb.append("alt ");
if (isShift()) sb.append("shift ");
if (isMeta()) sb.append("meta ");
String name = KEYCODE_NAMES.get(keyCode);
if (name != null) {
sb.append(name);
} else {
sb.append("KEY_").append(keyCode);
}
return sb.toString().trim();
}
public String toHashKey() {
return HodKeyMap.hashKey(keyCode, modifiers, keyLocation);
}
@Override
public String toString() {
return toHashKey() + "=" + value + " (" + toKeyStrokeString() + ")";
}
}
/**
* Compute HoD modifier prefix character.
*/
public static char getModifierPrefix(int modifiers) {
int m = 0;
if ((modifiers & MOD_SHIFT) != 0) m |= 1;
if ((modifiers & MOD_CTRL) != 0) m |= 2;
if ((modifiers & MOD_META) != 0) m |= 4;
if ((modifiers & MOD_ALT) != 0) m |= 8;
switch (m) {
case 1: return 'S';
case 2: return 'C';
case 3: return 'D';
case 4: return 'M';
case 5: return 'E';
case 6: return 'F';
case 7: return 'G';
case 8: return 'A';
case 9: return 'H';
case 10: return 'I';
case 11: return 'J';
case 12: return 'K';
case 13: return 'L';
case 14: return 'N';
case 15: return 'O';
default: return 'B';
}
}
/**
* Decode HoD modifier prefix character into modifier bitmask.
*/
public static int getModifiersFromPrefix(char c) {
switch (Character.toUpperCase(c)) {
case 'S': return MOD_SHIFT;
case 'C': return MOD_CTRL;
case 'D': return MOD_SHIFT | MOD_CTRL;
case 'M': return MOD_META;
case 'E': return MOD_SHIFT | MOD_META;
case 'F': return MOD_CTRL | MOD_META;
case 'G': return MOD_SHIFT | MOD_CTRL | MOD_META;
case 'A': return MOD_ALT;
case 'H': return MOD_SHIFT | MOD_ALT;
case 'I': return MOD_CTRL | MOD_ALT;
case 'J': return MOD_SHIFT | MOD_CTRL | MOD_ALT;
case 'K': return MOD_META | MOD_ALT;
case 'L': return MOD_SHIFT | MOD_META | MOD_ALT;
case 'N': return MOD_CTRL | MOD_META | MOD_ALT;
case 'O': return MOD_SHIFT | MOD_CTRL | MOD_META | MOD_ALT;
default: return 0; // 'B' = base/none
}
}
/**
* Generates a HoD hashKey string for a given keycode, modifier mask, and key location.
*/
public static String hashKey(int keyCode, int modifiers, int keyLocation) {
char prefix = getModifierPrefix(modifiers);
String codeStr;
if (keyLocation == LOCATION_NUMPAD) {
if (keyCode == VK_UP) {
codeStr = String.valueOf(NUMPAD_UP_CODE);
} else if (keyCode == VK_DOWN) {
codeStr = String.valueOf(NUMPAD_DOWN_CODE);
} else if (keyCode == VK_LEFT) {
codeStr = String.valueOf(NUMPAD_LEFT_CODE);
} else if (keyCode == VK_RIGHT) {
codeStr = String.valueOf(NUMPAD_RIGHT_CODE);
} else if (NUMPAD_DUP_KEYS.contains(keyCode)) {
codeStr = String.valueOf(KEY_LOC_NUMPAD + keyCode);
} else {
codeStr = String.valueOf(keyCode);
}
} else if (keyLocation == LOCATION_LEFT && LEFT_DUP_KEYS.contains(keyCode)) {
codeStr = String.valueOf(KEY_LOC_LEFT + keyCode);
} else {
codeStr = String.valueOf(keyCode);
}
return prefix + codeStr;
}
/**
* Parses a HoD hashKey and optional value.
*/
public static KeyEntry parseHashKey(String hashKey, String value) {
if (hashKey == null || hashKey.length() < 2) {
return null;
}
char prefix = hashKey.charAt(0);
int mods = getModifiersFromPrefix(prefix);
int rawCode;
try {
rawCode = Integer.parseInt(hashKey.substring(1));
} catch (NumberFormatException e) {
return null;
}
int keyCode = rawCode;
int location = LOCATION_STANDARD;
if (rawCode == NUMPAD_UP_CODE) {
keyCode = VK_UP;
location = LOCATION_NUMPAD;
} else if (rawCode == NUMPAD_DOWN_CODE) {
keyCode = VK_DOWN;
location = LOCATION_NUMPAD;
} else if (rawCode == NUMPAD_LEFT_CODE) {
keyCode = VK_LEFT;
location = LOCATION_NUMPAD;
} else if (rawCode == NUMPAD_RIGHT_CODE) {
keyCode = VK_RIGHT;
location = LOCATION_NUMPAD;
} else if (rawCode >= KEY_LOC_NUMPAD) {
keyCode = rawCode - KEY_LOC_NUMPAD;
location = LOCATION_NUMPAD;
} else if (rawCode >= KEY_LOC_LEFT) {
keyCode = rawCode - KEY_LOC_LEFT;
location = LOCATION_LEFT;
}
return new KeyEntry(keyCode, mods, location, value);
}
/**
* Converts a human-readable keystroke expression (e.g. "alt C", "ctrl shift ENTER")
* into a KeyEntry.
*/
public static KeyEntry keyStrokeToEntry(String stroke, String value) {
if (stroke == null || stroke.trim().isEmpty()) return null;
String s = stroke.replace("+", " ").trim();
String[] tokens = s.split("\\s+");
int mods = 0;
int location = LOCATION_STANDARD;
Integer keyCode = null;
for (String t : tokens) {
String lower = t.toLowerCase();
if ("ctrl".equals(lower) || "control".equals(lower)) {
mods |= MOD_CTRL;
} else if ("shift".equals(lower)) {
mods |= MOD_SHIFT;
} else if ("alt".equals(lower)) {
mods |= MOD_ALT;
} else if ("meta".equals(lower) || "cmd".equals(lower)) {
mods |= MOD_META;
} else if ("numpad".equals(lower)) {
location = LOCATION_NUMPAD;
} else {
String upper = t.toUpperCase();
if (NAME_KEYCODES.containsKey(upper)) {
keyCode = NAME_KEYCODES.get(upper);
} else if (upper.startsWith("NUMPAD") && upper.length() > 6) {
location = LOCATION_NUMPAD;
String rest = upper.substring(6);
if (NAME_KEYCODES.containsKey(rest)) {
keyCode = NAME_KEYCODES.get(rest);
}
} else if (upper.startsWith("KEY_")) {
try {
keyCode = Integer.parseInt(upper.substring(4));
} catch (NumberFormatException ignored) {}
}
}
}
if (keyCode == null) return null;
return new KeyEntry(keyCode, mods, location, value);
}
/**
* Parses an IBM HoD .kmp profile.
* Returns a map of j3270 action names to normalized key strokes.
*/
public static Map<String, String> parseHodKmp(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();
// Comments
int cIdx = line.indexOf(';');
if (cIdx >= 0) line = line.substring(0, cIdx).trim();
cIdx = line.indexOf('#');
if (cIdx >= 0) line = line.substring(0, cIdx).trim();
if (line.isEmpty() || line.startsWith("[")) {
continue;
}
int eq = line.indexOf('=');
if (eq <= 0) continue;
String left = line.substring(0, eq).trim();
String right = line.substring(eq + 1).trim();
// Ignore configuration/session properties
if (left.equalsIgnoreCase("sessionType") ||
left.equalsIgnoreCase("codePage") ||
left.equalsIgnoreCase("autoApply") ||
left.equalsIgnoreCase("keyVersion") ||
left.equalsIgnoreCase("disableKeyBuffer") ||
left.equalsIgnoreCase("id") ||
left.equalsIgnoreCase("Description") ||
left.equalsIgnoreCase("Version")) {
continue;
}
// Extended custom function mapping
if (left.startsWith("XF:")) {
continue;
}
// HoD hashKey assignment: e.g. A67=[clear]
if (left.matches("^[A-Za-z][0-9]+$")) {
KeyEntry entry = parseHashKey(left, right);
if (entry != null) {
String stroke = entry.toKeyStrokeString();
String action = resolveMnemonicToAction(right);
if (action != null && stroke != null && !stroke.isEmpty()) {
actionToBindings.computeIfAbsent(action, k -> new ArrayList<>()).add(stroke);
}
}
}
}
Map<String, String> result = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> entry : actionToBindings.entrySet()) {
result.put(entry.getKey(), String.join(", ", entry.getValue()));
}
return result;
}
/**
* Exports key bindings into standard IBM HoD .kmp file format.
*/
public static void exportHodKmp(Writer writer, Map<String, String> keyBindings) throws IOException {
writer.write("[KeyRemap]\n");
writer.write("keyVersion=2\n");
writer.write("sessionType=1\n");
writer.write("codePage=037\n");
writer.write("autoApply=false\n");
writer.write("disableKeyBuffer=false\n");
for (Map.Entry<String, String> entry : keyBindings.entrySet()) {
String action = entry.getKey();
String strokeList = entry.getValue();
if (strokeList == null || strokeList.trim().isEmpty() || "UNBOUND".equalsIgnoreCase(strokeList.trim())) {
continue;
}
String mnemonic = actionToMnemonic(action);
String[] strokes = strokeList.split(",");
for (String stroke : strokes) {
KeyEntry ke = keyStrokeToEntry(stroke.trim(), mnemonic);
if (ke != null) {
writer.write(ke.toHashKey() + "=" + mnemonic + "\n");
}
}
}
writer.flush();
}
/**
* Map HoD mnemonic string (with or without brackets) to standard j3270 action name.
*/
public static String resolveMnemonicToAction(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();
switch (clean) {
case "enter":
case "enterreset":
return "ENTER";
case "newline":
return "NEWLINE";
case "tab":
return "TAB";
case "backtab":
return "shift TAB";
case "reset":
return "ESCAPE";
case "clear":
return "CLEAR";
case "eraseeof":
case "eof":
return "END";
case "erasefld":
case "erinp":
case "eraseinput":
return "ERASE_INPUT";
case "dup":
return "DUP";
case "fieldmark":
case "fldmark":
return "FIELD_MARK";
case "fldext":
case "field-exit":
return "NEWLINE";
case "attn":
return "ATTN";
case "sysreq":
return "SYSREQ";
case "cursel":
return "CURSEL";
case "copy":
return "COPY";
case "paste":
return "PASTE";
case "selectall":
return "SELECTALL";
case "insert":
return "INSERT";
case "delete":
return "DELETE";
case "backspace":
return "BACK_SPACE";
case "home":
return "HOME";
case "end":
return "END";
case "up":
return "UP";
case "down":
return "DOWN";
case "left":
return "LEFT";
case "right":
return "RIGHT";
case "pageup":
return "PAGE_UP";
case "pagedn":
case "pagedown":
return "PAGE_DOWN";
case "docmode":
return "DOCMODE";
case "wordwrap":
return "WORDWRAP";
case "apl":
return "APL";
case "rule":
return "CROSSHAIR_RULER";
case "statusbar":
return "STATUS_BAR";
default:
if (clean.matches("pf[0-9]+")) {
return clean.toUpperCase();
}
if (clean.matches("pa[1-3]")) {
return clean.toUpperCase();
}
if (clean.matches("spf[0-9]+")) {
int n = Integer.parseInt(clean.substring(3));
return "PF" + (n + 12);
}
return clean.toUpperCase();
}
}
/**
* Map j3270 action name to HoD mnemonic token with brackets.
*/
public static String actionToMnemonic(String action) {
if (action == null) return "[]";
String u = action.toUpperCase().trim();
switch (u) {
case "ENTER": return "[enter]";
case "NEWLINE": return "[newline]";
case "TAB": return "[tab]";
case "BACKTAB":
case "SHIFT TAB": return "[backtab]";
case "ESCAPE":
case "RESET": return "[reset]";
case "CLEAR": return "[clear]";
case "END":
case "ERASEEOF": return "[eraseeof]";
case "ERASE_INPUT": return "[erasefld]";
case "DUP": return "[dup]";
case "FIELD_MARK": return "[fieldmark]";
case "ATTN": return "[attn]";
case "SYSREQ": return "[sysreq]";
case "CURSEL": return "[cursel]";
case "COPY": return "[copy]";
case "PASTE": return "[paste]";
case "SELECTALL": return "[selectall]";
case "INSERT": return "[insert]";
case "DELETE": return "[delete]";
case "BACK_SPACE": return "[backspace]";
case "HOME": return "[home]";
case "UP": return "[up]";
case "DOWN": return "[down]";
case "LEFT": return "[left]";
case "RIGHT": return "[right]";
case "PAGE_UP": return "[pageup]";
case "PAGE_DOWN": return "[pagedn]";
case "DOCMODE": return "[docmode]";
case "WORDWRAP": return "[wordwrap]";
case "APL": return "[apl]";
case "CROSSHAIR_RULER": return "[rule]";
case "STATUS_BAR": return "[statusbar]";
default:
if (u.matches("PF[0-9]+")) return "[" + u.toLowerCase() + "]";
if (u.matches("PA[1-3]")) return "[" + u.toLowerCase() + "]";
return "[" + u.toLowerCase() + "]";
}
}
}
@@ -92,6 +92,14 @@ public class InputProcessor {
if (fsm == null || fsm.getConnectionState() == null || !fsm.getConnectionState().isConnected()) {
return OiaStatus.NOT_CONNECTED;
}
if (oia != null) {
if (oia.isXProt()) {
return OiaStatus.X_PROT;
}
if (oia.isXSystem()) {
return OiaStatus.X_SYSTEM;
}
}
if (keyboardLocked) {
return OiaStatus.X_SYSTEM;
}
@@ -112,6 +120,11 @@ public class InputProcessor {
public void setKeyboardLocked(boolean locked) {
if (this.keyboardLocked != locked) {
this.keyboardLocked = locked;
if (!locked && oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
} else if (locked && oia != null && oia.getInputInhibited() == ECLConstants.INHIBIT_NOT_INHIBITED) {
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
}
if (lockStateListener != null) {
lockStateListener.onLockStateChanged(locked);
}
@@ -334,11 +347,17 @@ public class InputProcessor {
}
// Set MDT on field attribute
boolean isLastFieldChar = false;
ExtendedAttribute currentFaEa = null;
if (screen.isFormatted()) {
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
ExtendedAttribute faEa = screen.getCell(faAddr);
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
currentFaEa = screen.getCell(faAddr);
currentFaEa.fa = (byte) (currentFaEa.fa | FA_MODIFY);
}
int nextAddr = screen.incrementAddress(baddr);
if (screen.getCell(nextAddr).isFieldAttribute()) {
isLastFieldChar = true;
}
}
@@ -391,6 +410,11 @@ public class InputProcessor {
screen.markAllChanged();
screen.updateDisplaySnapshot();
// Field Validation: Trigger field (0x04) — automatically send AID upon filling the last character position
if (isLastFieldChar && currentFaEa != null && (currentFaEa.vl & ExtendedAttribute.VL_TRIGGER) != 0) {
sendAid(AID_ENTER);
}
}
/**
@@ -443,14 +467,15 @@ public class InputProcessor {
* - '\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) {
return pasteText(text, enablePasteFromExcel, pasteStopAtProtectedLine, this.blockPaste);
private boolean pasteWordBreak = true;
public boolean isPasteWordBreak() {
return pasteWordBreak;
}
public void setPasteWordBreak(boolean pasteWordBreak) {
this.pasteWordBreak = pasteWordBreak;
}
/**
@@ -459,10 +484,27 @@ public class InputProcessor {
* @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
* @param blockPaste whether newlines align each line with the initial starting cursor column
* @return number of characters pasted
*/
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) {
return pasteText(text, enablePasteFromExcel, pasteStopAtProtectedLine, this.blockPaste, this.pasteWordBreak);
}
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine, boolean blockPaste) {
return pasteText(text, enablePasteFromExcel, pasteStopAtProtectedLine, blockPaste, this.pasteWordBreak);
}
/**
* Pastes text onto the screen with tabular, boundary, block alignment, and word break controls.
*
* @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
* @param blockPaste whether newlines align each line with the initial starting cursor column
* @param pasteWordBreak whether to prevent splitting words across field boundaries
* @return number of characters pasted
*/
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine, boolean blockPaste, boolean pasteWordBreak) {
if (text == null || text.isEmpty() || screen == null || keyboardLocked) {
return 0;
}
@@ -549,9 +591,10 @@ public class InputProcessor {
if (ch >= 0x20 && ch != 0x7F) {
if (screen.isFormatted()) {
int baddr = screen.getCursorAddress();
int totalScreenSize = screen.getRows() * screen.getCols();
ExtendedAttribute ea = screen.getCell(baddr);
if (ea.isFieldAttribute()) {
baddr = (baddr + 1) % (screen.getRows() * screen.getCols());
baddr = (baddr + 1) % totalScreenSize;
}
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) {
@@ -565,6 +608,56 @@ public class InputProcessor {
}
}
}
if (pasteWordBreak) {
int remaining = screen.getFieldRemainingLength(baddr);
int faAddr = screen.findFieldAttribute(baddr);
int startOfField = (faAddr >= 0) ? (faAddr + 1) % totalScreenSize : 0;
if (ch == ' ') {
// If separating space occurs when current field is full, advance to next field
if (remaining <= 0) {
tab();
i++;
continue;
}
// If cursor just transitioned to start of next field after completing a word,
// don't insert a leading delimiter space into the new field
if (baddr == startOfField && i > 0 && !Character.isWhitespace(text.charAt(i - 1))) {
i++;
continue;
}
} else {
// Check if at the start of a word
boolean isWordStart = (i == 0 || Character.isWhitespace(text.charAt(i - 1)));
if (isWordStart) {
int wordEnd = i;
while (wordEnd < len) {
char wc = text.charAt(wordEnd);
if (Character.isWhitespace(wc) || wc == '\t' || wc == '\r' || wc == '\n') {
break;
}
wordEnd++;
}
int wordLen = wordEnd - i;
// If the word cannot fit in remaining space and we're not at start of field,
// advance to next field to prevent splitting the word
if (wordLen > remaining && baddr != startOfField) {
if (pasteStopAtProtectedLine && isLineProtected(screen.getCursorRow())) {
break;
}
tab();
baddr = screen.getCursorAddress();
if (screen.getCell(baddr).isFieldAttribute()) {
baddr = (baddr + 1) % totalScreenSize;
}
if (faIsProtected(screen.getFieldAttributeAt(baddr) & 0xFF)) {
break;
}
}
}
}
}
}
typeCharacter(ch);
@@ -846,6 +939,11 @@ public class InputProcessor {
if (graphicsPlane != null) {
graphicsPlane.clear();
}
lastAid = aidCode;
setKeyboardLocked(true);
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
}
byte[] data = new byte[] { (byte) aidCode };
sendAidResponse(data);
return;
@@ -856,12 +954,22 @@ public class InputProcessor {
return;
}
if (aidCode != AID_CLEAR && aidCode != AID_PA1 && aidCode != AID_PA2 && aidCode != AID_PA3 && aidCode != AID_SYSREQ && aidCode != AID_SELECT) {
if (!validateInput()) {
log.fine("Input validation failed for AID 0x" + Integer.toHexString(aidCode));
return;
}
}
if (insertOffOnAid && insertMode) {
setInsertMode(false);
}
lastAid = aidCode;
setKeyboardLocked(true);
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
}
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
// SSCP-LU Mode: send raw EBCDIC line data without AID or 3270 cursor address
@@ -948,6 +1056,15 @@ public class InputProcessor {
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G and DS3270.
*/
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
boolean hasGraphics = gocaDecoder != null && (gocaDecoder.isGraphicsCursorActive() ||
(gocaDecoder.getGraphicsPlane() != null && gocaDecoder.getGraphicsPlane().hasContent()));
if (hasGraphics) {
int gx = gocaDecoder.getGraphicCursorX();
int gy = gocaDecoder.getGraphicCursorY();
sendGraphicMouseAid(gx, gy, aidCode, button, isShift, isCtrl);
return;
}
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
return;
}
@@ -955,47 +1072,198 @@ public class InputProcessor {
return;
}
setKeyboardLocked(true);
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
int gx = gocaDecoder.getGraphicCursorX();
int gy = gocaDecoder.getGraphicCursorY();
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 24;
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
int row = cursorAddr / cols;
int col = cursorAddr % cols;
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
gx, gy, row, col, button, true, isShift, isCtrl
);
StringBuilder sfHex = new StringBuilder();
for (byte b : sf) {
sfHex.append(String.format("%02X ", b & 0xFF));
}
log.info(String.format(
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d cursorAddr=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
gx, gy, row, col, cursorAddr, button, isShift, isCtrl, sfHex.toString().trim()
));
// Structured Field AID (0x88) + 56-byte Graphic Input SF
out.write(AID_SF);
try {
out.write(sf);
byte[] rma = buildReadModifiedInboundData(aidCode, false);
out.write(rma);
} catch (java.io.IOException ignored) {}
} else {
byte[] rma = buildReadModifiedInboundData(aidCode, false);
try {
out.write(rma);
} catch (java.io.IOException ignored) {}
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] rma = buildReadModifiedInboundData(aidCode, false);
try {
out.write(rma);
} catch (java.io.IOException ignored) {}
sendAidResponse(out.toByteArray());
}
/**
* Transmits a mouse / light-pen graphic input structured field AID with explicit GOCA coordinates.
*/
public void sendGraphicMouseAid(int gx, int gy, int aidCode, int button, boolean isShift, boolean isCtrl) {
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
return;
}
if (isKeyboardLocked()) {
return;
}
setKeyboardLocked(true);
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
int row = cursorAddr / cols;
int col = cursorAddr % cols;
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
gx, gy, row, col, button, true, isShift, isCtrl
);
StringBuilder sfHex = new StringBuilder();
for (byte b : sf) {
sfHex.append(String.format("%02X ", b & 0xFF));
}
log.info(String.format(
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d cursorAddr=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
gx, gy, row, col, cursorAddr, button, isShift, isCtrl, sfHex.toString().trim()
));
// Structured Field AID (0x88) + 56-byte Graphic Input SF
out.write(AID_SF);
try {
out.write(sf);
byte[] rma = buildReadModifiedInboundData(aidCode, false);
out.write(rma);
} catch (java.io.IOException ignored) {}
sendAidResponse(out.toByteArray());
}
// ========== 3270 EDS Field Validation (0xC1 / TY_FVALIDATE) ==========
/**
* Enforce 3270 Extended Data Stream field validation attributes (0xC1 / TY_FVALIDATE)
* prior to transmitting AID keys (Mandatory Fill: 0x01, Mandatory Entry: 0x02).
*
* @return true if all fields pass validation, false if validation failed and input inhibited
*/
public boolean validateInput() {
if (screen == null || !screen.isFormatted()) {
return true;
}
int size = screen.getRows() * screen.getCols();
if (size <= 0) return true;
for (int i = 0; i < size; i++) {
ExtendedAttribute faCell = screen.getCell(i);
if (!faCell.isFieldAttribute()) continue;
int vl = faCell.vl & 0xFF;
if (vl == 0) continue;
// Only validate unprotected input fields
if (faIsProtected(faCell.fa & 0xFF)) continue;
int fieldStart = (i + 1) % size;
int totalChars = 0;
int filledChars = 0;
int firstEmptyPos = -1;
int p = fieldStart;
while (p != i && !screen.getCell(p).isFieldAttribute()) {
ExtendedAttribute cell = screen.getCell(p);
totalChars++;
int ec = cell.ec & 0xFF;
char ucs = cell.ucs4;
boolean isFilled = (ec != 0x00 && ec != 0x40 && ucs != 0 && ucs != ' ' && ucs != '\u3000');
if (isFilled) {
filledChars++;
} else if (firstEmptyPos < 0) {
firstEmptyPos = p;
}
p = (p + 1) % size;
}
if (totalChars == 0) continue;
boolean isModified = faIsModified(faCell.fa & 0xFF);
boolean mandatoryFill = (vl & ExtendedAttribute.VL_MANDATORY_FILL) != 0;
boolean mandatoryEntry = (vl & ExtendedAttribute.VL_MANDATORY_ENTRY) != 0;
// Mandatory Entry (0x02): Field must contain at least one user-entered character
if (mandatoryEntry && filledChars == 0) {
failValidation(fieldStart);
return false;
}
// Mandatory Fill (0x01): Field must be completely filled with characters (no nulls/blanks).
// If the field is modified, has characters entered, or has Mandatory Entry also set:
if (mandatoryFill) {
if (mandatoryEntry || isModified || filledChars > 0) {
if (filledChars < totalChars) {
failValidation(firstEmptyPos >= 0 ? firstEmptyPos : fieldStart);
return false;
}
}
}
}
return true;
}
/**
* Validates the field at the current cursor position when the user attempts to tab away.
* If the field has Mandatory Fill (0x01) and is partially filled, inhibits keyboard with input error.
*/
public boolean validateCurrentFieldOnExit() {
if (screen == null || !screen.isFormatted()) {
return true;
}
int curAddr = screen.getCursorAddress();
int faPos = screen.findFieldAttribute(curAddr);
if (faPos < 0) return true;
ExtendedAttribute faCell = screen.getCell(faPos);
int vl = faCell.vl & 0xFF;
if ((vl & ExtendedAttribute.VL_MANDATORY_FILL) == 0) {
return true;
}
if (faIsProtected(faCell.fa & 0xFF)) {
return true;
}
int size = screen.getRows() * screen.getCols();
int fieldStart = (faPos + 1) % size;
int totalChars = 0;
int filledChars = 0;
int firstEmptyPos = -1;
int p = fieldStart;
while (p != faPos && !screen.getCell(p).isFieldAttribute()) {
ExtendedAttribute cell = screen.getCell(p);
totalChars++;
int ec = cell.ec & 0xFF;
char ucs = cell.ucs4;
boolean isFilled = (ec != 0x00 && ec != 0x40 && ucs != 0 && ucs != ' ' && ucs != '\u3000');
if (isFilled) {
filledChars++;
} else if (firstEmptyPos < 0) {
firstEmptyPos = p;
}
p = (p + 1) % size;
}
// If partially filled (incomplete):
if (filledChars > 0 && filledChars < totalChars) {
failValidation(firstEmptyPos >= 0 ? firstEmptyPos : curAddr);
return false;
}
return true;
}
private void failValidation(int errorCursorPos) {
if (errorCursorPos >= 0 && screen != null) {
screen.setCursorAddress(errorCursorPos);
screen.updateDisplaySnapshot();
}
if (oia != null) {
oia.setInputInhibited(ECLConstants.INHIBIT_OPERATOR_DUE);
}
setKeyboardLocked(true);
if (bellListener != null) {
bellListener.onBell();
}
}
// ========== Cursor movement ==========
/**
@@ -1030,7 +1298,12 @@ public class InputProcessor {
screen.setCursorAddress(address);
// Immediate Selection (Space or Null) -> AID_SELECT (0x7E)
// Per IBM 3270 Reference (GA23-0059), light-pen selection has no effect
// on protected fields with a space or null designator character.
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || ascii == '\u3000') {
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa)) {
return false;
}
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
return true;
@@ -1118,6 +1391,9 @@ public class InputProcessor {
screen.processWordTab(true);
return;
}
if (!validateCurrentFieldOnExit()) {
return;
}
int addr = screen.findNextUnprotected(screen.getCursorAddress());
screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
@@ -1137,6 +1413,9 @@ public class InputProcessor {
return;
}
if (!screen.isFormatted()) return;
if (!validateCurrentFieldOnExit()) {
return;
}
int size = screen.getRows() * screen.getCols();
if (size <= 0) return;
int addr = screen.getCursorAddress();
@@ -1318,14 +1597,13 @@ public class InputProcessor {
screen.updateDisplaySnapshot();
}
/** Attention key (sends Telnet Interrupt Process + IAC EOR break signal). */
/** Attention key (sends Telnet Interrupt Process or PA1/BREAK signal). */
public void processAttn() {
if (insertOffOnAid && insertMode) {
setInsertMode(false);
}
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
byte[] ipEor = new byte[] {
(byte) TelnetConstants.IAC, (byte) TelnetConstants.IP,
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
};
fsm.sendRecord(ipEor);
fsm.handleAttn();
}
}
@@ -1550,11 +1828,110 @@ public class InputProcessor {
public void processBackspace() { backspace(); }
public void processEraseEOF() { eraseEof(); }
public void processEraseInput() { eraseInput(); }
public void processEraseField() { eraseField(); }
public void processDup() { dup(); }
public void processFieldMark() { fieldMark(); }
public void processToggleInsert() { setInsertMode(!isInsertMode()); }
public boolean processCurSel() { return cursorSelect(); }
public boolean processCursorSelect() { return cursorSelect(); }
public boolean processSelFld() { return selectField(); }
public boolean processDeSelFld() { return deselectField(); }
public void processFieldStart() {
if (screen == null) return;
int cur = screen.getCursorAddress();
if (screen.isFormatted()) {
int faPos = screen.findFieldAttribute(cur);
if (faPos >= 0) {
int fieldStart = screen.incrementAddress(faPos);
screen.setCursorAddress(fieldStart);
}
} else {
int cols = screen.getCols();
int row = cur / cols;
screen.setCursorAddress(row * cols);
}
screen.updateDisplaySnapshot();
}
public void eraseField() {
if (screen == null) return;
if (!screen.isFormatted()) {
eraseEof();
return;
}
int cur = screen.getCursorAddress();
int faPos = screen.findFieldAttribute(cur);
if (faPos < 0) return;
byte faVal = screen.getCell(faPos).fa;
if (faIsProtected(faVal & 0xFF)) return;
int fieldStart = screen.incrementAddress(faPos);
int pos = fieldStart;
int size = screen.getRows() * screen.getCols();
int count = 0;
while (!screen.getCell(pos).isFieldAttribute() && count < size) {
ExtendedAttribute ea = screen.getCell(pos);
ea.ec = 0;
ea.ucs4 = 0;
pos = screen.incrementAddress(pos);
count++;
}
screen.getCell(faPos).fa = (byte) (screen.getCell(faPos).fa | FA_MODIFY);
screen.setCursorAddress(fieldStart);
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
public boolean selectField() {
if (screen == null || !screen.isFormatted()) return false;
int cur = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
int faPos = screen.findFieldAttribute(cur);
if (faPos < 0) return false;
ExtendedAttribute faCell = screen.getCell(faPos);
int fa = faCell.fa & 0xFF;
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) return false;
int desPos = (faPos + 1) % size;
ExtendedAttribute desCell = screen.getCell(desPos);
int ebcdic = desCell.ec & 0xFF;
char ascii = (char) desCell.ucs4;
if (ebcdic == 0x6F || ascii == '?' || ascii == '\uFF1F') {
desCell.ec = (byte) 0x6E;
desCell.ucs4 = '>';
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.markAllChanged();
screen.updateDisplaySnapshot();
return true;
}
return false;
}
public boolean deselectField() {
if (screen == null || !screen.isFormatted()) return false;
int cur = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
int faPos = screen.findFieldAttribute(cur);
if (faPos < 0) return false;
ExtendedAttribute faCell = screen.getCell(faPos);
int fa = faCell.fa & 0xFF;
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) return false;
int desPos = (faPos + 1) % size;
ExtendedAttribute desCell = screen.getCell(desPos);
int ebcdic = desCell.ec & 0xFF;
char ascii = (char) desCell.ucs4;
if (ebcdic == 0x6E || ascii == '>' || ascii == '\uFF1E') {
desCell.ec = (byte) 0x6F;
desCell.ucs4 = '?';
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.markAllChanged();
screen.updateDisplaySnapshot();
return true;
}
return false;
}
// ========== File Transfer Support ==========
@@ -1667,34 +2044,46 @@ public class InputProcessor {
/**
* Parse and execute IBM ECL standard mnemonic keystrokes (e.g. "USER[tab]PASS[enter]").
* Fully compatible with IBM Host On-Demand SendKeyTokenizer and 120+ standard mnemonics.
*/
public void sendKeys(String keys) {
if (keys == null || keys.isEmpty()) return;
int len = keys.length();
int i = 0;
while (i < len) {
char c = keys.charAt(i);
if (c == '[') {
int end = keys.indexOf(']', i);
if (end > i) {
String token = keys.substring(i + 1, end).trim().toLowerCase();
try {
haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer tokenizer =
new haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer(keys);
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (token == null) break;
if (token.length() == 1) {
typeCharacter(token.charAt(0));
} else if (token.startsWith("[") && token.endsWith("]")) {
executeMnemonicToken(token);
i = end + 1;
continue;
} else {
for (int i = 0; i < token.length(); i++) {
typeCharacter(token.charAt(i));
}
}
}
typeCharacter(c);
i++;
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
log.warning("Syntax error in sendKeys: " + e.getMessage());
}
}
private void executeMnemonicToken(String token) {
public void executeMnemonicToken(String rawToken) {
if (rawToken == null || rawToken.isEmpty()) return;
String token = rawToken.trim().toLowerCase();
if (token.startsWith("[") && token.endsWith("]") && token.length() >= 2) {
token = token.substring(1, token.length() - 1).trim();
}
if (isNvtMode()) {
try {
switch (token) {
case "enter":
case "return":
case "keypadenter":
fsm.sendNVTString("\r\n");
break;
case "clear":
@@ -1715,6 +2104,8 @@ public class InputProcessor {
fsm.sendNVTString("\u001B[H");
break;
case "end":
case "eof":
case "fieldend":
fsm.sendNVTString("\u001B[F");
break;
case "up":
@@ -1737,6 +2128,7 @@ public class InputProcessor {
case "pgup":
fsm.sendNVTString("\u001B[5~");
break;
case "pagedn":
case "pagedown":
case "pgdn":
fsm.sendNVTString("\u001B[6~");
@@ -1751,6 +2143,7 @@ public class InputProcessor {
break;
case "attn":
case "break":
case "telnetbreak":
fsm.sendNVTChar('\u0003');
break;
case "sysreq":
@@ -1761,10 +2154,36 @@ public class InputProcessor {
case "reset":
setKeyboardLocked(false);
break;
case "enterreset":
if (keyboardLocked) {
setKeyboardLocked(false);
} else {
fsm.sendNVTString("\r\n");
}
break;
case "keypad0": fsm.sendNVTChar('0'); break;
case "keypad1": fsm.sendNVTChar('1'); break;
case "keypad2": fsm.sendNVTChar('2'); break;
case "keypad3": fsm.sendNVTChar('3'); break;
case "keypad4": fsm.sendNVTChar('4'); break;
case "keypad5": fsm.sendNVTChar('5'); break;
case "keypad6": fsm.sendNVTChar('6'); break;
case "keypad7": fsm.sendNVTChar('7'); break;
case "keypad8": fsm.sendNVTChar('8'); break;
case "keypad9": fsm.sendNVTChar('9'); break;
case "keypad.": fsm.sendNVTChar('.'); break;
case "keypad,": fsm.sendNVTChar(','); break;
case "keypad-": fsm.sendNVTChar('-'); break;
default:
if (token.startsWith("pf") || token.startsWith("f")) {
if (token.length() == 1 && Character.isDigit(token.charAt(0))) {
fsm.sendNVTChar(token.charAt(0));
} else if (token.startsWith("pf") || token.startsWith("f") || token.startsWith("vt-pf") || token.startsWith("spf")) {
try {
String numStr = token.startsWith("pf") ? token.substring(2) : token.substring(1);
String numStr = token;
if (numStr.startsWith("vt-pf")) numStr = numStr.substring(5);
else if (numStr.startsWith("spf")) numStr = numStr.substring(3);
else if (numStr.startsWith("pf")) numStr = numStr.substring(2);
else if (numStr.startsWith("f")) numStr = numStr.substring(1);
int fn = Integer.parseInt(numStr);
sendNvtFunctionKey(fn);
} catch (NumberFormatException ignored) {}
@@ -1815,11 +2234,36 @@ public class InputProcessor {
case "curright":
cursorRight();
break;
case "pagedn":
case "pagedown":
case "pgdn":
sendAid(AID_PF8);
break;
case "pageup":
case "pgup":
sendAid(AID_PF7);
break;
case "eof":
case "fieldend":
case "end":
processFieldEnd();
break;
case "bof":
case "fieldstart":
processFieldStart();
break;
case "eraseeof":
case "erase_eof":
eraseEof();
break;
case "erasefld":
case "erasefield":
case "erase_field":
eraseField();
break;
case "erinp":
case "eraseinp":
case "eraseinput":
case "erase_input":
eraseInput();
break;
@@ -1830,7 +2274,24 @@ public class InputProcessor {
case "fieldmark":
fieldMark();
break;
case "fldext":
case "fieldexit":
case "field+":
case "field-":
tab();
break;
case "+cr":
if (graphicsPlane != null) {
if (graphicsPlane.isGraphicCursorAttached()) {
graphicsPlane.detachGraphicCursor();
} else {
graphicsPlane.attachGraphicCursor(graphicsPlane.getGraphicCursorX(), graphicsPlane.getGraphicCursorY());
}
}
break;
case "attn":
case "break":
case "telnetbreak":
attn();
break;
case "sysreq":
@@ -1839,10 +2300,18 @@ public class InputProcessor {
case "reset":
reset();
break;
case "enterreset":
if (keyboardLocked) {
reset();
} else {
sendAid(AID_ENTER);
}
break;
case "insert":
setInsertMode(!isInsertMode());
break;
case "delete":
case "del":
deleteChar();
break;
case "backspace":
@@ -1850,9 +2319,13 @@ public class InputProcessor {
backspace();
break;
case "wordtab":
case "tabword":
case "nextword":
processWordTab(true);
break;
case "wordbacktab":
case "backtabword":
case "previousword":
processWordTab(false);
break;
case "wordleft":
@@ -1861,10 +2334,6 @@ public class InputProcessor {
case "wordright":
processWordRight();
break;
case "fieldend":
case "end":
processFieldEnd();
break;
case "deleteword":
processDeleteWord();
break;
@@ -1873,6 +2342,12 @@ public class InputProcessor {
case "select":
cursorSelect();
break;
case "selfld":
selectField();
break;
case "deselfld":
deselectField();
break;
case "lightpen":
processLightPen();
break;
@@ -1892,8 +2367,37 @@ public class InputProcessor {
case "aplmode":
toggleAplKeyboardMode();
break;
case "altview":
if (screen != null) {
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
break;
case "dspsosi":
if (screen != null) {
screen.balanceSOSI();
screen.markAllChanged();
screen.updateDisplaySnapshot();
}
break;
case "keypad0": typeCharacter('0'); break;
case "keypad1": typeCharacter('1'); break;
case "keypad2": typeCharacter('2'); break;
case "keypad3": typeCharacter('3'); break;
case "keypad4": typeCharacter('4'); break;
case "keypad5": typeCharacter('5'); break;
case "keypad6": typeCharacter('6'); break;
case "keypad7": typeCharacter('7'); break;
case "keypad8": typeCharacter('8'); break;
case "keypad9": typeCharacter('9'); break;
case "keypad.": typeCharacter('.'); break;
case "keypad,": typeCharacter(','); break;
case "keypad-": typeCharacter('-'); break;
case "keypadenter": sendAid(AID_ENTER); break;
default:
if (token.startsWith("pf")) {
if (token.length() == 1 && Character.isDigit(token.charAt(0))) {
typeCharacter(token.charAt(0));
} else if (token.startsWith("pf")) {
try {
int pfNum = Integer.parseInt(token.substring(2));
if (pfNum >= 1 && pfNum <= 24) {
@@ -1907,6 +2411,24 @@ public class InputProcessor {
sendAid(AID_PA1 + (paNum - 1));
}
} catch (NumberFormatException ignored) {}
} else if (token.startsWith("spf")) {
try {
int spfNum = Integer.parseInt(token.substring(3));
if (spfNum >= 1 && spfNum <= 24) {
sendAid(AID_PF1 + (spfNum - 1));
}
} catch (NumberFormatException ignored) {}
} else if (token.startsWith("vt-pf")) {
try {
int vpfNum = Integer.parseInt(token.substring(5));
if (vpfNum >= 1 && vpfNum <= 24) {
sendAid(AID_PF1 + (vpfNum - 1));
}
} catch (NumberFormatException ignored) {}
} else {
// Recognized HoD Bidi / Layer / Print / GUI event tokens:
// log or no-op safely without throwing unknown errors
log.fine("Mnemonic token received: " + token);
}
break;
}
@@ -0,0 +1,236 @@
package haus.nightmare.lib3270j.macro;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ecl.ECLSession;
import haus.nightmare.lib3270j.macro.action.MacroAction;
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
import haus.nightmare.lib3270j.macro.variable.MacroVariables;
/**
* High-level Macro engine matching IBM Host On-Demand Macro.
* Coordinates loading, variable scopes, screen state transitions, and background PlayThread execution.
*/
public class Macro implements Serializable {
private static final long serialVersionUID = 1L;
public static final int STATE_STOPPED = 1;
public static final int STATE_PLAYING = 2;
public static final int STATE_PAUSED = 4;
public static final int STATE_ERROR = 8;
public interface MacroListener {
void macroStateChanged(Macro macro, int oldState, int newState);
void macroScreenMatching(MacroScreen screen);
void macroActionExecuting(MacroAction action);
void macroMessage(String title, String message);
void macroError(String error);
}
public interface MacroPromptHandler {
String prompt(MacroActionPrompt prompt);
}
private MacroScreens macScreens = new MacroScreens();
private ECLSession eclSess;
private Telnet3270Client client;
private PlayThread playThread;
private int state = STATE_STOPPED;
private MacroPromptHandler promptHandler;
private final List<MacroListener> listeners = new CopyOnWriteArrayList<>();
private final Map<String, Macro> chainedMacros = new HashMap<>();
public Macro() {}
public Macro(MacroScreens screens) {
setScreens(screens);
}
public Macro(ECLSession session) {
this.eclSess = session;
}
public Macro(Telnet3270Client client) {
setClient(client);
}
public void setScreens(MacroScreens screens) {
this.macScreens = (screens != null) ? screens : new MacroScreens();
}
public MacroScreens getScreens() {
return macScreens;
}
public void setSession(ECLSession session) {
this.eclSess = session;
}
public ECLSession getECLSession() {
return eclSess;
}
public void setClient(Telnet3270Client client) {
this.client = client;
if (client != null && this.eclSess == null) {
this.eclSess = new ECLSession(client);
}
}
public Telnet3270Client getClient() {
return client;
}
public MacroVariables getVariables() {
return (macScreens != null) ? macScreens.getVariables() : null;
}
public boolean isUseVars() {
return macScreens != null && macScreens.isUseVars();
}
public int getState() {
return state;
}
public synchronized void setState(int newState) {
if (this.state != newState) {
int old = this.state;
this.state = newState;
for (MacroListener l : listeners) {
try {
l.macroStateChanged(this, old, newState);
} catch (Throwable ignored) {}
}
}
}
public void setPromptHandler(MacroPromptHandler handler) {
this.promptHandler = handler;
}
public MacroPromptHandler getPromptHandler() {
return promptHandler;
}
public void addMacroListener(MacroListener listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
}
}
public void removeMacroListener(MacroListener listener) {
listeners.remove(listener);
}
public void load(String xml) {
setScreens(MacroXmlParser.parse(xml));
}
public synchronized void play() {
play(null);
}
public synchronized void play(String screenName) {
if (state == STATE_PLAYING) {
return;
}
if (state == STATE_PAUSED && playThread != null) {
resume();
return;
}
stop();
if (eclSess == null && client != null) {
eclSess = new ECLSession(client);
}
if (macScreens.getVariables() != null) {
macScreens.getVariables().reset();
}
playThread = new PlayThread(this, macScreens, eclSess, screenName);
playThread.start();
}
public synchronized void pause() {
if (state == STATE_PLAYING && playThread != null) {
playThread.pausePlay();
setState(STATE_PAUSED);
}
}
public synchronized void resume() {
if (state == STATE_PAUSED && playThread != null) {
playThread.resumePlay();
setState(STATE_PLAYING);
}
}
public synchronized void stop() {
if (playThread != null) {
playThread.halt();
playThread = null;
}
setState(STATE_STOPPED);
}
public void halt() {
stop();
}
public void playChainedMacro(String name, String startScreen) {
Macro chained = chainedMacros.get(name);
if (chained != null) {
chained.setSession(this.eclSess);
chained.setClient(this.client);
chained.setPromptHandler(this.promptHandler);
chained.play(startScreen);
} else {
logError("Chained macro not found: " + name);
}
}
public void registerChainedMacro(String name, Macro macro) {
if (name != null && macro != null) {
chainedMacros.put(name, macro);
}
}
public void notifyScreenMatching(MacroScreen screen) {
for (MacroListener l : listeners) {
try {
l.macroScreenMatching(screen);
} catch (Throwable ignored) {}
}
}
public void notifyActionExecuting(MacroAction action) {
for (MacroListener l : listeners) {
try {
l.macroActionExecuting(action);
} catch (Throwable ignored) {}
}
}
public void notifyMessage(String title, String message) {
for (MacroListener l : listeners) {
try {
l.macroMessage(title, message);
} catch (Throwable ignored) {}
}
}
public void logError(String error) {
for (MacroListener l : listeners) {
try {
l.macroError(error);
} catch (Throwable ignored) {}
}
}
}
@@ -0,0 +1,101 @@
package haus.nightmare.lib3270j.macro;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import haus.nightmare.lib3270j.macro.action.MacroAction;
/**
* Collection of MacroAction items matching IBM Host On-Demand MacroActions.
*/
public class MacroActions implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private final List<MacroAction> actions = new ArrayList<>();
private boolean promptAll = false;
public MacroActions() {}
public MacroActions(List<MacroAction> actions) {
if (actions != null) {
this.actions.addAll(actions);
}
}
public void add(MacroAction action) {
if (action != null) {
actions.add(action);
}
}
public void add(int index, MacroAction action) {
if (action != null) {
actions.add(index, action);
}
}
public MacroAction get(int index) {
return actions.get(index);
}
public int size() {
return actions.size();
}
public boolean isEmpty() {
return actions.isEmpty();
}
public void clear() {
actions.clear();
}
public boolean remove(MacroAction action) {
return actions.remove(action);
}
public MacroAction remove(int index) {
return actions.remove(index);
}
public List<MacroAction> getActions() {
return Collections.unmodifiableList(actions);
}
public boolean isPromptAll() {
return promptAll;
}
public void setPromptAll(boolean promptAll) {
this.promptAll = promptAll;
}
public String format(int indent) {
StringBuilder sb = new StringBuilder();
String ind = "";
for (int i = 0; i < indent; i++) ind += " ";
sb.append(ind).append("<actions");
if (promptAll) sb.append(" promptall=\"true\"");
sb.append(">\n");
for (MacroAction act : actions) {
sb.append(act.format(indent + 1)).append("\n");
}
sb.append(ind).append("</actions>");
return sb.toString();
}
@Override
public MacroActions clone() {
MacroActions clone = new MacroActions();
clone.promptAll = this.promptAll;
for (MacroAction act : actions) {
clone.add(act.clone());
}
return clone;
}
}

Some files were not shown because too many files have changed in this diff Show More