13 Commits

Author SHA1 Message Date
rudi 0201c7c277 Fix Read Modified Query from Moshix bug report
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 50s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m24s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m38s
Release j3270 / Build & Publish Release (push) Successful in 1m38s
2026-09-24 17:34:43 +00:00
rudi b61702a57c Fix Read Modified Query from Moshix bug report
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 42s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 52s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m42s
Release j3270 / Build & Publish Release (push) Failing after 1m24s
2026-09-24 17:03:38 +00:00
rudi bd4c1f3040 Add Read Modified Query from Moshix bug report
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m19s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m24s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m43s
Release j3270 / Build & Publish Release (push) Successful in 39s
2026-09-24 15:35:08 +00:00
rudi 0d6ab31f0a Connection storage
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 50s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m34s
2026-09-24 12:44:50 +00:00
rudi 1f0e36c561 Connection hardening and FileTransfer UI changes
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m20s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m32s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 2m7s
2026-09-24 04:15:36 +00:00
rudi 8286af19e6 IND$FILE Browse!
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 36s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m27s
2026-09-24 01:28:55 +00:00
rudi 3d3a939ece Codepage, ECL, and some UI updates
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m21s
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 1m29s
2026-09-22 13:38:12 +00:00
rudi d05f1a4c45 Resize debugging
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 39s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 47s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 1m36s
2026-09-21 16:26:13 +00:00
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
179 changed files with 24151 additions and 1062 deletions
@@ -27,6 +27,30 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private static final Logger log = Logger.getLogger(J3270App.class.getName()); private static final Logger log = Logger.getLogger(J3270App.class.getName());
// Active open windows tracking
private static final java.util.List<J3270App> openWindows = new java.util.concurrent.CopyOnWriteArrayList<>();
public static java.util.List<J3270App> getOpenWindows() {
return java.util.Collections.unmodifiableList(openWindows);
}
@FunctionalInterface
public interface ConnectChoiceHandler {
ConnectChoiceDialog.Choice askChoice(Window owner, String currentTarget);
}
private ConnectChoiceHandler connectChoiceHandler = null;
public void setConnectChoiceHandler(ConnectChoiceHandler handler) {
this.connectChoiceHandler = handler;
}
public ConnectChoiceHandler getConnectChoiceHandler() {
return connectChoiceHandler;
}
private final java.util.function.Consumer<UITheme> themeChangeListener = this::onThemeChanged;
private Telnet3270Client client; private Telnet3270Client client;
private TerminalPanel terminalPanel; private TerminalPanel terminalPanel;
private StatusBar statusBar; private StatusBar statusBar;
@@ -42,8 +66,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// Dialog instances // Dialog instances
private FindDialog findDialog; private FindDialog findDialog;
private ScriptDialog scriptDialog; private ScriptDialog scriptDialog;
private MacroDialog macroDialog;
private FieldInspectorDialog fieldInspectorDialog; private FieldInspectorDialog fieldInspectorDialog;
private PrinterSessionDialog printerSessionDialog; private PrinterSessionDialog printerSessionDialog;
private ScreenHistoryDialog screenHistoryDialog;
// Modes menu items // Modes menu items
private JCheckBoxMenuItem docModeItem; private JCheckBoxMenuItem docModeItem;
@@ -58,14 +84,16 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
public J3270App() { public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator"); super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
setBackground(Color.BLACK); setBackground(Color.BLACK);
AppIcon.applyTo(this); AppIcon.applyTo(this);
openWindows.add(this);
buildUI(); buildUI();
buildMenuBar(); buildMenuBar();
ThemeManager.addThemeChangeListener(this::onThemeChanged); ThemeManager.addThemeChangeListener(themeChangeListener);
pack(); pack();
setLocationRelativeTo(null); setLocationRelativeTo(null);
@@ -76,7 +104,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (client != null) { if (client != null) {
statusBar.updateStatus(); statusBar.updateStatus();
syncModeMenuItems(); syncModeMenuItems();
if (!isAnyChildDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) { if (!isModalDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) {
if (!terminalPanel.hasFocus()) { if (!terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocusInWindow();
} }
@@ -85,38 +113,62 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}); });
refreshTimer.start(); refreshTimer.start();
setAutoRequestFocus(true);
addWindowListener(new WindowAdapter() { addWindowListener(new WindowAdapter() {
@Override @Override
public void windowClosing(WindowEvent e) { public void windowClosing(WindowEvent e) {
disconnect(); closeWindow();
if (refreshTimer != null)
refreshTimer.stop();
if (printerSessionDialog != null) {
printerSessionDialog.dispose();
}
terminalPanel.dispose();
} }
@Override @Override
public void windowActivated(WindowEvent e) { public void windowActivated(WindowEvent e) {
if (!isAnyChildDialogActive()) { if (!isModalDialogActive() && terminalPanel != null) {
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocus();
} }
} }
}); });
addWindowFocusListener(new WindowAdapter() {
@Override
public void windowGainedFocus(WindowEvent e) {
if (!isModalDialogActive() && terminalPanel != null) {
terminalPanel.requestFocus();
}
}
});
// 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 && !isModalDialogActive()) {
if (!J3270App.this.isFocused() || !J3270App.this.isActive()) {
J3270App.this.toFront();
}
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.requestFocus();
SwingUtilities.invokeLater(() -> {
if (terminalPanel != null && !terminalPanel.hasFocus()) {
terminalPanel.requestFocus();
}
});
}
}
}
}
}, AWTEvent.MOUSE_EVENT_MASK);
} }
private boolean isAnyChildDialogActive() { private boolean isModalDialogActive() {
Window focusedWindow = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
if (focusedWindow != null && focusedWindow != this) {
return true;
}
if (findDialog != null && findDialog.isVisible()) return true;
if (scriptDialog != null && scriptDialog.isVisible()) return true;
if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
for (Window owned : getOwnedWindows()) { for (Window owned : getOwnedWindows()) {
if (owned != null && owned.isVisible()) { if (owned instanceof Dialog && owned.isVisible() && ((Dialog) owned).isModal()) {
return true; return true;
} }
} }
@@ -167,6 +219,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog)); fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect)); fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
fileMenu.addSeparator(); fileMenu.addSeparator();
fileMenu.add(createMenuItem("Close Window", KeyEvent.VK_W, this::closeWindow));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel))); fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel)));
fileMenu.add(createMenuItem("3287 Printer Session...", -1, this::showPrinterSessionDialog)); fileMenu.add(createMenuItem("3287 Printer Session...", -1, this::showPrinterSessionDialog));
fileMenu.add(createMenuItem("Print Screen...", KeyEvent.VK_P, this::printScreen)); fileMenu.add(createMenuItem("Print Screen...", KeyEvent.VK_P, this::printScreen));
@@ -174,13 +228,51 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog)); fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
fileMenu.addSeparator(); fileMenu.addSeparator();
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> { fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
disconnect(); for (J3270App win : new java.util.ArrayList<>(openWindows)) {
win.disconnect();
win.dispose();
}
System.exit(0); System.exit(0);
})); }));
menuBar.add(fileMenu); menuBar.add(fileMenu);
// 2. Edit menu // 2. Connection menu
JMenu connectionMenu = createMenu("Connection");
connectionMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
connectionMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
connectionMenu.addSeparator();
// Host Type Submenu (CMS/TSO/CICS)
JMenu hostTypeMenu = createMenu("Host Type");
ButtonGroup hostTypeGroup = new ButtonGroup();
for (haus.nightmare.lib3270j.ft.FTConfig.HostType ht : haus.nightmare.lib3270j.ft.FTConfig.HostType.values()) {
JRadioButtonMenuItem htItem = new JRadioButtonMenuItem(ht.name());
ThemeManager.styleMenuItem(htItem);
hostTypeGroup.add(htItem);
htItem.addActionListener(e -> setConnectionHostType(ht));
hostTypeMenu.add(htItem);
}
connectionMenu.add(hostTypeMenu);
connectionMenu.addSeparator();
// Saved Connections Submenu
JMenu savedHostsMenu = createMenu("Saved Connections");
connectionMenu.add(savedHostsMenu);
connectionMenu.addMenuListener(new javax.swing.event.MenuListener() {
@Override
public void menuSelected(javax.swing.event.MenuEvent e) {
updateConnectionMenu(hostTypeMenu, hostTypeGroup, savedHostsMenu);
}
@Override public void menuDeselected(javax.swing.event.MenuEvent e) {}
@Override public void menuCanceled(javax.swing.event.MenuEvent e) {}
});
menuBar.add(connectionMenu);
// 3. Edit menu
JMenu editMenu = createMenu("Edit"); 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("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection()));
editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard())); editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard()));
editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog)); editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog));
@@ -312,6 +404,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
viewMenu.add(gfxMenu); viewMenu.add(gfxMenu);
viewMenu.addSeparator(); viewMenu.addSeparator();
viewMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog)); viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
menuBar.add(viewMenu); menuBar.add(viewMenu);
@@ -430,6 +523,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}, true)); }, true));
actionsMenu.addSeparator(); actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog)); 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)); actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
menuBar.add(actionsMenu); menuBar.add(actionsMenu);
@@ -536,6 +631,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
scriptDialog.setVisible(true); scriptDialog.setVisible(true);
} }
private void showMacroDialog() {
if (macroDialog == null) {
macroDialog = new MacroDialog(this, client, terminalPanel);
}
macroDialog.setVisible(true);
}
private void showFieldInspectorDialog() { private void showFieldInspectorDialog() {
if (client == null) { if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE); JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
@@ -559,6 +661,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
printerSessionDialog.setVisible(true); 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() { private void showPasteLineWrapDialog() {
if (client == null || !client.getConnectionState().isFullSession()) { if (client == null || !client.getConnectionState().isFullSession()) {
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE); JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE);
@@ -685,7 +799,20 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// ========== Connection management ========== // ========== Connection management ==========
private void showConnectDialog() { public void showConnectDialog() {
boolean wasConnected = isConnected();
if (wasConnected && (!GraphicsEnvironment.isHeadless() || connectChoiceHandler != null)) {
ConnectChoiceDialog.Choice choice = promptConnectConflict();
if (choice == ConnectChoiceDialog.Choice.NEW_WINDOW) {
J3270App newWindow = createNewWindow(null);
SwingUtilities.invokeLater(newWindow::showConnectDialog);
return;
} else if (choice == ConnectChoiceDialog.Choice.CANCEL) {
return;
}
// choice == ABORT_CURRENT: proceed to showConnectDialog in this window
}
ConnectDialog dialog = new ConnectDialog(this); ConnectDialog dialog = new ConnectDialog(this);
String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : haus.nightmare.j3270.config.Settings.getAutoConnectHost(); String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : haus.nightmare.j3270.config.Settings.getAutoConnectHost();
int initialPort = lastPort > 0 ? lastPort : haus.nightmare.j3270.config.Settings.getAutoConnectPort(); int initialPort = lastPort > 0 ? lastPort : haus.nightmare.j3270.config.Settings.getAutoConnectPort();
@@ -694,13 +821,77 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls()); dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert()); dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage()); dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
dialog.setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
dialog.setVisible(true); dialog.setVisible(true);
if (dialog.isConfirmed()) { if (dialog.isConfirmed()) {
ConnectionConfig config = dialog.getConnectionConfig(); ConnectionConfig config = dialog.getConnectionConfig();
lastHost = config.getHost(); lastHost = config.getHost();
lastPort = config.getPort(); lastPort = config.getPort();
connect(config); connect(config, !wasConnected);
}
}
public void setConnectionHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType ht) {
if (ht == null) return;
haus.nightmare.j3270.config.Settings.setDefaultHostType(ht.name());
if (client != null && client.getConfig() != null) {
client.getConfig().setFtHostType(ht);
String profileId = client.getConfig().getSavedProfileId();
if (profileId != null) {
haus.nightmare.j3270.storage.SavedHost saved = haus.nightmare.j3270.storage.HostStorage.findById(profileId);
if (saved != null) {
saved.setHostType(ht.name());
haus.nightmare.j3270.storage.HostStorage.saveHost(saved);
}
}
}
if (FileTransferDialog.getLastTransferState() != null) {
FileTransferDialog.getLastTransferState().hostType = ht;
}
}
private void updateConnectionMenu(JMenu hostTypeMenu, ButtonGroup hostTypeGroup, JMenu savedHostsMenu) {
haus.nightmare.lib3270j.ft.FTConfig.HostType currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
if (client != null && client.getConfig() != null) {
currentHt = client.getConfig().getFtHostType();
} else {
String def = haus.nightmare.j3270.config.Settings.getDefaultHostType();
try {
if (def != null) currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(def.trim().toUpperCase());
} catch (Exception ignored) {}
}
for (int i = 0; i < hostTypeMenu.getItemCount(); i++) {
JMenuItem item = hostTypeMenu.getItem(i);
if (item instanceof JRadioButtonMenuItem) {
JRadioButtonMenuItem rb = (JRadioButtonMenuItem) item;
if (rb.getText().equalsIgnoreCase(currentHt.name())) {
rb.setSelected(true);
}
}
}
savedHostsMenu.removeAll();
java.util.List<haus.nightmare.j3270.storage.SavedHost> savedList = haus.nightmare.j3270.storage.HostStorage.getSavedHosts();
if (savedList.isEmpty()) {
JMenuItem noneItem = new JMenuItem("(No Saved Connections)");
noneItem.setEnabled(false);
ThemeManager.styleMenuItem(noneItem);
savedHostsMenu.add(noneItem);
} else {
for (haus.nightmare.j3270.storage.SavedHost sh : savedList) {
String label = sh.getName();
if (label == null || label.trim().isEmpty()) {
label = sh.getHost() + ":" + sh.getPort();
}
String ht = (sh.getHostType() != null && !sh.getHostType().trim().isEmpty()) ? sh.getHostType().trim() : "TSO";
JMenuItem hostItem = new JMenuItem(label + " [" + ht + "]");
ThemeManager.styleMenuItem(hostItem);
hostItem.addActionListener(ev -> connect(sh.toConnectionConfig(), true));
savedHostsMenu.add(hostItem);
}
} }
} }
@@ -709,6 +900,79 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
dialog.setVisible(true); dialog.setVisible(true);
} }
public void closeWindow() {
disconnect();
if (refreshTimer != null) {
refreshTimer.stop();
}
if (printerSessionDialog != null) {
printerSessionDialog.dispose();
}
if (screenHistoryDialog != null) {
screenHistoryDialog.dispose();
}
terminalPanel.dispose();
ThemeManager.removeThemeChangeListener(themeChangeListener);
openWindows.remove(this);
dispose();
if (openWindows.isEmpty() && !GraphicsEnvironment.isHeadless()) {
System.exit(0);
}
}
@Override
public void dispose() {
openWindows.remove(this);
ThemeManager.removeThemeChangeListener(themeChangeListener);
super.dispose();
}
public boolean isConnected() {
return client != null && (client.isConnected() ||
(client.getConnectionState() != null && client.getConnectionState().isConnected()));
}
public String getConnectedTarget() {
if (client != null && client.getConfig() != null) {
String profileName = client.getConfig().getSavedProfileName();
String host = client.getConfig().getHost();
int port = client.getConfig().getPort();
if (profileName != null && !profileName.trim().isEmpty()) {
return profileName + " (" + host + ":" + port + ")";
}
if (host != null && !host.trim().isEmpty()) {
return host + ":" + port;
}
}
if (lastHost != null && !lastHost.trim().isEmpty()) {
return lastHost + ":" + lastPort;
}
return "the current host";
}
public J3270App createNewWindow(ConnectionConfig config) {
J3270App newApp = new J3270App();
try {
Point loc = getLocation();
newApp.setLocation(loc.x + 30, loc.y + 30);
} catch (Exception ignored) {}
newApp.setVisible(true);
if (config != null) {
newApp.connect(config, false);
}
return newApp;
}
public ConnectChoiceDialog.Choice promptConnectConflict() {
if (connectChoiceHandler != null) {
return connectChoiceHandler.askChoice(this, getConnectedTarget());
}
if (GraphicsEnvironment.isHeadless()) {
return ConnectChoiceDialog.Choice.ABORT_CURRENT;
}
return ConnectChoiceDialog.showPrompt(this, getConnectedTarget());
}
public TerminalPanel getTerminalPanel() { public TerminalPanel getTerminalPanel() {
return terminalPanel; return terminalPanel;
} }
@@ -721,7 +985,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return statusBar; return statusBar;
} }
void connect(ConnectionConfig config) { public void connect(ConnectionConfig config) {
connect(config, true);
}
public void connect(ConnectionConfig config, boolean promptIfConnected) {
if (promptIfConnected && isConnected() && (!GraphicsEnvironment.isHeadless() || connectChoiceHandler != null)) {
ConnectChoiceDialog.Choice choice = promptConnectConflict();
if (choice == ConnectChoiceDialog.Choice.NEW_WINDOW) {
createNewWindow(config);
return;
} else if (choice == ConnectChoiceDialog.Choice.CANCEL) {
return;
}
// choice == ABORT_CURRENT: proceed to connect in this window
}
lastHost = config.getHost(); lastHost = config.getHost();
lastPort = config.getPort(); lastPort = config.getPort();
@@ -730,8 +1009,29 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls()); haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert()); haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage()); haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
if (config.getHostType() != null) {
haus.nightmare.j3270.config.Settings.setDefaultHostType(config.getHostType());
}
if (FileTransferDialog.getLastTransferState() != null && config.isSavedConnection()) {
FileTransferDialog.getLastTransferState().hostType = config.getFtHostType();
}
if (config.getGraphicsMode() != null) {
haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode());
}
disconnect(); Telnet3270Client oldClient = client;
if (oldClient != null) {
if (fileTransfer != null) {
fileTransfer.cancel();
fileTransfer = null;
}
if (macroDialog != null) {
macroDialog.stopMacro();
}
oldClient.removeConnectionListener(this);
oldClient.removeScreenUpdateListener(this);
client = null;
}
if (config.getCertificateVerifier() == null) { if (config.getCertificateVerifier() == null) {
config.setCertificateVerifier((chain, authType, exception) -> config.setCertificateVerifier((chain, authType, exception) ->
@@ -749,10 +1049,27 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : ""; String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator); setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
terminalPanel.guardedPack(); int targetRows = 24;
int targetCols = 80;
TerminalModel model = config.getModel();
if (model != null) {
if (model.isDynamic()) {
targetRows = config.getDynamicRows();
targetCols = config.getDynamicCols();
} else {
targetRows = model.getAlternateRows();
targetCols = model.getAlternateCols();
}
}
terminalPanel.packForModel(targetRows, targetCols);
new Thread(() -> { new Thread(() -> {
try { try {
if (oldClient != null) {
try {
oldClient.disconnect(1000);
} catch (Exception ignored) {}
}
client.connect(); client.connect();
} catch (IOException e) { } catch (IOException e) {
log.log(Level.WARNING, "Connection failed", e); log.log(Level.WARNING, "Connection failed", e);
@@ -767,31 +1084,43 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocusInWindow();
} }
private void disconnect() { public void disconnect() {
disconnect(true);
}
public void disconnect(boolean resetUi) {
if (client != null) { if (client != null) {
if (fileTransfer != null) { if (fileTransfer != null) {
fileTransfer.cancel(); fileTransfer.cancel();
fileTransfer = null; fileTransfer = null;
} }
if (macroDialog != null) {
macroDialog.stopMacro();
}
Telnet3270Client c = client; Telnet3270Client c = client;
client = null; client = null;
c.removeConnectionListener(this);
c.removeScreenUpdateListener(this);
new Thread(() -> { new Thread(() -> {
try { try {
c.disconnect(); c.disconnect(1000);
} catch (Exception ignored) {} } catch (Exception ignored) {}
}, "Disconnect-Thread").start(); }, "Disconnect-Thread").start();
Runnable uiReset = () -> { if (resetUi) {
terminalPanel.setClient(null); Runnable uiReset = () -> {
statusBar.setClient(null, terminalPanel); terminalPanel.setClient(null);
syncModeMenuItems(); statusBar.setClient(null, terminalPanel);
terminalPanel.repaint(); syncModeMenuItems();
setTitle("j3270 — Java TN3270 Terminal Emulator"); terminalPanel.repaint();
}; setTitle("j3270 — Java TN3270 Terminal Emulator");
if (SwingUtilities.isEventDispatchThread()) { };
uiReset.run(); if (SwingUtilities.isEventDispatchThread()) {
} else { uiReset.run();
SwingUtilities.invokeLater(uiReset); } else {
SwingUtilities.invokeLater(uiReset);
}
} }
} }
} }
@@ -843,7 +1172,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
String dev = (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) ? String dev = (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) ?
" [" + client.getTelnetFSM().getConnectedLu() + "]" : ""; " [" + client.getTelnetFSM().getConnectedLu() + "]" : "";
setTitle("j3270 — " + lastHost + ":" + lastPort + tlsIndicator + dev); setTitle("j3270 — " + lastHost + ":" + lastPort + tlsIndicator + dev);
terminalPanel.guardedPack(); terminalPanel.revalidate();
terminalPanel.repaint();
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocusInWindow();
} }
}); });
@@ -894,7 +1224,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
@Override @Override
public void onScreenSizeChanged(int rows, int cols) { public void onScreenSizeChanged(int rows, int cols) {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
terminalPanel.guardedPack(); terminalPanel.revalidate();
terminalPanel.repaint();
statusBar.updateStatus(); statusBar.updateStatus();
}); });
} }
@@ -1122,6 +1453,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
String configFile = null; String configFile = null;
String cliProxyUrl = null; String cliProxyUrl = null;
String cliProfileName = null; String cliProfileName = null;
String cliHostType = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>(); java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {
@@ -1140,6 +1472,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
cliAutoSysUnlock = true; cliAutoSysUnlock = true;
} else if ("--no-auto-sys-unlock".equals(arg)) { } else if ("--no-auto-sys-unlock".equals(arg)) {
cliAutoSysUnlock = false; cliAutoSysUnlock = false;
} else if (arg.startsWith("--host-type=")) {
cliHostType = arg.substring(12).trim().toUpperCase();
} else if (("--host-type".equals(arg) || "-ht".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
cliHostType = args[++i].trim().toUpperCase();
} else if (arg.startsWith("--graphics=")) { } else if (arg.startsWith("--graphics=")) {
cliGraphicsMode = GraphicsMode.fromString(arg.substring(11)); cliGraphicsMode = GraphicsMode.fromString(arg.substring(11));
} else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) { } else if (("--graphics".equals(arg) || "-g".equals(arg)) && i + 1 < args.length && !args[i + 1].startsWith("-")) {
@@ -1194,6 +1530,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
final GraphicsMode finalGraphicsMode = cliGraphicsMode; final GraphicsMode finalGraphicsMode = cliGraphicsMode;
final String finalProxyUrl = cliProxyUrl; final String finalProxyUrl = cliProxyUrl;
final String finalProfileName = cliProfileName; final String finalProfileName = cliProfileName;
final String finalHostType = cliHostType;
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App(); J3270App app = new J3270App();
@@ -1206,6 +1543,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
if (profile != null) { if (profile != null) {
ConnectionConfig config = profile.toConnectionConfig(); ConnectionConfig config = profile.toConnectionConfig();
if (finalHostType != null) config.setHostType(finalHostType);
if (finalTls) config.setUseTls(true); if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false); if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e); if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
@@ -1262,6 +1600,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (finalProxyUrl != null) { if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl); applyProxyUrl(config, finalProxyUrl);
} }
if (finalHostType != null) {
config.setHostType(finalHostType);
} else {
config.setHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
}
app.connect(config); app.connect(config);
} else { } else {
haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings
@@ -1274,6 +1617,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
haus.nightmare.j3270.storage.SavedHost autoHost = haus.nightmare.j3270.storage.HostStorage.getAutoConnectHost(); haus.nightmare.j3270.storage.SavedHost autoHost = haus.nightmare.j3270.storage.HostStorage.getAutoConnectHost();
if (autoHost != null) { if (autoHost != null) {
ConnectionConfig config = autoHost.toConnectionConfig(); ConnectionConfig config = autoHost.toConnectionConfig();
if (finalHostType != null) config.setHostType(finalHostType);
if (finalTls) config.setUseTls(true); if (finalTls) config.setUseTls(true);
if (finalNoVerify) config.setTlsVerifyCert(false); if (finalNoVerify) config.setTlsVerifyCert(false);
if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e); if (finalTn3270e != null) config.setTn3270eEnabled(finalTn3270e);
@@ -1302,6 +1646,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (finalProxyUrl != null) { if (finalProxyUrl != null) {
applyProxyUrl(config, finalProxyUrl); applyProxyUrl(config, finalProxyUrl);
} }
if (finalHostType != null) {
config.setHostType(finalHostType);
} else {
config.setHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
}
app.connect(config); app.connect(config);
} else { } else {
SwingUtilities.invokeLater(app::showConnectDialog); SwingUtilities.invokeLater(app::showConnectDialog);
@@ -163,6 +163,15 @@ public class Settings {
return getAutoConnectAutoReconnect(); return getAutoConnectAutoReconnect();
} }
public static String getDefaultHostType() {
return prefs.get("defaultHostType", "TSO");
}
public static void setDefaultHostType(String hostType) {
prefs.put("defaultHostType", (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO");
flushPrefs();
}
public static void setAutoReconnect(boolean autoReconnect) { public static void setAutoReconnect(boolean autoReconnect) {
setAutoConnectAutoReconnect(autoReconnect); setAutoConnectAutoReconnect(autoReconnect);
} }
@@ -439,6 +448,33 @@ public class Settings {
flushPrefs(); 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 ========== // ========== Crosshair Ruler ==========
public static boolean getCrosshairRuler() { public static boolean getCrosshairRuler() {
@@ -449,6 +485,71 @@ public class Settings {
prefs.putBoolean("crosshairRuler", enabled); 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 ========== // ========== Status Bar Visibility ==========
public static boolean getStatusBarVisible() { public static boolean getStatusBarVisible() {
@@ -646,6 +747,11 @@ public class Settings {
case "dynamic_cols": case "dynamic_cols":
setDynamicCols(Integer.parseInt(value)); setDynamicCols(Integer.parseInt(value));
break; break;
case "hostType":
case "hosttype":
case "host_type":
setDefaultHostType(value);
break;
case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break;
case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break; case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break;
case "autoReconnect": case "autoReconnect":
@@ -685,6 +791,18 @@ public class Settings {
case "pasteStopAtProtected": case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value)); setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break; 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: default:
log.warning("Unknown behavior/connection key: " + key); log.warning("Unknown behavior/connection key: " + key);
} }
@@ -706,6 +824,18 @@ public class Settings {
case "pasteStopAtProtected": case "pasteStopAtProtected":
setPasteStopAtProtectedLine(Boolean.parseBoolean(value)); setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
break; 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: default:
log.warning("Unknown clipboard key: " + key); log.warning("Unknown clipboard key: " + key);
} }
@@ -829,6 +959,7 @@ public class Settings {
w.println("autoConnectTn3270e = " + getAutoConnectTn3270e()); w.println("autoConnectTn3270e = " + getAutoConnectTn3270e());
} }
w.println("codePage = " + getCodePage()); w.println("codePage = " + getCodePage());
w.println("hostType = " + getDefaultHostType());
w.println("dynamicRows = " + getDynamicRows()); w.println("dynamicRows = " + getDynamicRows());
w.println("dynamicCols = " + getDynamicCols()); w.println("dynamicCols = " + getDynamicCols());
w.println("blockSelectMode = " + getBlockSelectMode()); w.println("blockSelectMode = " + getBlockSelectMode());
@@ -840,6 +971,9 @@ public class Settings {
w.println("autoSysUnlock = " + getAutoSysUnlock()); w.println("autoSysUnlock = " + getAutoSysUnlock());
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel()); w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine()); w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
w.println("trimRectSizingHandles = " + getTrimRectSizingHandles());
w.println("trimRectRemainAfterEdit = " + getTrimRectRemainAfterEdit());
w.println("pasteWordBreak = " + getPasteWordBreak());
w.println(); w.println();
// [entryassist] // [entryassist]
@@ -5,6 +5,7 @@ import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.j3270.ui.ThemeManager; import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.FTConstants; import haus.nightmare.lib3270j.ft.FTConstants;
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder; import javax.swing.border.EmptyBorder;
@@ -28,6 +29,8 @@ public class FileTransferDialog extends JDialog {
public String lrecl = ""; public String lrecl = "";
public String blksize = ""; public String blksize = "";
public String space = ""; public String space = "";
public FTConfig.AllocationUnit units = FTConfig.AllocationUnit.TRACKS;
public String dirBlocks = "";
public String options = ""; public String options = "";
} }
@@ -82,6 +85,8 @@ public class FileTransferDialog extends JDialog {
private JTextField lreclField; private JTextField lreclField;
private JTextField blksizeField; private JTextField blksizeField;
private JTextField spaceField; private JTextField spaceField;
private JComboBox<FTConfig.AllocationUnit> unitsCombo;
private JTextField dirField;
// VM specific // VM specific
private JTextField optionsField; private JTextField optionsField;
@@ -220,7 +225,7 @@ public class FileTransferDialog extends JDialog {
formPanel.add(createRow("File Options:", optionsPanel2)); formPanel.add(createRow("File Options:", optionsPanel2));
// Host-specific options panel (TSO dataset allocation) // Host-specific options panel (TSO dataset allocation)
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5)); JPanel hostOptsPanel = new JPanel(new GridLayout(3, 4, 5, 5));
hostOptsPanel.setOpaque(false); hostOptsPanel.setOpaque(false);
hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)")); hostOptsPanel.setBorder(ThemeManager.createTitledBorder("TSO Allocation Options (Send Only)"));
@@ -236,10 +241,24 @@ public class FileTransferDialog extends JDialog {
blksizeField = new JTextField(5); blksizeField = new JTextField(5);
hostOptsPanel.add(blksizeField); hostOptsPanel.add(blksizeField);
hostOptsPanel.add(new JLabel("SPACE:")); hostOptsPanel.add(new JLabel("Units:"));
unitsCombo = new JComboBox<>(new FTConfig.AllocationUnit[]{
FTConfig.AllocationUnit.TRACKS,
FTConfig.AllocationUnit.CYLINDERS,
FTConfig.AllocationUnit.AVBLOCK,
FTConfig.AllocationUnit.DEFAULT
});
unitsCombo.setSelectedItem(FTConfig.AllocationUnit.TRACKS);
hostOptsPanel.add(unitsCombo);
hostOptsPanel.add(new JLabel("SPACE (Pri,Sec):"));
spaceField = new JTextField(15); spaceField = new JTextField(15);
hostOptsPanel.add(spaceField); hostOptsPanel.add(spaceField);
hostOptsPanel.add(new JLabel("Directory (PDS):"));
dirField = new JTextField(5);
hostOptsPanel.add(dirField);
formPanel.add(Box.createRigidArea(new Dimension(0, 10))); formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(hostOptsPanel); formPanel.add(hostOptsPanel);
@@ -306,9 +325,25 @@ public class FileTransferDialog extends JDialog {
if (lastTransferState.recfm != null) recfmField.setText(lastTransferState.recfm); if (lastTransferState.recfm != null) recfmField.setText(lastTransferState.recfm);
if (lastTransferState.lrecl != null) lreclField.setText(lastTransferState.lrecl); if (lastTransferState.lrecl != null) lreclField.setText(lastTransferState.lrecl);
if (lastTransferState.blksize != null) blksizeField.setText(lastTransferState.blksize); if (lastTransferState.blksize != null) blksizeField.setText(lastTransferState.blksize);
if (lastTransferState.units != null) unitsCombo.setSelectedItem(lastTransferState.units);
if (lastTransferState.space != null) spaceField.setText(lastTransferState.space); if (lastTransferState.space != null) spaceField.setText(lastTransferState.space);
if (lastTransferState.dirBlocks != null) dirField.setText(lastTransferState.dirBlocks);
if (lastTransferState.options != null) optionsField.setText(lastTransferState.options); if (lastTransferState.options != null) optionsField.setText(lastTransferState.options);
} }
// When using a saved connection (or when no previous transfer state exists for this connection),
// use the configured host type (CMS/TSO/CICS) for File Transfers
if (coordinator != null && coordinator.getClient() != null) {
haus.nightmare.lib3270j.ConnectionConfig cfg = coordinator.getClient().getConfig();
if (cfg != null) {
FTConfig.HostType configuredType = cfg.getFtHostType();
if (configuredType != null) {
if (cfg.isSavedConnection() || lastTransferState == null) {
hostTypeCombo.setSelectedItem(configuredType);
}
}
}
}
} }
private void updateOptionStates() { private void updateOptionStates() {
@@ -327,7 +362,9 @@ public class FileTransferDialog extends JDialog {
recfmField.setEnabled(isSend && isTso); recfmField.setEnabled(isSend && isTso);
lreclField.setEnabled(isSend && isTso); lreclField.setEnabled(isSend && isTso);
blksizeField.setEnabled(isSend && isTso); blksizeField.setEnabled(isSend && isTso);
unitsCombo.setEnabled(isSend && isTso);
spaceField.setEnabled(isSend && isTso); spaceField.setEnabled(isSend && isTso);
dirField.setEnabled(isSend && isTso);
optionsField.setEnabled(isCms); optionsField.setEnabled(isCms);
} }
@@ -359,11 +396,26 @@ public class FileTransferDialog extends JDialog {
private void browseHostDirectory() { private void browseHostDirectory() {
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem(); FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
String initialQuery = hostFileField.getText().trim(); String initialQuery = hostFileField.getText().trim();
if (hostType == FTConfig.HostType.CMS && (initialQuery.isEmpty() || initialQuery.equals("* * *") || initialQuery.equals("*"))) {
initialQuery = "* * A";
}
HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery); HostDirectoryDialog dialog = new HostDirectoryDialog(this, coordinator.getClient(), hostType, initialQuery);
dialog.setVisible(true); dialog.setVisible(true);
if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) { if (dialog.isConfirmed() && dialog.getSelectedHostFile() != null) {
hostFileField.setText(dialog.getSelectedHostFile()); hostFileField.setText(dialog.getSelectedHostFile());
HostDirectoryEntry entry = dialog.getSelectedEntry();
if (entry != null) {
if (entry.getRecfm() != null && !entry.getRecfm().trim().isEmpty()) {
recfmField.setText(entry.getRecfm().trim());
}
if (entry.getLrecl() > 0) {
lreclField.setText(String.valueOf(entry.getLrecl()));
}
if (entry.getBlksize() > 0) {
blksizeField.setText(String.valueOf(entry.getBlksize()));
}
}
boolean shouldAutofill = localFileField.getText().trim().isEmpty() || boolean shouldAutofill = localFileField.getText().trim().isEmpty() ||
(lastTransferState != null && localFileField.getText().trim().equals(lastTransferState.localFile)); (lastTransferState != null && localFileField.getText().trim().equals(lastTransferState.localFile));
if (shouldAutofill) { if (shouldAutofill) {
@@ -410,7 +462,10 @@ public class FileTransferDialog extends JDialog {
config.setRecfm(recfmField.getText().trim()); config.setRecfm(recfmField.getText().trim());
config.setLrecl(lreclField.getText().trim()); config.setLrecl(lreclField.getText().trim());
config.setBlksize(blksizeField.getText().trim()); config.setBlksize(blksizeField.getText().trim());
FTConfig.AllocationUnit selectedUnits = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
config.setUnits(selectedUnits != null ? selectedUnits : FTConfig.AllocationUnit.DEFAULT);
config.setSpace(spaceField.getText().trim()); config.setSpace(spaceField.getText().trim());
config.setDirectoryBlocks(dirField.getText().trim());
config.setOptions(optionsField.getText().trim()); config.setOptions(optionsField.getText().trim());
recordSessionTransfer(); recordSessionTransfer();
@@ -439,7 +494,9 @@ public class FileTransferDialog extends JDialog {
state.recfm = recfmField.getText().trim(); state.recfm = recfmField.getText().trim();
state.lrecl = lreclField.getText().trim(); state.lrecl = lreclField.getText().trim();
state.blksize = blksizeField.getText().trim(); state.blksize = blksizeField.getText().trim();
state.units = (FTConfig.AllocationUnit) unitsCombo.getSelectedItem();
state.space = spaceField.getText().trim(); state.space = spaceField.getText().trim();
state.dirBlocks = dirField.getText().trim();
state.options = optionsField.getText().trim(); state.options = optionsField.getText().trim();
lastTransferState = state; lastTransferState = state;
} }
@@ -459,7 +516,9 @@ public class FileTransferDialog extends JDialog {
public JTextField getRecfmField() { return recfmField; } public JTextField getRecfmField() { return recfmField; }
public JTextField getLreclField() { return lreclField; } public JTextField getLreclField() { return lreclField; }
public JTextField getBlksizeField() { return blksizeField; } public JTextField getBlksizeField() { return blksizeField; }
public JComboBox<FTConfig.AllocationUnit> getUnitsCombo() { return unitsCombo; }
public JTextField getSpaceField() { return spaceField; } public JTextField getSpaceField() { return spaceField; }
public JTextField getDirField() { return dirField; }
public JTextField getOptionsField() { return optionsField; } public JTextField getOptionsField() { return optionsField; }
private void applyTheme(Container container) { private void applyTheme(Container container) {
@@ -1,5 +1,6 @@
package haus.nightmare.j3270.storage; package haus.nightmare.j3270.storage;
import java.io.*;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -109,6 +110,267 @@ public class HostStorage {
} }
} }
// ================= INI File Export / Import =================
/**
* Export all currently saved host connections to an INI file.
*/
public static synchronized void exportToIniFile(File file) throws IOException {
List<SavedHost> hosts = getSavedHosts();
exportToIniFile(file, hosts);
}
public static synchronized void exportToIniFile(String path) throws IOException {
exportToIniFile(new File(path));
}
/**
* Export a specified list of host connections to an INI file.
*/
public static void exportToIniFile(File file, List<SavedHost> hosts) throws IOException {
if (file == null) {
throw new IllegalArgumentException("Target file cannot be null");
}
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(file)))) {
w.println("; j3270 Saved Connections Configuration File");
w.println("; Exported on " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
w.println("; Connections count: " + (hosts != null ? hosts.size() : 0));
w.println();
if (hosts != null) {
for (SavedHost h : hosts) {
String section = (h.getName() != null && !h.getName().trim().isEmpty())
? h.getName().trim()
: (h.getHost() != null && !h.getHost().trim().isEmpty() ? h.getHost().trim() : "Connection");
w.println("[" + section + "]");
if (h.getId() != null && !h.getId().trim().isEmpty()) {
w.println("id = " + h.getId());
}
w.println("name = " + (h.getName() != null ? h.getName() : ""));
w.println("host = " + (h.getHost() != null ? h.getHost() : ""));
w.println("port = " + h.getPort());
w.println("model = " + h.getModel());
w.println("dynamicRows = " + h.getDynamicRows());
w.println("dynamicCols = " + h.getDynamicCols());
w.println("luName = " + (h.getLuName() != null ? h.getLuName() : ""));
w.println("autoConnect = " + h.isAutoConnect());
w.println("hostType = " + (h.getHostType() != null ? h.getHostType() : "TSO"));
w.println("useTls = " + h.isUseTls());
w.println("tlsVerifyCert = " + h.isTlsVerifyCert());
w.println("tn3270e = " + h.isTn3270e());
w.println("graphicsMode = " + (h.getGraphicsMode() != null ? h.getGraphicsMode() : "BOTH"));
w.println("codePage = " + (h.getCodePage() != null ? h.getCodePage() : "037"));
w.println("proxyType = " + (h.getProxyType() != null ? h.getProxyType() : "NONE"));
w.println("proxyHost = " + (h.getProxyHost() != null ? h.getProxyHost() : ""));
w.println("proxyPort = " + h.getProxyPort());
w.println("proxyUsername = " + (h.getProxyUsername() != null ? h.getProxyUsername() : ""));
w.println("proxyPassword = " + (h.getProxyPassword() != null ? h.getProxyPassword() : ""));
w.println();
}
}
}
log.info("Saved connections exported to: " + file.getAbsolutePath());
}
/**
* Load connections from an INI file without saving them to persistent storage.
*/
public static List<SavedHost> loadFromIniFile(File file) throws IOException {
if (file == null || !file.exists()) {
throw new FileNotFoundException("Connections file not found: " + (file != null ? file.getAbsolutePath() : "null"));
}
List<SavedHost> hosts = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
SavedHost current = null;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) {
continue;
}
if (line.startsWith("[") && line.endsWith("]")) {
if (current != null) {
hosts.add(current);
}
String section = line.substring(1, line.length() - 1).trim();
if (section.toLowerCase().startsWith("connection:")) {
section = section.substring("connection:".length()).trim();
} else if (section.toLowerCase().startsWith("host:")) {
section = section.substring("host:".length()).trim();
}
current = new SavedHost();
current.setName(section);
continue;
}
int eq = line.indexOf('=');
if (eq < 0) {
continue;
}
String key = line.substring(0, eq).trim();
String value = line.substring(eq + 1).trim();
if (current == null) {
current = new SavedHost();
current.setName("Default");
}
applyIniEntry(current, key, value);
}
if (current != null) {
hosts.add(current);
}
}
return hosts;
}
public static List<SavedHost> loadFromIniFile(String path) throws IOException {
return loadFromIniFile(new File(path));
}
/**
* Import connections from an INI file and merge them into persistent storage.
* Updates existing profiles matching by ID or Name, and appends new profiles.
*/
public static synchronized List<SavedHost> importFromIniFile(File file) throws IOException {
List<SavedHost> imported = loadFromIniFile(file);
if (imported == null || imported.isEmpty()) {
return getSavedHosts();
}
List<SavedHost> current = new ArrayList<>(getSavedHosts());
for (SavedHost host : imported) {
int index = -1;
for (int i = 0; i < current.size(); i++) {
SavedHost existing = current.get(i);
if ((host.getId() != null && !host.getId().trim().isEmpty() && Objects.equals(existing.getId(), host.getId())) ||
(host.getName() != null && !host.getName().trim().isEmpty() &&
host.getName().equalsIgnoreCase(existing.getName()))) {
index = i;
break;
}
}
if (host.isAutoConnect()) {
for (SavedHost c : current) {
c.setAutoConnect(false);
}
}
if (index >= 0) {
if (host.getId() == null || host.getId().trim().isEmpty()) {
host.setId(current.get(index).getId());
}
current.set(index, host);
} else {
current.add(host);
}
}
saveAll(current);
return current;
}
public static synchronized List<SavedHost> importFromIniFile(String path) throws IOException {
return importFromIniFile(new File(path));
}
private static void applyIniEntry(SavedHost h, String key, String value) {
String lowerKey = key.toLowerCase();
switch (lowerKey) {
case "id":
if (!value.isEmpty()) h.setId(value);
break;
case "name":
case "profilename":
case "profile":
if (!value.isEmpty()) h.setName(value);
break;
case "host":
case "hostname":
case "address":
h.setHost(value);
break;
case "port":
h.setPort(parseInt(value, 23));
break;
case "model":
case "terminalmodel":
h.setModel(parseInt(value, 4));
break;
case "dynamicrows":
case "dynamic_rows":
case "rows":
h.setDynamicRows(parseInt(value, 62));
break;
case "dynamiccols":
case "dynamic_cols":
case "cols":
h.setDynamicCols(parseInt(value, 160));
break;
case "luname":
case "lu_name":
case "lu":
h.setLuName(value);
break;
case "autoconnect":
case "auto_connect":
h.setAutoConnect(Boolean.parseBoolean(value));
break;
case "hosttype":
case "host_type":
case "type":
h.setHostType(value.toUpperCase());
break;
case "usetls":
case "use_tls":
case "tls":
case "ssl":
h.setUseTls(Boolean.parseBoolean(value));
break;
case "tlsverifycert":
case "verifycert":
case "tls_verify_cert":
h.setTlsVerifyCert(Boolean.parseBoolean(value));
break;
case "tn3270e":
case "enabletn3270e":
h.setTn3270e(Boolean.parseBoolean(value));
break;
case "graphicsmode":
case "graphics_mode":
case "graphics":
h.setGraphicsMode(value.toUpperCase());
break;
case "codepage":
case "code_page":
case "charset":
h.setCodePage(value);
break;
case "proxytype":
case "proxy_type":
h.setProxyType(value.toUpperCase());
break;
case "proxyhost":
case "proxy_host":
h.setProxyHost(value);
break;
case "proxyport":
case "proxy_port":
h.setProxyPort(parseInt(value, 0));
break;
case "proxyusername":
case "proxy_username":
case "proxyuser":
h.setProxyUsername(value);
break;
case "proxypassword":
case "proxy_password":
case "proxypass":
h.setProxyPassword(value);
break;
default:
break;
}
}
// ================= JSON Serialization ================= // ================= JSON Serialization =================
public static String serializeHostsJson(List<SavedHost> hosts) { public static String serializeHostsJson(List<SavedHost> hosts) {
@@ -234,7 +496,6 @@ public class HostStorage {
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true)); if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
if (map.containsKey("graphicsMode")) { if (map.containsKey("graphicsMode")) {
String gm = String.valueOf(map.get("graphicsMode")); String gm = String.valueOf(map.get("graphicsMode"));
if ("PROGRAMMED_SYMBOLS".equalsIgnoreCase(gm)) gm = "BOTH";
h.setGraphicsMode(gm); h.setGraphicsMode(gm);
} }
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage"))); if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
@@ -108,6 +108,10 @@ public class SavedHost {
} }
ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls); ConnectionConfig config = new ConnectionConfig(host, port, tm, useTls);
config.setSavedConnection(true);
config.setSavedProfileId(this.id);
config.setSavedProfileName(this.name);
config.setHostType(this.hostType != null ? this.hostType : "TSO");
if (model == 0 || tm.isDynamic()) { if (model == 0 || tm.isDynamic()) {
config.setDynamicDimensions(dynamicRows, dynamicCols); config.setDynamicDimensions(dynamicRows, dynamicCols);
} }
@@ -0,0 +1,146 @@
package haus.nightmare.j3270.ui;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.KeyEvent;
/**
* Dialogue shown when a connection is initiated while j3270 is already connected.
* Asks the user whether the connection should be opened in a new window or
* if the current connection should be aborted.
*/
public class ConnectChoiceDialog extends JDialog {
public enum Choice {
NEW_WINDOW,
ABORT_CURRENT,
CANCEL
}
private Choice result = Choice.CANCEL;
private final JButton newWindowBtn;
private final JButton abortBtn;
private final JButton cancelBtn;
private final JLabel titleLabel;
private final JLabel line1;
private final JLabel line2;
public ConnectChoiceDialog(Window owner, String currentTarget) {
super(owner, "Active Connection", ModalityType.APPLICATION_MODAL);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setResizable(false);
JPanel mainPanel = new JPanel(new BorderLayout(16, 16));
mainPanel.setBorder(new EmptyBorder(20, 24, 20, 24));
// Message Panel
JPanel messagePanel = new JPanel();
messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.Y_AXIS));
messagePanel.setOpaque(false);
titleLabel = new JLabel("Connection Already Active");
titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 2));
messagePanel.add(titleLabel);
messagePanel.add(Box.createVerticalStrut(10));
String targetText = (currentTarget != null && !currentTarget.trim().isEmpty())
? currentTarget.trim()
: "the host";
line1 = new JLabel("j3270 is currently connected to " + targetText + ".");
line1.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
messagePanel.add(line1);
messagePanel.add(Box.createVerticalStrut(6));
line2 = new JLabel("Would you like to open the connection in a new window or abort the current connection?");
line2.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
messagePanel.add(line2);
mainPanel.add(messagePanel, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
buttonPanel.setOpaque(false);
cancelBtn = new JButton("Cancel");
ThemeManager.styleButton(cancelBtn, ThemeManager.ButtonVariant.CANCEL);
cancelBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
cancelBtn.addActionListener(e -> {
result = Choice.CANCEL;
dispose();
});
abortBtn = new JButton("Abort Current Connection");
ThemeManager.styleButton(abortBtn, ThemeManager.ButtonVariant.DANGER);
abortBtn.setFont(ThemeManager.getUiFont(Font.PLAIN, 0));
abortBtn.addActionListener(e -> {
result = Choice.ABORT_CURRENT;
dispose();
});
newWindowBtn = new JButton("New Window");
ThemeManager.styleButton(newWindowBtn, ThemeManager.ButtonVariant.PRIMARY);
newWindowBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0));
newWindowBtn.addActionListener(e -> {
result = Choice.NEW_WINDOW;
dispose();
});
buttonPanel.add(cancelBtn);
buttonPanel.add(abortBtn);
buttonPanel.add(newWindowBtn);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this);
// ESC key cancels
getRootPane().registerKeyboardAction(e -> {
result = Choice.CANCEL;
dispose();
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
getRootPane().setDefaultButton(newWindowBtn);
pack();
if (owner != null) {
setLocationRelativeTo(owner);
} else {
setLocationRelativeTo(null);
}
}
public Choice getChoice() {
return result;
}
public JButton getNewWindowButton() {
return newWindowBtn;
}
public JButton getAbortButton() {
return abortBtn;
}
public JButton getCancelButton() {
return cancelBtn;
}
public JLabel getTitleLabel() {
return titleLabel;
}
public JLabel getLine1() {
return line1;
}
public JLabel getLine2() {
return line2;
}
public static Choice showPrompt(Window owner, String currentTarget) {
ConnectChoiceDialog dialog = new ConnectChoiceDialog(owner, currentTarget);
dialog.setVisible(true);
return dialog.getChoice();
}
}
@@ -6,6 +6,7 @@ import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel; import haus.nightmare.lib3270j.TerminalModel;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder; import javax.swing.border.TitledBorder;
import java.awt.*; import java.awt.*;
import java.util.List; import java.util.List;
@@ -35,6 +36,7 @@ public class ConnectDialog extends JDialog {
private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo; private JComboBox<haus.nightmare.lib3270j.graphics.GraphicsMode> graphicsCombo;
private JComboBox<String> codePageCombo; private JComboBox<String> codePageCombo;
private JTextField luField; private JTextField luField;
private JComboBox<String> hostTypeCombo;
private JCheckBox tlsCheckBox; private JCheckBox tlsCheckBox;
private JCheckBox verifyCertCheckBox; private JCheckBox verifyCertCheckBox;
private JCheckBox tn3270eCheckBox; private JCheckBox tn3270eCheckBox;
@@ -213,6 +215,25 @@ public class ConnectDialog extends JDialog {
gridy++; gridy++;
// Host Type (CMS/TSO/CICS)
gbc.gridx = 0;
gbc.gridy = gridy;
gbc.weightx = 0;
JLabel hostTypeLabel = new JLabel("Host Type:");
hostTypeLabel.setFont(labelFont);
contentPanel.add(hostTypeLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
String[] hostTypes = {"TSO", "CMS", "CICS"};
hostTypeCombo = new JComboBox<>(hostTypes);
hostTypeCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(hostTypeCombo);
setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
contentPanel.add(hostTypeCombo, gbc);
gridy++;
// 6. Graphics Mode // 6. Graphics Mode
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = gridy; gbc.gridy = gridy;
@@ -226,6 +247,24 @@ public class ConnectDialog extends JDialog {
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode()); graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
graphicsCombo.setFont(ThemeManager.getUiFont()); graphicsCombo.setFont(ThemeManager.getUiFont());
ThemeManager.styleComboBox(graphicsCombo); ThemeManager.styleComboBox(graphicsCombo);
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); contentPanel.add(graphicsCombo, gbc);
gridy++; gridy++;
@@ -546,6 +585,7 @@ public class ConnectDialog extends JDialog {
} else { } else {
selectedProfileId = null; selectedProfileId = null;
deleteProfileBtn.setEnabled(false); deleteProfileBtn.setEnabled(false);
setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType());
} }
} }
@@ -571,6 +611,7 @@ public class ConnectDialog extends JDialog {
dynamicRowsSpinner.setValue(h.getDynamicRows() > 0 ? h.getDynamicRows() : 62); dynamicRowsSpinner.setValue(h.getDynamicRows() > 0 ? h.getDynamicRows() : 62);
dynamicColsSpinner.setValue(h.getDynamicCols() > 0 ? h.getDynamicCols() : 160); dynamicColsSpinner.setValue(h.getDynamicCols() > 0 ? h.getDynamicCols() : 160);
luField.setText(h.getLuName() != null ? h.getLuName() : ""); luField.setText(h.getLuName() != null ? h.getLuName() : "");
setInitialHostType(h.getHostType() != null ? h.getHostType() : "TSO");
tlsCheckBox.setSelected(h.isUseTls()); tlsCheckBox.setSelected(h.isUseTls());
verifyCertCheckBox.setEnabled(h.isUseTls()); verifyCertCheckBox.setEnabled(h.isUseTls());
verifyCertCheckBox.setSelected(h.isTlsVerifyCert()); verifyCertCheckBox.setSelected(h.isTlsVerifyCert());
@@ -658,13 +699,14 @@ public class ConnectDialog extends JDialog {
int dRows = (Integer) dynamicRowsSpinner.getValue(); int dRows = (Integer) dynamicRowsSpinner.getValue();
int dCols = (Integer) dynamicColsSpinner.getValue(); int dCols = (Integer) dynamicColsSpinner.getValue();
String lu = luField.getText().trim(); String lu = luField.getText().trim();
String hostType = getSelectedHostType();
boolean tls = tlsCheckBox.isSelected(); boolean tls = tlsCheckBox.isSelected();
boolean verify = verifyCertCheckBox.isSelected(); boolean verify = verifyCertCheckBox.isSelected();
boolean tn3270e = tn3270eCheckBox.isSelected(); boolean tn3270e = tn3270eCheckBox.isSelected();
String gfx = "BOTH"; String gfx = "BOTH";
if (graphicsCombo.getSelectedItem() != null) { if (graphicsCombo.getSelectedItem() != null) {
gfx = graphicsCombo.getSelectedItem().toString(); gfx = ((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()).name();
} }
String cp = "037"; String cp = "037";
@@ -686,7 +728,7 @@ public class ConnectDialog extends JDialog {
String pPass = new String(proxyPassField.getPassword()); String pPass = new String(proxyPassField.getPassword());
return new SavedHost(id, profileName, h, port, mNum, dRows, dCols, lu, false, return new SavedHost(id, profileName, h, port, mNum, dRows, dCols, lu, false,
"TSO", tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass); hostType, tls, verify, tn3270e, gfx, cp, ptStr, pHost, pPort, pUser, pPass);
} }
// ================= Connect Action ================= // ================= Connect Action =================
@@ -731,7 +773,9 @@ public class ConnectDialog extends JDialog {
result.setLuName(lu); result.setLuName(lu);
} }
if (graphicsCombo.getSelectedItem() != null) { if (graphicsCombo.getSelectedItem() != null) {
result.setGraphicsMode((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()); 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(); Object cpSelection = codePageCombo.getSelectedItem();
@@ -776,6 +820,18 @@ public class ConnectDialog extends JDialog {
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass); result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
} }
result.setHostType(getSelectedHostType());
if (selectedProfileId != null) {
result.setSavedConnection(true);
result.setSavedProfileId(selectedProfileId);
SavedHost existing = HostStorage.findById(selectedProfileId);
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
? existing.getName() : host;
result.setSavedProfileName(pName);
SavedHost updated = buildCurrentSavedHost(pName);
HostStorage.saveHost(updated);
}
confirmed = true; confirmed = true;
dispose(); dispose();
} }
@@ -810,6 +866,12 @@ public class ConnectDialog extends JDialog {
tn3270eCheckBox.setSelected(tn3270e); 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) { public void setInitialCodePage(String cp) {
if (cp != null && codePageCombo != null) { if (cp != null && codePageCombo != null) {
for (int i = 0; i < codePageCombo.getItemCount(); i++) { for (int i = 0; i < codePageCombo.getItemCount(); i++) {
@@ -867,4 +929,44 @@ public class ConnectDialog extends JDialog {
public String getProxyPassword() { public String getProxyPassword() {
return new String(proxyPassField.getPassword()); return new String(proxyPassField.getPassword());
} }
public void setInitialHostType(String hostType) {
if (hostType != null && hostTypeCombo != null) {
for (int i = 0; i < hostTypeCombo.getItemCount(); i++) {
if (hostTypeCombo.getItemAt(i).equalsIgnoreCase(hostType.trim())) {
hostTypeCombo.setSelectedIndex(i);
return;
}
}
}
}
public void setInitialHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType hostType) {
if (hostType != null) {
setInitialHostType(hostType.name());
}
}
public String getSelectedHostType() {
if (hostTypeCombo != null && hostTypeCombo.getSelectedItem() != null) {
return hostTypeCombo.getSelectedItem().toString().trim().toUpperCase();
}
return "TSO";
}
public haus.nightmare.lib3270j.ft.FTConfig.HostType getSelectedFtHostType() {
try {
return haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(getSelectedHostType());
} catch (Exception e) {
return haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
}
}
public JComboBox<String> getHostTypeCombo() {
return hostTypeCombo;
}
public String getSelectedProfileId() {
return selectedProfileId;
}
} }
@@ -3,15 +3,19 @@ package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.Telnet3270Client; import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry; import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryParser;
import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry; import haus.nightmare.lib3270j.ft.dir.HostDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry; import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryParser;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder; import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader; import javax.swing.table.JTableHeader;
import java.awt.*; import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.util.ArrayList; import java.util.ArrayList;
@@ -19,6 +23,7 @@ import java.util.List;
/** /**
* Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments. * Host Directory and Dataset Catalog browser dialog for TSO and VM/CMS environments.
* Aligned with IBM Host On-Demand presentation space queries and clipboard parsing.
*/ */
public class HostDirectoryDialog extends JDialog { public class HostDirectoryDialog extends JDialog {
@@ -27,13 +32,16 @@ public class HostDirectoryDialog extends JDialog {
private JComboBox<FTConfig.HostType> hostTypeCombo; private JComboBox<FTConfig.HostType> hostTypeCombo;
private JTextField queryField; private JTextField queryField;
private JButton queryHostBtn;
private JTable table; private JTable table;
private DefaultTableModel tableModel; private DefaultTableModel tableModel;
private JLabel statusLabel; private JLabel statusLabel;
private String selectedHostFile = null; private String selectedHostFile = null;
private HostDirectoryEntry selectedEntry = null;
private boolean confirmed = false; private boolean confirmed = false;
private final List<HostDirectoryEntry> allEntries = new ArrayList<>();
private final List<HostDirectoryEntry> currentEntries = new ArrayList<>(); private final List<HostDirectoryEntry> currentEntries = new ArrayList<>();
public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) { public HostDirectoryDialog(Dialog parent, Telnet3270Client client, FTConfig.HostType hostType, String initialQuery) {
@@ -43,7 +51,7 @@ public class HostDirectoryDialog extends JDialog {
AppIcon.applyTo(this); AppIcon.applyTo(this);
buildUI(initialQuery); buildUI(initialQuery);
setSize(700, 420); setSize(840, 480);
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
} }
@@ -69,33 +77,68 @@ public class HostDirectoryDialog extends JDialog {
if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS); if (initialHostType == FTConfig.HostType.CMS) hostTypeCombo.setSelectedItem(FTConfig.HostType.CMS);
else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO); else hostTypeCombo.setSelectedItem(FTConfig.HostType.TSO);
ThemeManager.styleComboBox(hostTypeCombo); ThemeManager.styleComboBox(hostTypeCombo);
hostTypeCombo.addActionListener(e -> {
setupTableColumns();
FTConfig.HostType ht = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
if (ht == FTConfig.HostType.CMS) {
String cur = queryField.getText().trim();
if (cur.isEmpty() || cur.equals("* * *") || cur.equals("*")) {
queryField.setText("* * A");
}
} else if (ht == FTConfig.HostType.TSO) {
String cur = queryField.getText().trim();
if (cur.equals("* * A") || cur.equals("* * *")) {
queryField.setText("");
}
}
if (client != null && client.getConnectionState().isFullSession()) {
queryHost();
} else {
parseCurrentScreen();
}
});
topPanel.add(hostTypeCombo, gbc); topPanel.add(hostTypeCombo, gbc);
gbc.gridx = 2; gbc.gridx = 2;
JLabel queryLabel = new JLabel("Query Pattern / Text:"); JLabel queryLabel = new JLabel("Filter / Pattern:");
queryLabel.setFont(font); queryLabel.setFont(font);
topPanel.add(queryLabel, gbc); topPanel.add(queryLabel, gbc);
gbc.gridx = 3; gbc.gridx = 3;
gbc.weightx = 1.0; gbc.weightx = 1.0;
queryField = new JTextField(initialQuery != null ? initialQuery : "", 16); String defaultQuery = resolveDefaultQuery(initialHostType, initialQuery);
queryField = new JTextField(defaultQuery, 16);
queryField.setFont(ThemeManager.getMonospacedUiFont()); queryField.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleTextField(queryField); queryField.addActionListener(e -> onFilterOrParse());
topPanel.add(queryField, gbc); topPanel.add(queryField, gbc);
gbc.gridx = 4; gbc.gridx = 4;
gbc.weightx = 0; gbc.weightx = 0;
JButton parseBtn = new JButton("Query / Parse"); JButton filterBtn = new JButton("Filter / Parse");
ThemeManager.styleButton(parseBtn, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(filterBtn, ThemeManager.ButtonVariant.DEFAULT);
parseBtn.setFont(font); filterBtn.setFont(font);
parseBtn.addActionListener(e -> runQuery()); filterBtn.addActionListener(e -> onFilterOrParse());
topPanel.add(parseBtn, gbc); topPanel.add(filterBtn, gbc);
gbc.gridx = 5;
queryHostBtn = new JButton("Query Host");
ThemeManager.styleButton(queryHostBtn, ThemeManager.ButtonVariant.DEFAULT);
queryHostBtn.setFont(font);
queryHostBtn.setToolTipText("Send LISTCAT or LISTFILE query to active host session");
queryHostBtn.addActionListener(e -> queryHost());
topPanel.add(queryHostBtn, gbc);
mainPanel.add(topPanel, BorderLayout.NORTH); mainPanel.add(topPanel, BorderLayout.NORTH);
// Table // Table
tableModel = new DefaultTableModel(); tableModel = new DefaultTableModel() {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
table = new JTable(tableModel); table = new JTable(tableModel);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
ThemeManager.styleTable(table); ThemeManager.styleTable(table);
table.setFont(ThemeManager.getMonospacedUiFont()); table.setFont(ThemeManager.getMonospacedUiFont());
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
@@ -114,26 +157,42 @@ public class HostDirectoryDialog extends JDialog {
}); });
JScrollPane scrollPane = new JScrollPane(table); JScrollPane scrollPane = new JScrollPane(table);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
ThemeManager.styleScrollPane(scrollPane); ThemeManager.styleScrollPane(scrollPane);
mainPanel.add(scrollPane, BorderLayout.CENTER); mainPanel.add(scrollPane, BorderLayout.CENTER);
// Bottom // Bottom
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5)); JPanel bottomPanel = new JPanel(new BorderLayout(5, 6));
bottomPanel.setOpaque(false); bottomPanel.setOpaque(false);
bottomPanel.setBorder(BorderFactory.createEmptyBorder(6, 4, 4, 4));
statusLabel = new JLabel("Enter a dataset pattern or parse active screen."); statusLabel = new JLabel("Enter a dataset pattern or parse active screen.");
statusLabel.setForeground(ThemeManager.getFgMuted()); statusLabel.setForeground(ThemeManager.getFgMuted());
statusLabel.setFont(font); statusLabel.setFont(font);
bottomPanel.add(statusLabel, BorderLayout.WEST); bottomPanel.add(statusLabel, BorderLayout.NORTH);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
btnPanel.setOpaque(false); btnPanel.setOpaque(false);
JButton pasteScreenBtn = new JButton("Parse Current Screen"); JButton inspectBtn = new JButton("Inspect (LISTDS)");
ThemeManager.styleButton(inspectBtn, ThemeManager.ButtonVariant.DEFAULT);
inspectBtn.setFont(font);
inspectBtn.setToolTipText("Retrieve VTOC attributes (RECFM, LRECL, BLKSIZE) for the selected dataset via TSO LISTDS");
inspectBtn.addActionListener(e -> inspectSelectedDataset());
JButton pasteScreenBtn = new JButton("Parse Screen");
ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(pasteScreenBtn, ThemeManager.ButtonVariant.DEFAULT);
pasteScreenBtn.setFont(font); pasteScreenBtn.setFont(font);
pasteScreenBtn.setToolTipText("Parse items currently visible on the 3270 presentation space");
pasteScreenBtn.addActionListener(e -> parseCurrentScreen()); pasteScreenBtn.addActionListener(e -> parseCurrentScreen());
JButton pasteClipBtn = new JButton("Paste Clipboard");
ThemeManager.styleButton(pasteClipBtn, ThemeManager.ButtonVariant.DEFAULT);
pasteClipBtn.setFont(font);
pasteClipBtn.setToolTipText("Parse host filenames copied to the system clipboard (HoD compatible)");
pasteClipBtn.addActionListener(e -> parseClipboard());
JButton selectBtn = new JButton("Select Dataset"); JButton selectBtn = new JButton("Select Dataset");
ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY); ThemeManager.styleButton(selectBtn, ThemeManager.ButtonVariant.PRIMARY);
selectBtn.setFont(font); selectBtn.setFont(font);
@@ -147,23 +206,28 @@ public class HostDirectoryDialog extends JDialog {
dispose(); dispose();
}); });
btnPanel.add(inspectBtn);
btnPanel.add(pasteScreenBtn); btnPanel.add(pasteScreenBtn);
btnPanel.add(pasteClipBtn);
btnPanel.add(Box.createHorizontalStrut(10)); btnPanel.add(Box.createHorizontalStrut(10));
btnPanel.add(selectBtn); btnPanel.add(selectBtn);
btnPanel.add(cancelBtn); btnPanel.add(cancelBtn);
bottomPanel.add(btnPanel, BorderLayout.EAST); bottomPanel.add(btnPanel, BorderLayout.SOUTH);
mainPanel.add(bottomPanel, BorderLayout.SOUTH); mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel); setContentPane(mainPanel);
ThemeManager.applyThemeToWindow(this); ThemeManager.applyThemeToWindow(this);
// Initial setup // Initial setup: columns and screen parse or host query
setupTableColumns(); setupTableColumns();
if (initialQuery != null && !initialQuery.isEmpty()) { if (client != null && client.getConnectionState().isFullSession()) {
runQuery(); queryHost();
} else { } else {
parseCurrentScreen(); parseCurrentScreen();
if (!queryField.getText().trim().isEmpty()) {
applyFilter();
}
} }
} }
@@ -174,76 +238,665 @@ public class HostDirectoryDialog extends JDialog {
tableModel.setColumnIdentifiers(new Object[]{ tableModel.setColumnIdentifiers(new Object[]{
"Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time" "Filename", "Filetype", "Filemode", "Format", "Lrecl", "Records", "Date / Time"
}); });
int[] widths = {120, 100, 80, 70, 70, 90, 170};
for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
}
} else { } else {
tableModel.setColumnIdentifiers(new Object[]{ tableModel.setColumnIdentifiers(new Object[]{
"Dataset Name", "Volume", "Recfm", "Lrecl", "Blksize", "Tracks", "Extents" "Dataset Name", "Volume"
}); });
} int[] widths = {620, 160};
} for (int i = 0; i < widths.length && i < table.getColumnCount(); i++) {
table.getColumnModel().getColumn(i).setPreferredWidth(widths[i]);
private void runQuery() { if (i == 0) {
setupTableColumns(); table.getColumnModel().getColumn(0).setMinWidth(250);
currentEntries.clear();
String query = queryField.getText().trim();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
if (client != null && client.getXfer() != null) {
if (hostType == FTConfig.HostType.CMS) {
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(query);
currentEntries.addAll(cmsEntries);
for (CMSDirectoryEntry entry : cmsEntries) {
tableModel.addRow(new Object[]{
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
});
}
} else {
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(query);
currentEntries.addAll(tsoEntries);
for (TSODirectoryEntry entry : tsoEntries) {
tableModel.addRow(new Object[]{
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(),
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents()
});
} }
} }
} }
}
statusLabel.setText(currentEntries.size() + " entries found."); private void onFilterOrParse() {
String query = queryField.getText().trim();
// If text contains multiple lines, treat it as pasted raw text to parse
if (query.contains("\n") || query.contains("\r") || query.toUpperCase().contains("NONVSAM")
|| query.toUpperCase().contains("IN-CAT") || query.toUpperCase().contains("FILENAME")) {
parseText(query, "query text");
} else if (allEntries.isEmpty() && client != null && client.getConnectionState().isFullSession()) {
queryHost();
} else {
applyFilter();
}
}
private void queryHost() {
if (client == null || !client.getConnectionState().isFullSession()) {
statusLabel.setText("No active session to query.");
return;
}
String pattern = queryField.getText().trim();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
statusLabel.setText("Querying host (" + hostType + ")...");
if (queryHostBtn != null) queryHostBtn.setEnabled(false);
SwingWorker<String, Void> worker = new SwingWorker<>() {
@Override
protected String doInBackground() throws Exception {
if (client == null || client.getPS() == null) return "";
StringBuilder allPages = new StringBuilder();
int maxPages = 500;
if (hostType == FTConfig.HostType.CMS) {
// CMS Query:
// 1. Clear presentation space first to ensure clean state
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
// 2. Format CMS LISTFILE query command (default to minidisk A unless specified)
String cmsFilter = formatCMSFilter(pattern);
client.getPS().SendKeys("LISTFILE " + cmsFilter + " ( nohead alloc date[enter]");
Thread.sleep(100);
// 3. Multi-page collection loop:
// CMS pagination displays MORE... or HOLDING when a screen is full.
// When advanced with [clear], CMS flashes RUNNING while generating the next screen.
// We poll up to 10 seconds per page for MORE... / HOLDING or Ready;
for (int page = 0; page < maxPages; page++) {
long startWait = System.currentTimeMillis();
long timeoutMs = 10000; // 10 seconds per page
String screen = "";
boolean isMore = false;
while (System.currentTimeMillis() - startWait < timeoutMs) {
waitForKeyboardUnlock(client, 500);
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
screen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
String upper = screen.toUpperCase();
if (upper.contains("MORE...") || upper.contains("HOLDING")) {
isMore = true;
break;
}
if (upper.contains("READY;") || upper.contains("READY(")) {
break;
}
// Still RUNNING or transitioning, wait and re-check
Thread.sleep(150);
}
allPages.append(screen).append("\n");
if (isMore) {
// In CMS, sending [clear] advances to the next page of output
client.getPS().SendKeys("[clear]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 3000);
} else {
// Ready; encountered or timed out -> listing complete
break;
}
}
// Automatically clear the listing from the terminal presentation space
// so the terminal screen is left clean for the user while keeping the dialog populated
if (client.getPS() != null) {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
}
} else {
// TSO Query:
// Check if current screen is in ISPF (formatted screen or containing ===> or ISPF)
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
String initialScreen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
boolean isFormatted = client.getScreenBuffer() != null && client.getScreenBuffer().isFormatted();
boolean hasCmdArrow = initialScreen.contains("===>");
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
String tsoCmd = "TSO LISTCAT ALL";
if (!pattern.isEmpty()) {
String dsnPattern = pattern.trim();
if (dsnPattern.startsWith("'") && dsnPattern.endsWith("'") && dsnPattern.length() > 2) {
dsnPattern = dsnPattern.substring(1, dsnPattern.length() - 1);
}
int paren = dsnPattern.indexOf('(');
if (paren > 0) {
dsnPattern = dsnPattern.substring(0, paren);
}
if (dsnPattern.equals("*") || dsnPattern.equals("* * *") || dsnPattern.equals("*.*")) {
tsoCmd = "TSO LISTCAT ALL";
} else {
String level = dsnPattern;
if (level.endsWith(".*")) {
level = level.substring(0, level.length() - 2);
} else if (level.endsWith("*")) {
level = level.substring(0, level.length() - 1);
}
if (level.isEmpty()) {
tsoCmd = "TSO LISTCAT ALL";
} else {
tsoCmd = "TSO LISTCAT LEVEL(" + level + ") ALL";
}
}
}
if (isIspf) {
// In ISPF, do NOT send [clear] because [clear] destroys the ISPF panel.
// Locate the command/option line (indicated by ===>) or home to first input field.
int arrowPos = initialScreen.indexOf("===>");
if (arrowPos >= 0 && client.getScreenBuffer() != null) {
int inputAddr = client.getScreenBuffer().findNextUnprotected(arrowPos);
if (inputAddr > 0) {
client.getScreenBuffer().setCursorAddress(inputAddr);
client.getScreenBuffer().updateDisplaySnapshot();
} else {
client.getPS().SendKeys("[home]");
}
} else {
client.getPS().SendKeys("[home]");
}
client.getPS().SendKeys("[erase_eof]");
client.getPS().SendKeys(tsoCmd + "[enter]");
} else {
// In native TSO (ISPF closed, line mode READY prompt)
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
client.getPS().SendKeys(tsoCmd + "[enter]");
}
Thread.sleep(100);
// Multi-page collection loop for TSO:
// 1. Wait for output to start appearing after sending command
long startCmd = System.currentTimeMillis();
while (System.currentTimeMillis() - startCmd < 4000) {
waitForKeyboardUnlock(client, 200);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String s = client.getPS().copyString(0, 0, r - 1, c - 1);
String u = s.toUpperCase();
if (u.contains("NONVSAM") || u.contains("CLUSTER") || u.contains("GDG")
|| u.contains("ALIAS") || u.contains("IN-CAT") || u.contains("IDCAMS")
|| u.contains("***") || u.contains("READY") || u.contains("NOT FOUND")
|| u.contains("NO ENTRY") || s.contains(" --- ")
|| u.contains("--RECFM-") || u.contains("--VOLUMES--")) {
break;
}
Thread.sleep(80);
}
// 2. Multi-page collection
String prevScreen = "";
for (int page = 0; page < maxPages; page++) {
waitForKeyboardUnlock(client, 300);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String screen = client.getPS().copyString(0, 0, r - 1, c - 1);
String upper = screen.toUpperCase();
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
break;
}
prevScreen = screen;
allPages.append(screen).append("\n");
if (upper.contains("***")) {
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 2000);
} else if (upper.contains("NONVSAM") || upper.contains("CLUSTER") || upper.contains("GDG")
|| upper.contains("ALIAS") || upper.contains("IDCAMS") || screen.contains(" --- ")
|| upper.contains("--RECFM-") || upper.contains("--VOLUMES--")) {
// If screen shows dataset data without ***, send [enter] once to see if *** or more output is triggered
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 1000);
int r2 = client.getPS().getRows();
int c2 = client.getPS().getCols();
String s2 = client.getPS().copyString(0, 0, r2 - 1, c2 - 1);
if (s2.toUpperCase().contains("***")) {
prevScreen = s2;
allPages.append(s2).append("\n");
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 2000);
continue;
} else {
if (!s2.equals(screen) && !s2.trim().isEmpty()) {
allPages.append(s2).append("\n");
}
break;
}
} else {
break;
}
}
// 3. Terminal clean-up: clear the list from the terminal presentation space
if (client.getPS() != null) {
if (isIspf) {
client.getPS().SendKeys("[enter]");
Thread.sleep(150);
waitForKeyboardUnlock(client, 1500);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String currentScr = client.getPS().copyString(0, 0, r - 1, c - 1);
if (currentScr.toUpperCase().contains("NONVSAM") || currentScr.toUpperCase().contains("IDCAMS")
|| currentScr.contains(" --- ") || currentScr.toUpperCase().contains("--RECFM-")
|| (client.getScreenBuffer() != null && !client.getScreenBuffer().isFormatted())) {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
} else {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
}
}
return allPages.toString();
}
@Override
protected void done() {
if (queryHostBtn != null) queryHostBtn.setEnabled(true);
try {
String screenText = get();
parseText(screenText, "host query");
if (!pattern.isEmpty() && !allEntries.isEmpty()) {
applyFilter();
}
} catch (Exception ex) {
statusLabel.setText("Host query failed: " + ex.getMessage());
}
}
};
worker.execute();
}
private void inspectSelectedDataset() {
int row = table.getSelectedRow();
if (row < 0 || row >= currentEntries.size()) {
JOptionPane.showMessageDialog(this, "Please select an entry from the table to inspect.",
"No Selection", JOptionPane.INFORMATION_MESSAGE);
return;
}
HostDirectoryEntry entry = currentEntries.get(row);
if (!(entry instanceof TSODirectoryEntry)) {
JOptionPane.showMessageDialog(this, "Inspection is only supported for TSO datasets.",
"Not Supported", JOptionPane.INFORMATION_MESSAGE);
return;
}
if (client == null || !client.getConnectionState().isFullSession()) {
JOptionPane.showMessageDialog(this, "No active full 3270 session to query host.",
"Not Connected", JOptionPane.WARNING_MESSAGE);
return;
}
TSODirectoryEntry tso = (TSODirectoryEntry) entry;
String dsn = tso.getDatasetName();
if (dsn == null || dsn.trim().isEmpty()) {
return;
}
statusLabel.setText("Inspecting " + dsn + " via TSO LISTDS...");
SwingWorker<String, Void> worker = new SwingWorker<String, Void>() {
@Override
protected String doInBackground() throws Exception {
StringBuilder output = new StringBuilder();
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
String initialScreen = client.getPS().copyString(0, 0, rows - 1, cols - 1);
boolean isFormatted = client.getScreenBuffer() != null && client.getScreenBuffer().isFormatted();
boolean hasCmdArrow = initialScreen.contains("===>");
boolean isIspf = isFormatted || hasCmdArrow || initialScreen.contains("ISPF");
String tsoCmd = "TSO LISTDS '" + dsn + "' STATUS";
if (isIspf) {
int arrowPos = initialScreen.indexOf("===>");
if (arrowPos >= 0 && client.getScreenBuffer() != null) {
int inputAddr = client.getScreenBuffer().findNextUnprotected(arrowPos);
if (inputAddr > 0) {
client.getScreenBuffer().setCursorAddress(inputAddr);
client.getScreenBuffer().updateDisplaySnapshot();
} else {
client.getPS().SendKeys("[home]");
}
} else {
client.getPS().SendKeys("[home]");
}
client.getPS().SendKeys("[erase_eof]");
client.getPS().SendKeys(tsoCmd + "[enter]");
} else {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 3000);
client.getPS().SendKeys(tsoCmd + "[enter]");
}
Thread.sleep(100);
// Wait for output to start appearing
long startCmd = System.currentTimeMillis();
while (System.currentTimeMillis() - startCmd < 4000) {
waitForKeyboardUnlock(client, 200);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String s = client.getPS().copyString(0, 0, r - 1, c - 1);
String u = s.toUpperCase();
if (u.contains("--RECFM-") || u.contains("--VOLUMES--") || u.contains("***")
|| u.contains("READY") || u.contains("NOT IN CATALOG") || u.contains("NOT FOUND")) {
break;
}
Thread.sleep(80);
}
String prevScreen = "";
for (int page = 0; page < 5; page++) {
waitForKeyboardUnlock(client, 300);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String screen = client.getPS().copyString(0, 0, r - 1, c - 1);
String upper = screen.toUpperCase();
if (screen.equals(prevScreen) || screen.trim().isEmpty()) {
break;
}
prevScreen = screen;
output.append(screen).append("\n");
if (upper.contains("***")) {
client.getPS().SendKeys("[enter]");
Thread.sleep(100);
waitForKeyboardUnlock(client, 2000);
} else {
break;
}
}
// Terminal cleanup
if (client.getPS() != null) {
if (isIspf) {
client.getPS().SendKeys("[enter]");
Thread.sleep(150);
waitForKeyboardUnlock(client, 1500);
int r = client.getPS().getRows();
int c = client.getPS().getCols();
String currentScr = client.getPS().copyString(0, 0, r - 1, c - 1);
if (currentScr.toUpperCase().contains("--RECFM-") || (client.getScreenBuffer() != null && !client.getScreenBuffer().isFormatted())) {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
} else {
client.getPS().SendKeys("[clear]");
waitForKeyboardUnlock(client, 2000);
}
}
return output.toString();
}
@Override
protected void done() {
try {
String outText = get();
List<TSODirectoryEntry> parsed = TSODirectoryParser.parse(outText);
if (!parsed.isEmpty()) {
TSODirectoryEntry updated = parsed.get(0);
if (updated.getRecfm() != null) tso.setRecfm(updated.getRecfm());
if (updated.getLrecl() > 0) tso.setLrecl(updated.getLrecl());
if (updated.getBlksize() > 0) tso.setBlksize(updated.getBlksize());
if (updated.getDsorg() != null) tso.setDsorg(updated.getDsorg());
if (updated.getVolume() != null && (tso.getVolume() == null || tso.getVolume().equals("******") || tso.getVolume().isEmpty())) {
tso.setVolume(updated.getVolume());
}
// Update table row in view
int modelRow = table.convertRowIndexToModel(row);
if (modelRow >= 0 && modelRow < tableModel.getRowCount()) {
tableModel.setValueAt(tso.getVolume() != null ? tso.getVolume() : "", modelRow, 1);
}
statusLabel.setText("Attributes for " + dsn + ": RECFM=" + tso.getRecfm() + ", LRECL=" + tso.getLrecl() + ", BLKSIZE=" + tso.getBlksize() + ", VOL=" + tso.getVolume());
StringBuilder msg = new StringBuilder();
msg.append("Dataset: ").append(dsn).append("\n\n");
msg.append("Volume: ").append(tso.getVolume() != null ? tso.getVolume() : "N/A").append("\n");
msg.append("Record Format (RECFM): ").append(tso.getRecfm() != null ? tso.getRecfm() : "N/A").append("\n");
msg.append("Logical Record Length (LRECL): ").append(tso.getLrecl() > 0 ? String.valueOf(tso.getLrecl()) : "N/A").append("\n");
msg.append("Block Size (BLKSIZE): ").append(tso.getBlksize() > 0 ? String.valueOf(tso.getBlksize()) : "N/A").append("\n");
msg.append("Dataset Organization (DSORG): ").append(tso.getDsorg() != null ? tso.getDsorg() : "N/A").append("\n");
if (tso.getTracksAllocated() > 0) {
msg.append("Allocated Tracks: ").append(tso.getTracksAllocated()).append("\n");
}
if (tso.getExtents() > 0) {
msg.append("Extents: ").append(tso.getExtents()).append("\n");
}
JOptionPane.showMessageDialog(HostDirectoryDialog.this, msg.toString(), "Dataset Attributes (LISTDS)", JOptionPane.INFORMATION_MESSAGE);
} else {
statusLabel.setText("No VTOC attributes found for " + dsn);
}
} catch (Exception ex) {
statusLabel.setText("Inspection failed: " + ex.getMessage());
}
}
};
worker.execute();
}
private static boolean waitForKeyboardUnlock(Telnet3270Client client, long timeoutMs) {
if (client == null) return false;
long deadline = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < deadline) {
boolean locked = false;
if (client.getInputProcessor() != null && client.getInputProcessor().isKeyboardLocked()) {
locked = true;
}
if (client.getOIA() != null && (client.getOIA().isKeyLocked() || client.getOIA().isXSystem())) {
locked = true;
}
if (!locked) {
return true;
}
try {
Thread.sleep(40);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
} }
private void parseCurrentScreen() { private void parseCurrentScreen() {
if (client == null || !client.getConnectionState().isFullSession()) { if (client == null || !client.getConnectionState().isFullSession() || client.getPS() == null) {
statusLabel.setText("No active session to parse."); statusLabel.setText("No active session to parse.");
return; return;
} }
int rows = client.getPS().getRows();
int cols = client.getPS().getCols();
String screenText = client.getPS().copyString(0, 0, rows - 1, cols - 1);
parseText(screenText, "presentation space");
}
private void parseClipboard() {
try {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
Transferable contents = clipboard.getContents(null);
if (contents != null && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
String clipText = (String) contents.getTransferData(DataFlavor.stringFlavor);
if (clipText != null && !clipText.trim().isEmpty()) {
parseText(clipText, "clipboard");
return;
}
}
statusLabel.setText("Clipboard contains no text.");
} catch (Exception ex) {
statusLabel.setText("Error reading clipboard: " + ex.getMessage());
}
}
private void parseText(String text, String source) {
setupTableColumns(); setupTableColumns();
currentEntries.clear(); allEntries.clear();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem(); FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
String screenText = client.getPS().getString(0, client.getPS().getSize());
if (hostType == FTConfig.HostType.CMS) { if (hostType == FTConfig.HostType.CMS) {
List<CMSDirectoryEntry> cmsEntries = client.getXfer().getCmsDirectory(screenText); List<CMSDirectoryEntry> cmsEntries = CMSDirectoryParser.parse(text);
currentEntries.addAll(cmsEntries); allEntries.addAll(cmsEntries);
for (CMSDirectoryEntry entry : cmsEntries) {
tableModel.addRow(new Object[]{
entry.getFilename(), entry.getFiletype(), entry.getFilemode(),
entry.getRecfm(), entry.getLrecl(), entry.getNumRecords(), entry.getDate() + " " + entry.getTime()
});
}
} else { } else {
List<TSODirectoryEntry> tsoEntries = client.getXfer().getTsoDirectory(screenText); List<TSODirectoryEntry> tsoEntries = TSODirectoryParser.parse(text);
currentEntries.addAll(tsoEntries); allEntries.addAll(tsoEntries);
for (TSODirectoryEntry entry : tsoEntries) { }
applyFilter();
if (allEntries.isEmpty()) {
statusLabel.setText("0 entries found from " + source + ". Use 'Query Host' or 'Paste Clipboard'.");
} else {
statusLabel.setText("Parsed " + allEntries.size() + " entries from " + source + ".");
}
}
private void applyFilter() {
tableModel.setRowCount(0);
currentEntries.clear();
String filter = queryField.getText().trim();
FTConfig.HostType hostType = (FTConfig.HostType) hostTypeCombo.getSelectedItem();
for (HostDirectoryEntry entry : allEntries) {
if (!matchesFilter(entry, filter)) {
continue;
}
currentEntries.add(entry);
if (hostType == FTConfig.HostType.CMS && entry instanceof CMSDirectoryEntry) {
CMSDirectoryEntry cms = (CMSDirectoryEntry) entry;
tableModel.addRow(new Object[]{ tableModel.addRow(new Object[]{
entry.getDatasetName(), entry.getVolume(), entry.getRecfm(), cms.getFilename() != null ? cms.getFilename() : "",
entry.getLrecl(), entry.getBlksize(), entry.getTracksAllocated(), entry.getExtents() cms.getFiletype() != null ? cms.getFiletype() : "",
cms.getFilemode() != null ? cms.getFilemode() : "",
cms.getRecfm() != null ? cms.getRecfm() : "",
cms.getLrecl() > 0 ? String.valueOf(cms.getLrecl()) : "",
cms.getNumRecords() > 0 ? String.valueOf(cms.getNumRecords()) : "",
(cms.getDate() != null ? cms.getDate() : "") + " " + (cms.getTime() != null ? cms.getTime() : "")
});
} else if (entry instanceof TSODirectoryEntry) {
TSODirectoryEntry tso = (TSODirectoryEntry) entry;
tableModel.addRow(new Object[]{
tso.getDatasetName() != null ? tso.getDatasetName() : "",
tso.getVolume() != null ? tso.getVolume() : ""
}); });
} }
} }
statusLabel.setText("Parsed " + currentEntries.size() + " entries from presentation space."); if (!allEntries.isEmpty()) {
statusLabel.setText(currentEntries.size() + " entries shown (" + allEntries.size() + " total).");
}
}
public static boolean matchesFilter(HostDirectoryEntry entry, String filter) {
if (filter == null || filter.trim().isEmpty()) {
return true;
}
String cleanFilter = filter.replace("'", "").replace("\"", "").trim();
if (cleanFilter.isEmpty() || cleanFilter.equals("*") || cleanFilter.equals("* * *") || cleanFilter.equals("*.*")) {
return true;
}
if (entry instanceof CMSDirectoryEntry) {
return matchesCmsFilter((CMSDirectoryEntry) entry, cleanFilter);
} else if (entry instanceof TSODirectoryEntry) {
return matchesTsoFilter((TSODirectoryEntry) entry, cleanFilter);
} else {
return matchesWildcard(entry.getName(), cleanFilter);
}
}
public static boolean matchesCmsFilter(CMSDirectoryEntry cms, String filter) {
String fn = cms.getFilename() != null ? cms.getFilename().trim() : "";
String ft = cms.getFiletype() != null ? cms.getFiletype().trim() : "";
String fm = cms.getFilemode() != null ? cms.getFilemode().trim() : "";
String[] tokens = filter.split("\\s+");
if (tokens.length >= 3) {
String fnPat = tokens[0];
String ftPat = tokens[1];
String fmPat = tokens[2];
return matchesWildcard(fn, fnPat)
&& matchesWildcard(ft, ftPat)
&& matchesCmsFilemode(fm, fmPat);
} else if (tokens.length == 2) {
String fnPat = tokens[0];
String ftPat = tokens[1];
return matchesWildcard(fn, fnPat) && matchesWildcard(ft, ftPat);
} else {
String tok = tokens[0];
if (tok.contains(".")) {
int dot = tok.indexOf('.');
String fnPat = tok.substring(0, dot);
String ftPat = tok.substring(dot + 1);
return matchesWildcard(fn, fnPat) && matchesWildcard(ft, ftPat);
}
if (matchesWildcard(fn, tok) || matchesWildcard(ft, tok) || matchesCmsFilemode(fm, tok)) {
return true;
}
return matchesWildcard(cms.getName(), tok);
}
}
public static boolean matchesCmsFilemode(String actualFm, String fmPattern) {
if (fmPattern == null || fmPattern.isEmpty() || fmPattern.equals("*")) {
return true;
}
if (actualFm == null || actualFm.isEmpty()) {
return false;
}
if (fmPattern.length() == 1 && Character.isLetter(fmPattern.charAt(0))) {
return actualFm.toUpperCase().startsWith(fmPattern.toUpperCase());
}
return matchesWildcard(actualFm, fmPattern);
}
public static boolean matchesTsoFilter(TSODirectoryEntry tso, String filter) {
String dsn = tso.getDatasetName() != null ? tso.getDatasetName().trim() : "";
if (filter.equals("*") || filter.equals("* * *") || filter.equals("*.*")) {
return true;
}
if (filter.contains("*") || filter.contains("?")) {
return matchesWildcard(dsn, filter);
}
return dsn.toUpperCase().contains(filter.toUpperCase());
}
public static boolean matchesWildcard(String text, String pattern) {
if (text == null) text = "";
if (pattern == null || pattern.isEmpty() || pattern.equals("*")) return true;
StringBuilder regex = new StringBuilder("^");
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
if (c == '*') {
regex.append(".*");
} else if (c == '?') {
regex.append(".");
} else if ("\\.[]{}()+^$|".indexOf(c) >= 0) {
regex.append("\\").append(c);
} else {
regex.append(Character.toUpperCase(c));
}
}
regex.append("$");
return text.toUpperCase().matches(regex.toString());
} }
private void onConfirmSelection() { private void onConfirmSelection() {
@@ -256,6 +909,7 @@ public class HostDirectoryDialog extends JDialog {
HostDirectoryEntry entry = currentEntries.get(row); HostDirectoryEntry entry = currentEntries.get(row);
selectedHostFile = entry.getName(); selectedHostFile = entry.getName();
selectedEntry = entry;
confirmed = true; confirmed = true;
dispose(); dispose();
} }
@@ -267,4 +921,50 @@ public class HostDirectoryDialog extends JDialog {
public String getSelectedHostFile() { public String getSelectedHostFile() {
return selectedHostFile; return selectedHostFile;
} }
public HostDirectoryEntry getSelectedEntry() {
return selectedEntry;
}
public List<HostDirectoryEntry> getAllEntries() {
return allEntries;
}
public List<HostDirectoryEntry> getCurrentEntries() {
return currentEntries;
}
public JTextField getQueryField() {
return queryField;
}
public static String resolveDefaultQuery(FTConfig.HostType hostType, String initialQuery) {
String defaultQuery = initialQuery != null ? initialQuery.trim() : "";
if (hostType == FTConfig.HostType.CMS && (defaultQuery.isEmpty() || defaultQuery.equals("* * *") || defaultQuery.equals("*"))) {
defaultQuery = "* * A";
}
return defaultQuery;
}
public static String formatCMSFilter(String pattern) {
if (pattern == null || pattern.trim().isEmpty()) {
return "* * A";
}
String clean = pattern.replace("'", "").replace("\"", "").trim();
if (clean.equals("* * *") || clean.equals("*")) {
return "* * A";
} else if (clean.contains(".")) {
int dot = clean.indexOf('.');
return clean.substring(0, dot).trim() + " " + clean.substring(dot + 1).trim() + " A";
} else {
String[] parts = clean.split("\\s+");
if (parts.length == 1) {
return parts[0] + " * A";
} else if (parts.length == 2) {
return parts[0] + " " + parts[1] + " A";
} else {
return parts[0] + " " + parts[1] + " " + parts[2];
}
}
}
} }
@@ -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. * 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 * @param reader the reader to parse from
* @return Map of j3270 action names to key stroke bindings * @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 { public static Map<String, String> parseKmp(Reader reader) throws IOException {
BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader); 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; String line;
while ((line = br.readLine()) != null) { 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 // Strip comments
int commentIdx = line.indexOf(';'); int commentIdx = line.indexOf(';');
@@ -262,6 +290,22 @@ public class KeyBindings {
return result; 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) { private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
String[] parts = stmt.split("\\s+", 2); String[] parts = stmt.split("\\s+", 2);
if (parts.length < 2) return; 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) { 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 // Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
String action = resolveMnemonic(right); String action = resolveMnemonic(right);
String keyStroke = null; 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();
}
}
}
@@ -11,12 +11,16 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableModel;
import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader; import javax.swing.table.JTableHeader;
import javax.swing.border.LineBorder; import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder; import javax.swing.border.TitledBorder;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
public class SettingsDialog extends JDialog { public class SettingsDialog extends JDialog {
@@ -42,12 +46,23 @@ public class SettingsDialog extends JDialog {
private JCheckBox blockPasteCheck; private JCheckBox blockPasteCheck;
private JCheckBox enablePasteFromExcelCheck; private JCheckBox enablePasteFromExcelCheck;
private JCheckBox pasteStopAtProtectedCheck; 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 // Connection tab
private JComboBox<Settings.StartupBehavior> startupBehaviorBox; private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
private JPanel autoConnectPanel; private JPanel autoConnectPanel;
private JTextField hostField; private JComboBox<SavedHost> savedHostBox;
private JTextField portField;
private JCheckBox autoReconnectCheck; private JCheckBox autoReconnectCheck;
private JSpinner dynamicRowsSpinner; private JSpinner dynamicRowsSpinner;
private JSpinner dynamicColsSpinner; private JSpinner dynamicColsSpinner;
@@ -71,6 +86,8 @@ public class SettingsDialog extends JDialog {
// Bottom action buttons // Bottom action buttons
private JButton btnImport; private JButton btnImport;
private JButton btnExport; private JButton btnExport;
private JButton btnImportConnections;
private JButton btnExportConnections;
// Colors tab component tracking // Colors tab component tracking
private final JPanel[] hostColorSwatches = new JPanel[16]; private final JPanel[] hostColorSwatches = new JPanel[16];
@@ -82,7 +99,7 @@ public class SettingsDialog extends JDialog {
AppIcon.applyTo(this); AppIcon.applyTo(this);
initComponents(); initComponents();
setSize(660, 540); setSize(680, 650);
setLocationRelativeTo(parent); setLocationRelativeTo(parent);
} }
@@ -95,13 +112,18 @@ public class SettingsDialog extends JDialog {
tabbedPane.addTab("Connection", createConnectionPanel()); tabbedPane.addTab("Connection", createConnectionPanel());
tabbedPane.addTab("Entry Assist", createEntryAssistPanel()); tabbedPane.addTab("Entry Assist", createEntryAssistPanel());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
btnImport = new JButton("Import Config..."); btnImport = new JButton("Import Config...");
ThemeManager.styleButton(btnImport, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(btnImport, ThemeManager.ButtonVariant.DEFAULT);
btnExport = new JButton("Export Config..."); btnExport = new JButton("Export Config...");
ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT);
btnImportConnections = new JButton("Import Connections...");
ThemeManager.styleButton(btnImportConnections, ThemeManager.ButtonVariant.DEFAULT);
btnExportConnections = new JButton("Export Connections...");
ThemeManager.styleButton(btnExportConnections, ThemeManager.ButtonVariant.DEFAULT);
JButton btnApply = new JButton("Apply"); JButton btnApply = new JButton("Apply");
ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(btnApply, ThemeManager.ButtonVariant.DEFAULT);
@@ -119,6 +141,14 @@ public class SettingsDialog extends JDialog {
exportConfig(); exportConfig();
}); });
btnImportConnections.addActionListener((ActionEvent e) -> {
importConnections();
});
btnExportConnections.addActionListener((ActionEvent e) -> {
exportConnections();
});
btnOk.addActionListener((ActionEvent e) -> { btnOk.addActionListener((ActionEvent e) -> {
boolean success = applySettings(); boolean success = applySettings();
if (success) { if (success) {
@@ -134,12 +164,28 @@ public class SettingsDialog extends JDialog {
dispose(); dispose();
}); });
buttonPanel.add(btnImport); JPanel buttonPanel = new JPanel(new GridLayout(3, 1, 0, 4));
buttonPanel.add(btnExport); buttonPanel.setBorder(BorderFactory.createEmptyBorder(4, 8, 8, 8));
buttonPanel.add(Box.createHorizontalStrut(20));
buttonPanel.add(btnApply); JPanel row1 = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
buttonPanel.add(btnCancel); row1.setOpaque(false);
buttonPanel.add(btnOk); row1.add(btnImport);
row1.add(btnExport);
JPanel row2 = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
row2.setOpaque(false);
row2.add(btnImportConnections);
row2.add(btnExportConnections);
JPanel row3 = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 2));
row3.setOpaque(false);
row3.add(btnApply);
row3.add(btnCancel);
row3.add(btnOk);
buttonPanel.add(row1);
buttonPanel.add(row2);
buttonPanel.add(row3);
getContentPane().add(tabbedPane, BorderLayout.CENTER); getContentPane().add(tabbedPane, BorderLayout.CENTER);
getContentPane().add(buttonPanel, BorderLayout.SOUTH); getContentPane().add(buttonPanel, BorderLayout.SOUTH);
@@ -338,7 +384,17 @@ public class SettingsDialog extends JDialog {
JTabbedPane behaviorTabs = new JTabbedPane(); JTabbedPane behaviorTabs = new JTabbedPane();
ThemeManager.styleTabbedPane(behaviorTabs); 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()); behaviorTabs.addTab("Keymap", createKeymapPanel());
panel.add(behaviorTabs, BorderLayout.CENTER); panel.add(behaviorTabs, BorderLayout.CENTER);
@@ -346,12 +402,13 @@ public class SettingsDialog extends JDialog {
} }
private JPanel createGeneralBehaviorPanel() { private JPanel createGeneralBehaviorPanel() {
JPanel panel = new JPanel(); ScrollablePanel panel = new ScrollablePanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// Group 1: Typing & Operational Modes // Group 1: Typing & Operational Modes
JPanel typingGroup = new JPanel(new GridBagLayout()); JPanel typingGroup = new JPanel(new GridBagLayout());
typingGroup.setAlignmentX(Component.LEFT_ALIGNMENT);
typingGroup.setBorder(BorderFactory.createTitledBorder("Typing & Operational Modes")); typingGroup.setBorder(BorderFactory.createTitledBorder("Typing & Operational Modes"));
GridBagConstraints gbc = new GridBagConstraints(); GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 6, 4, 6); gbc.insets = new Insets(4, 6, 4, 6);
@@ -402,6 +459,7 @@ public class SettingsDialog extends JDialog {
// Group 2: Selection & Clipboard // Group 2: Selection & Clipboard
JPanel selGroup = new JPanel(new GridBagLayout()); JPanel selGroup = new JPanel(new GridBagLayout());
selGroup.setAlignmentX(Component.LEFT_ALIGNMENT);
selGroup.setBorder(BorderFactory.createTitledBorder("Selection & Clipboard")); selGroup.setBorder(BorderFactory.createTitledBorder("Selection & Clipboard"));
GridBagConstraints gbcS = new GridBagConstraints(); GridBagConstraints gbcS = new GridBagConstraints();
gbcS.insets = new Insets(4, 6, 4, 6); gbcS.insets = new Insets(4, 6, 4, 6);
@@ -430,7 +488,106 @@ public class SettingsDialog extends JDialog {
gbcS.gridy = 3; gbcS.gridy = 3;
selGroup.add(pasteStopAtProtectedCheck, gbcS); 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(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()); panel.add(Box.createVerticalGlue());
return panel; return panel;
@@ -465,22 +622,33 @@ public class SettingsDialog extends JDialog {
acGbc.insets = new Insets(5, 5, 5, 5); acGbc.insets = new Insets(5, 5, 5, 5);
acGbc.anchor = GridBagConstraints.WEST; acGbc.anchor = GridBagConstraints.WEST;
JLabel hostLabel = new JLabel("Host:"); JLabel savedConnLabel = new JLabel("Saved Connection:");
acGbc.gridx = 0; acGbc.gridx = 0;
acGbc.gridy = 0; acGbc.gridy = 0;
autoConnectPanel.add(hostLabel, acGbc); autoConnectPanel.add(savedConnLabel, acGbc);
hostField = new JTextField(Settings.getAutoConnectHost(), 15); savedHostBox = new JComboBox<>();
savedHostBox.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (value instanceof SavedHost) {
SavedHost sh = (SavedHost) value;
if (sh.getName() != null && !sh.getName().trim().isEmpty()) {
setText(sh.getName() + " (" + sh.getHost() + ":" + sh.getPort() + ")");
} else {
setText(sh.getHost() + ":" + sh.getPort());
}
} else if (value == null) {
setText("(No saved connections)");
}
return this;
}
});
refreshSavedHostsBox();
acGbc.gridx = 1; acGbc.gridx = 1;
autoConnectPanel.add(hostField, acGbc); acGbc.fill = GridBagConstraints.HORIZONTAL;
autoConnectPanel.add(savedHostBox, acGbc);
JLabel portLabel = new JLabel("Port:");
acGbc.gridx = 2;
autoConnectPanel.add(portLabel, acGbc);
portField = new JTextField(String.valueOf(Settings.getAutoConnectPort()), 4);
acGbc.gridx = 3;
autoConnectPanel.add(portField, acGbc);
gbc.gridx = 0; gbc.gridx = 0;
gbc.gridy = 1; gbc.gridy = 1;
@@ -535,6 +703,30 @@ public class SettingsDialog extends JDialog {
return panel; return panel;
} }
private void refreshSavedHostsBox() {
if (savedHostBox == null) return;
savedHostBox.removeAllItems();
List<SavedHost> savedHosts = HostStorage.getSavedHosts();
SavedHost currentAutoHost = HostStorage.getAutoConnectHost();
SavedHost toSelect = null;
for (SavedHost sh : savedHosts) {
savedHostBox.addItem(sh);
if (currentAutoHost != null && Objects.equals(sh.getId(), currentAutoHost.getId())) {
toSelect = sh;
}
}
if (toSelect != null) {
savedHostBox.setSelectedItem(toSelect);
savedHostBox.setEnabled(true);
} else if (!savedHosts.isEmpty()) {
savedHostBox.setSelectedIndex(0);
savedHostBox.setEnabled(true);
} else {
savedHostBox.addItem(null);
savedHostBox.setEnabled(false);
}
}
private JPanel createEntryAssistPanel() { private JPanel createEntryAssistPanel() {
JPanel panel = new JPanel(); JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
@@ -676,14 +868,23 @@ public class SettingsDialog extends JDialog {
monoPanel.add(p); 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)); 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); 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); ThemeManager.styleButton(btnResetColors, ThemeManager.ButtonVariant.DEFAULT);
btnResetColors.addActionListener(e -> { btnResetColors.addActionListener(e -> {
for (int i=0; i<16; i++) { for (int i=0; i<16; i++) {
@@ -701,7 +902,8 @@ public class SettingsDialog extends JDialog {
} }
JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save."); 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(hostPanel, BorderLayout.NORTH);
main.add(monoPanel, BorderLayout.CENTER); main.add(monoPanel, BorderLayout.CENTER);
@@ -870,7 +1072,7 @@ public class SettingsDialog extends JDialog {
btnPanel.add(btnReset); btnPanel.add(btnReset);
// Import Keymap — import from .kmp file // Import Keymap — import from .kmp file
JButton btnImportKmp = new JButton("Import Keymap..."); JButton btnImportKmp = new JButton("Import .kmp...");
ThemeManager.styleButton(btnImportKmp, ThemeManager.ButtonVariant.DEFAULT); ThemeManager.styleButton(btnImportKmp, ThemeManager.ButtonVariant.DEFAULT);
btnImportKmp.addActionListener(e -> { btnImportKmp.addActionListener(e -> {
JFileChooser chooser = new JFileChooser(); JFileChooser chooser = new JFileChooser();
@@ -908,6 +1110,36 @@ public class SettingsDialog extends JDialog {
}); });
btnPanel.add(btnImportKmp); 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(new JScrollPane(table), BorderLayout.CENTER);
main.add(btnPanel, BorderLayout.SOUTH); main.add(btnPanel, BorderLayout.SOUTH);
@@ -987,6 +1219,36 @@ public class SettingsDialog extends JDialog {
if (pasteStopAtProtectedCheck != null) { if (pasteStopAtProtectedCheck != null) {
Settings.setPasteStopAtProtectedLine(pasteStopAtProtectedCheck.isSelected()); 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 // Apply Connection
if (startupBehaviorBox != null) { if (startupBehaviorBox != null) {
@@ -994,11 +1256,32 @@ public class SettingsDialog extends JDialog {
if (behavior != null) { if (behavior != null) {
Settings.setStartupBehavior(behavior); Settings.setStartupBehavior(behavior);
} }
if (behavior == Settings.StartupBehavior.AUTO_CONNECT && hostField != null && portField != null) { if (behavior == Settings.StartupBehavior.AUTO_CONNECT && savedHostBox != null) {
Settings.setAutoConnectHost(hostField.getText().trim()); Object sel = savedHostBox.getSelectedItem();
try { if (sel instanceof SavedHost) {
Settings.setAutoConnectPort(Integer.parseInt(portField.getText().trim())); SavedHost selected = (SavedHost) sel;
} catch (NumberFormatException ignored) { List<SavedHost> hosts = HostStorage.getSavedHosts();
for (SavedHost h : hosts) {
h.setAutoConnect(Objects.equals(h.getId(), selected.getId()));
}
HostStorage.saveAll(hosts);
Settings.setAutoConnectHost(selected.getHost());
Settings.setAutoConnectPort(selected.getPort());
Settings.setAutoConnectTls(selected.isUseTls());
Settings.setAutoConnectVerifyCert(selected.isTlsVerifyCert());
Settings.setAutoConnectTn3270e(selected.isTn3270e());
}
} else if (behavior != Settings.StartupBehavior.AUTO_CONNECT) {
List<SavedHost> hosts = HostStorage.getSavedHosts();
boolean modified = false;
for (SavedHost h : hosts) {
if (h.isAutoConnect()) {
h.setAutoConnect(false);
modified = true;
}
}
if (modified) {
HostStorage.saveAll(hosts);
} }
} }
} }
@@ -1133,6 +1416,43 @@ public class SettingsDialog extends JDialog {
if (pasteStopAtProtectedCheck != null) { if (pasteStopAtProtectedCheck != null) {
pasteStopAtProtectedCheck.setSelected(Settings.getPasteStopAtProtectedLine()); 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 // Connection
if (startupBehaviorBox != null) { if (startupBehaviorBox != null) {
@@ -1141,12 +1461,7 @@ public class SettingsDialog extends JDialog {
autoConnectPanel.setVisible(startupBehaviorBox.getSelectedItem() == Settings.StartupBehavior.AUTO_CONNECT); autoConnectPanel.setVisible(startupBehaviorBox.getSelectedItem() == Settings.StartupBehavior.AUTO_CONNECT);
} }
} }
if (hostField != null) { refreshSavedHostsBox();
hostField.setText(Settings.getAutoConnectHost());
}
if (portField != null) {
portField.setText(String.valueOf(Settings.getAutoConnectPort()));
}
if (autoReconnectCheck != null) { if (autoReconnectCheck != null) {
autoReconnectCheck.setSelected(Settings.getAutoConnectAutoReconnect()); autoReconnectCheck.setSelected(Settings.getAutoConnectAutoReconnect());
} }
@@ -1226,7 +1541,7 @@ public class SettingsDialog extends JDialog {
// ========== Import Config ========== // ========== Import Config ==========
void importConfigFile(File file) throws IOException { public void importConfigFile(File file) throws IOException {
if (file == null || !file.exists()) { if (file == null || !file.exists()) {
throw new FileNotFoundException("Config file not found: " + (file != null ? file.getAbsolutePath() : "null")); throw new FileNotFoundException("Config file not found: " + (file != null ? file.getAbsolutePath() : "null"));
} }
@@ -1264,6 +1579,102 @@ public class SettingsDialog extends JDialog {
return btnExport; return btnExport;
} }
public JButton getBtnImportConnections() {
return btnImportConnections;
}
public JButton getBtnExportConnections() {
return btnExportConnections;
}
public JComboBox<SavedHost> getSavedHostBox() {
return savedHostBox;
}
// ========== Import / Export Connections ==========
public void importConnectionsFile(File file) throws IOException {
if (file == null || !file.exists()) {
throw new FileNotFoundException("Connections file not found: " + (file != null ? file.getAbsolutePath() : "null"));
}
HostStorage.importFromIniFile(file);
refreshSavedHostsBox();
SavedHost autoHost = HostStorage.getAutoConnectHost();
if (autoHost != null) {
Settings.setAutoConnectHost(autoHost.getHost());
Settings.setAutoConnectPort(autoHost.getPort());
Settings.setAutoConnectTls(autoHost.isUseTls());
Settings.setAutoConnectVerifyCert(autoHost.isTlsVerifyCert());
Settings.setAutoConnectTn3270e(autoHost.isTn3270e());
}
refreshFromSettings();
}
public void exportConnectionsFile(File file) throws IOException {
if (file == null) {
throw new IllegalArgumentException("Target file cannot be null");
}
HostStorage.exportToIniFile(file);
}
private void importConnections() {
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Import Connections");
fc.setSelectedFile(new java.io.File("connections.ini"));
fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("INI Files (*.ini)", "ini"));
int result = fc.showOpenDialog(this);
if (result == JFileChooser.APPROVE_OPTION) {
java.io.File file = fc.getSelectedFile();
try {
importConnectionsFile(file);
int count = HostStorage.getSavedHosts().size();
JOptionPane.showMessageDialog(this,
"Connections imported successfully from:\n" + file.getAbsolutePath() + "\nTotal saved connections: " + count,
"Import Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this,
"Import connections failed: " + ex.getMessage(),
"Import Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void exportConnections() {
JFileChooser fc = new JFileChooser();
fc.setDialogTitle("Export Connections");
fc.setSelectedFile(new java.io.File("connections.ini"));
fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("INI Files (*.ini)", "ini"));
int result = fc.showSaveDialog(this);
if (result == JFileChooser.APPROVE_OPTION) {
java.io.File file = fc.getSelectedFile();
// Ensure .ini extension
if (!file.getName().toLowerCase().endsWith(".ini")) {
file = new java.io.File(file.getAbsolutePath() + ".ini");
}
// Confirm overwrite
if (file.exists()) {
int confirm = JOptionPane.showConfirmDialog(this,
"File already exists. Overwrite?", "Confirm Overwrite",
JOptionPane.YES_NO_OPTION);
if (confirm != JOptionPane.YES_OPTION) return;
}
try {
exportConnectionsFile(file);
JOptionPane.showMessageDialog(this,
"Connections exported to:\n" + file.getAbsolutePath(),
"Export Successful", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this,
"Export connections failed: " + ex.getMessage(),
"Export Error", JOptionPane.ERROR_MESSAGE);
}
}
}
// ========== Export Config ========== // ========== Export Config ==========
private void exportConfig() { private void exportConfig() {
@@ -1302,4 +1713,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 Telnet3270Client client;
private TerminalPanel terminalPanel; private TerminalPanel terminalPanel;
private haus.nightmare.lib3270j.ecl.ECLOIANotify oiaNotifyListener;
public StatusBar() { public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
@@ -131,8 +132,15 @@ public class StatusBar extends JPanel {
} }
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) { 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.client = client;
this.terminalPanel = terminalPanel; this.terminalPanel = terminalPanel;
if (this.client != null && this.client.getOIA() != null) {
this.oiaNotifyListener = event -> updateStatus();
this.client.getOIA().registerOIAEvent(this.oiaNotifyListener);
}
updateStatus(); updateStatus();
} }
@@ -349,9 +357,12 @@ public class StatusBar extends JPanel {
wordWrapStatus.setText(""); wordWrapStatus.setText("");
} }
// Field status: Numeric vs Alphanumeric // Field status: Numeric vs Alphanumeric vs DBCS
if (state.isFullSession() && client.getScreenBuffer().isFormatted()) { if (state.isFullSession() && client.getScreenBuffer().isFormatted()) {
if (client.getOIA().isNumeric()) { if (client.getOIA().isDBCS()) {
fieldTypeStatus.setText("DBCS");
fieldTypeStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else if (client.getOIA().isNumeric()) {
fieldTypeStatus.setText("NUM"); fieldTypeStatus.setText("NUM");
fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme)); fieldTypeStatus.setForeground(ThemeManager.getOiaFgWarn(theme));
} else { } else {
@@ -388,4 +399,8 @@ public class StatusBar extends JPanel {
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr)); cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme)); cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
} }
public String getLockStatusText() {
return lockStatus != null ? lockStatus.getText() : "";
}
} }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,297 @@
package haus.nightmare.j3270;
import haus.nightmare.j3270.ui.ConnectChoiceDialog;
import haus.nightmare.j3270.ui.ThemeManager;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
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 javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ConnectionConflictTest {
private final List<J3270App> createdApps = new ArrayList<>();
@BeforeEach
public void setUp() {
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
try {
app.dispose();
} catch (Exception ignored) {}
}
}
@AfterEach
public void tearDown() {
for (J3270App app : createdApps) {
try {
app.dispose();
} catch (Exception ignored) {}
}
for (J3270App app : new ArrayList<>(J3270App.getOpenWindows())) {
try {
app.dispose();
} catch (Exception ignored) {}
}
}
private J3270App createApp() {
try {
J3270App app = new J3270App();
createdApps.add(app);
return app;
} catch (HeadlessException e) {
return null;
}
}
@Test
@DisplayName("ConnectChoiceDialog components, styling, and action results")
public void testConnectChoiceDialogComponents() {
try {
ConnectChoiceDialog dialog = new ConnectChoiceDialog(null, "mvs.corp.local:23");
assertEquals("Active Connection", dialog.getTitle());
assertNotNull(dialog.getNewWindowButton());
assertNotNull(dialog.getAbortButton());
assertNotNull(dialog.getCancelButton());
assertEquals("New Window", dialog.getNewWindowButton().getText());
assertEquals("Abort Current Connection", dialog.getAbortButton().getText());
assertEquals("Cancel", dialog.getCancelButton().getText());
assertTrue(dialog.getLine1().getText().contains("mvs.corp.local:23"));
assertTrue(dialog.getLine2().getText().contains("new window"));
assertTrue(dialog.getLine2().getText().contains("abort"));
// Test button click choices
dialog.getNewWindowButton().doClick();
assertEquals(ConnectChoiceDialog.Choice.NEW_WINDOW, dialog.getChoice());
ConnectChoiceDialog dialog2 = new ConnectChoiceDialog(null, "cms.test:992");
dialog2.getAbortButton().doClick();
assertEquals(ConnectChoiceDialog.Choice.ABORT_CURRENT, dialog2.getChoice());
ConnectChoiceDialog dialog3 = new ConnectChoiceDialog(null, "cics.test:23");
dialog3.getCancelButton().doClick();
assertEquals(ConnectChoiceDialog.Choice.CANCEL, dialog3.getChoice());
} catch (HeadlessException ignored) {
// Expected in headless CI environments without X11
}
}
@Test
@DisplayName("When connected, Connect with NEW_WINDOW choice opens in a new window and preserves current connection")
public void testConnectChoiceNewWindow() {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
config1.setSavedProfileName("Host 1");
app.connect(config1, false);
assertTrue(app.getConnectedTarget().contains("host1.test") || app.getConnectedTarget().contains("Host 1"));
// Configure handler to return NEW_WINDOW
boolean[] handlerCalled = new boolean[]{false};
app.setConnectChoiceHandler((owner, target) -> {
handlerCalled[0] = true;
assertTrue(target.contains("host1.test") || target.contains("Host 1"));
return ConnectChoiceDialog.Choice.NEW_WINDOW;
});
int initialWindows = J3270App.getOpenWindows().size();
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
config2.setSavedProfileName("Host 2");
app.connect(config2, true);
// Handler was invoked
assertTrue(handlerCalled[0], "Connect choice handler must be called when already connected");
// A new window was opened
assertEquals(initialWindows + 1, J3270App.getOpenWindows().size(),
"A new window should have been added to openWindows");
// Original app remains on config1
assertEquals("host1.test", app.getClient().getConfig().getHost());
// Find the new window
J3270App newWindow = null;
for (J3270App w : J3270App.getOpenWindows()) {
if (w != app) {
newWindow = w;
createdApps.add(w);
break;
}
}
assertNotNull(newWindow, "New window must exist");
assertNotNull(newWindow.getClient(), "New window should have a client");
assertEquals("host2.test", newWindow.getClient().getConfig().getHost(),
"New window must be connected to host2.test");
}
@Test
@DisplayName("When connected, Connect with ABORT_CURRENT choice aborts existing connection and connects in same window")
public void testConnectChoiceAbortCurrent() {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config1, false);
int initialWindows = J3270App.getOpenWindows().size();
boolean[] handlerCalled = new boolean[]{false};
app.setConnectChoiceHandler((owner, target) -> {
handlerCalled[0] = true;
return ConnectChoiceDialog.Choice.ABORT_CURRENT;
});
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
app.connect(config2, true);
assertTrue(handlerCalled[0], "Connect choice handler must be called");
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
"No new window should be created when choosing ABORT_CURRENT");
assertEquals("host2.test", app.getClient().getConfig().getHost(),
"Same window should now be connected to host2.test");
}
@Test
@DisplayName("When connected, Connect with CANCEL choice keeps existing connection and creates no new window")
public void testConnectChoiceCancel() {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config1, false);
int initialWindows = J3270App.getOpenWindows().size();
boolean[] handlerCalled = new boolean[]{false};
app.setConnectChoiceHandler((owner, target) -> {
handlerCalled[0] = true;
return ConnectChoiceDialog.Choice.CANCEL;
});
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
app.connect(config2, true);
assertTrue(handlerCalled[0], "Connect choice handler must be called");
assertEquals(initialWindows, J3270App.getOpenWindows().size(),
"No new window should be created when choosing CANCEL");
assertEquals("host1.test", app.getClient().getConfig().getHost(),
"Existing window must remain connected to host1.test");
}
@Test
@DisplayName("File menu contains Close Window item with VK_W accelerator")
public void testCloseWindowMenuItem() {
J3270App app = createApp();
if (app == null) return;
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb);
JMenu fileMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
if ("File".equals(mb.getMenu(i).getText())) {
fileMenu = mb.getMenu(i);
break;
}
}
assertNotNull(fileMenu, "File menu must exist");
JMenuItem closeWindowItem = null;
for (int i = 0; i < fileMenu.getItemCount(); i++) {
JMenuItem item = fileMenu.getItem(i);
if (item != null && "Close Window".equals(item.getText())) {
closeWindowItem = item;
break;
}
}
assertNotNull(closeWindowItem, "File menu must contain Close Window item");
KeyStroke ks = closeWindowItem.getAccelerator();
assertNotNull(ks, "Close Window item must have an accelerator");
assertEquals(KeyEvent.VK_W, ks.getKeyCode());
}
@Test
@DisplayName("Closing one window when multiple windows are open leaves remaining window open")
public void testMultiWindowCloseIndependent() {
J3270App app1 = createApp();
if (app1 == null) return;
J3270App app2 = createApp();
if (app2 == null) return;
assertTrue(J3270App.getOpenWindows().contains(app1));
assertTrue(J3270App.getOpenWindows().contains(app2));
assertEquals(2, J3270App.getOpenWindows().size());
// Close app1
app1.closeWindow();
assertFalse(J3270App.getOpenWindows().contains(app1), "app1 should be removed from open windows");
assertTrue(J3270App.getOpenWindows().contains(app2), "app2 should remain open");
assertEquals(1, J3270App.getOpenWindows().size());
}
@Test
@DisplayName("Abort current connection cleanly tears down old client and connects new client in same window")
public void testAbortCurrentConnectionGracefullyDisconnects() throws Exception {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config1 = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config1, false);
Telnet3270Client oldClient = app.getClient();
assertNotNull(oldClient);
app.setConnectChoiceHandler((owner, target) -> ConnectChoiceDialog.Choice.ABORT_CURRENT);
ConnectionConfig config2 = new ConnectionConfig("host2.test", 23, TerminalModel.IBM_3278_2);
app.connect(config2, true);
// Client on app is immediately replaced with new client
Telnet3270Client newClient = app.getClient();
assertNotNull(newClient);
assertNotSame(oldClient, newClient);
assertEquals("host2.test", newClient.getConfig().getHost());
// Old client should be disconnected
Thread.sleep(100);
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, oldClient.getConnectionState());
assertFalse(oldClient.isConnected());
}
@Test
@DisplayName("Explicit app.disconnect() tears down client and clears UI state")
public void testExplicitDisconnectResetsUI() throws Exception {
J3270App app = createApp();
if (app == null) return;
ConnectionConfig config = new ConnectionConfig("host1.test", 23, TerminalModel.IBM_3278_2);
app.connect(config, false);
Telnet3270Client client = app.getClient();
assertNotNull(client);
app.disconnect();
assertNull(app.getClient());
assertFalse(app.isConnected());
Thread.sleep(100);
assertEquals(haus.nightmare.lib3270j.ConnectionState.NOT_CONNECTED, client.getConnectionState());
assertFalse(client.isConnected());
}
}
@@ -85,7 +85,8 @@ public class LoggingConfigurationTest {
assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel()); assertEquals(Level.ALL, Logger.getLogger("haus.nightmare.lib3270j").getLevel());
File logFile = new File("j3270.log"); File logFile = new File("j3270.log");
assertTrue(logFile.exists(), "j3270.log should be created when debug is enabled"); File logFile1 = new File("j3270.log.1");
assertTrue(logFile.exists() || logFile1.exists(), "j3270.log should be created when debug is enabled");
// Clean up // Clean up
J3270App.configureLogging(false); J3270App.configureLogging(false);
@@ -0,0 +1,271 @@
package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.j3270.ui.ConnectDialog;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.ft.FTConfig;
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 javax.swing.*;
import java.awt.Frame;
import java.awt.HeadlessException;
import java.io.File;
import java.io.PrintWriter;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
public class ConnectionHostTypeConfigTest {
private List<SavedHost> originalSavedHosts;
private String originalDefaultHostType;
@BeforeEach
public void setUp() {
FileTransferDialog.resetSessionState();
originalSavedHosts = HostStorage.getSavedHosts();
originalDefaultHostType = Settings.getDefaultHostType();
HostStorage.clearAll();
}
@AfterEach
public void tearDown() {
FileTransferDialog.resetSessionState();
HostStorage.clearAll();
if (originalSavedHosts != null) {
HostStorage.saveAll(originalSavedHosts);
}
Settings.setDefaultHostType(originalDefaultHostType);
}
@Test
@DisplayName("SavedHost converts to ConnectionConfig preserving hostType and saved connection metadata")
public void testSavedHostToConnectionConfigPreservesHostType() {
SavedHost cmsHost = new SavedHost(
"cms-uuid-1", "VM/CMS System", "vm370.corp.local", 23, 4, 62, 160, "",
false, "CMS", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
ConnectionConfig cfgCms = cmsHost.toConnectionConfig();
assertTrue(cfgCms.isSavedConnection(), "Config from SavedHost must be marked as saved connection");
assertEquals("cms-uuid-1", cfgCms.getSavedProfileId());
assertEquals("VM/CMS System", cfgCms.getSavedProfileName());
assertEquals("CMS", cfgCms.getHostType());
assertEquals(FTConfig.HostType.CMS, cfgCms.getFtHostType());
SavedHost cicsHost = new SavedHost(
"cics-uuid-2", "CICS Gateway", "cics.corp.local", 23, 2, 24, 80, "LU1",
false, "CICS", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
ConnectionConfig cfgCics = cicsHost.toConnectionConfig();
assertTrue(cfgCics.isSavedConnection());
assertEquals("CICS", cfgCics.getHostType());
assertEquals(FTConfig.HostType.CICS, cfgCics.getFtHostType());
SavedHost tsoHost = new SavedHost(
"tso-uuid-3", "MVS TSO", "tso.corp.local", 23, 4, 43, 80, "",
false, "TSO", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
ConnectionConfig cfgTso = tsoHost.toConnectionConfig();
assertTrue(cfgTso.isSavedConnection());
assertEquals("TSO", cfgTso.getHostType());
assertEquals(FTConfig.HostType.TSO, cfgTso.getFtHostType());
}
@Test
@DisplayName("FileTransferDialog preselects CMS when using a saved connection configured as CMS")
public void testFileTransferDialogPrefillsFromSavedConnectionCms() {
ConnectionConfig config = new ConnectionConfig("vm.example.com", 23);
config.setSavedConnection(true);
config.setSavedProfileId("vm-1");
config.setSavedProfileName("VM370");
config.setHostType("CMS");
Telnet3270Client client = new Telnet3270Client(config);
FileTransfer ft = new FileTransfer(client, null);
try {
FileTransferDialog dialog = new FileTransferDialog(null, ft);
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
"FileTransferDialog must preselect CMS when saved connection is configured as CMS");
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("FileTransferDialog preselects CICS when using a saved connection configured as CICS")
public void testFileTransferDialogPrefillsFromSavedConnectionCics() {
ConnectionConfig config = new ConnectionConfig("cics.example.com", 23);
config.setSavedConnection(true);
config.setSavedProfileId("cics-1");
config.setSavedProfileName("CICS Region");
config.setHostType("CICS");
Telnet3270Client client = new Telnet3270Client(config);
FileTransfer ft = new FileTransfer(client, null);
try {
FileTransferDialog dialog = new FileTransferDialog(null, ft);
assertEquals(FTConfig.HostType.CICS, dialog.getHostTypeCombo().getSelectedItem(),
"FileTransferDialog must preselect CICS when saved connection is configured as CICS");
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("FileTransferDialog overrides stale prior transfer state with configured host type from saved connection")
public void testFileTransferDialogOverridesOldTransferStateWhenSavedConnectionUsed() {
// Simulate a prior transfer performed on a TSO system
FileTransferDialog.TransferSessionState oldState = new FileTransferDialog.TransferSessionState();
oldState.hostType = FTConfig.HostType.TSO;
oldState.localFile = "test.txt";
oldState.hostFile = "USER.DATA";
FileTransferDialog.setLastTransferState(oldState);
// Now connect to a CMS saved connection
ConnectionConfig cmsConfig = new ConnectionConfig("vm.corp.internal", 23);
cmsConfig.setSavedConnection(true);
cmsConfig.setSavedProfileName("VM Prod");
cmsConfig.setHostType("CMS");
Telnet3270Client client = new Telnet3270Client(cmsConfig);
FileTransfer ft = new FileTransfer(client, null);
try {
FileTransferDialog dialog = new FileTransferDialog(null, ft);
assertEquals(FTConfig.HostType.CMS, dialog.getHostTypeCombo().getSelectedItem(),
"FileTransferDialog must use the configured host type (CMS) of the saved connection rather than prior session's TSO");
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("ConnectDialog allows host type configuration and saves it to profile")
public void testConnectDialogHostTypeConfigurationAndProfile() {
try {
ConnectDialog dialog = new ConnectDialog((Frame) null);
assertNotNull(dialog.getHostTypeCombo(), "ConnectDialog must provide a Host Type combo box");
assertEquals("TSO", dialog.getSelectedHostType());
// Switch to CMS
dialog.setInitialHostType("CMS");
assertEquals("CMS", dialog.getSelectedHostType());
assertEquals(FTConfig.HostType.CMS, dialog.getSelectedFtHostType());
// Switch to CICS
dialog.setInitialHostType("CICS");
assertEquals("CICS", dialog.getSelectedHostType());
assertEquals(FTConfig.HostType.CICS, dialog.getSelectedFtHostType());
// Loading a SavedHost profile updates host type in ConnectDialog
SavedHost cmsProfile = new SavedHost(
"cms-prof-1", "CMS Dev", "cms.dev.local", 23, 4, 62, 160, "",
false, "CMS", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
dialog.loadProfile(cmsProfile);
assertEquals("CMS", dialog.getSelectedHostType());
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("Settings persists and exports defaultHostType")
public void testSettingsDefaultHostTypePersistenceAndIni() throws Exception {
Settings.setDefaultHostType("CMS");
assertEquals("CMS", Settings.getDefaultHostType());
File tmpIni = File.createTempFile("j3270_test_cfg", ".ini");
tmpIni.deleteOnExit();
try (PrintWriter writer = new PrintWriter(tmpIni)) {
writer.println("[connection]");
writer.println("hostType = CICS");
}
Settings.loadFromIniFile(tmpIni.getAbsolutePath());
assertEquals("CICS", Settings.getDefaultHostType());
// Test export
File exportFile = File.createTempFile("j3270_export", ".ini");
exportFile.deleteOnExit();
Settings.exportToIniFile(exportFile.getAbsolutePath());
String content = new String(java.nio.file.Files.readAllBytes(exportFile.toPath()));
assertTrue(content.contains("hostType = CICS"), "Exported INI must contain hostType = CICS");
}
@Test
@DisplayName("J3270App setConnectionHostType updates connection config and syncs with profile in HostStorage")
public void testSetConnectionHostTypeUpdatesActiveConnectionAndStorage() {
SavedHost saved = new SavedHost(
"saved-prof-1", "Prod CMS", "vm.prod", 23, 4, 62, 160, "",
false, "TSO", false, true, true, "BOTH", "037",
"NONE", "", 0, "", ""
);
HostStorage.saveHost(saved);
ConnectionConfig config = saved.toConnectionConfig();
assertEquals("TSO", config.getHostType());
try {
J3270App app = new J3270App();
try {
// Verify Connection menu exists in JMenuBar
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb);
boolean hasConnectionMenu = false;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null && "Connection".equals(m.getText())) {
hasConnectionMenu = true;
break;
}
}
assertTrue(hasConnectionMenu, "JMenuBar must include a Connection menu");
// Simulate connecting with this config
app.connect(config);
// Change host type via setConnectionHostType
app.setConnectionHostType(FTConfig.HostType.CMS);
assertEquals(FTConfig.HostType.CMS, app.getClient().getConfig().getFtHostType());
// Profile in storage should also be updated
SavedHost reloaded = HostStorage.findById("saved-prof-1");
assertNotNull(reloaded);
assertEquals("CMS", reloaded.getHostType());
} finally {
app.dispose();
}
} catch (HeadlessException ignored) {
// In headless environments where JFrame cannot be initialized, test model updates directly
Telnet3270Client client = new Telnet3270Client(config);
client.getConfig().setFtHostType(FTConfig.HostType.CMS);
saved.setHostType(FTConfig.HostType.CMS.name());
HostStorage.saveHost(saved);
SavedHost reloaded = HostStorage.findById("saved-prof-1");
assertNotNull(reloaded);
assertEquals("CMS", reloaded.getHostType());
}
}
}
@@ -1,6 +1,9 @@
package haus.nightmare.j3270.ft; package haus.nightmare.j3270.ft;
import haus.nightmare.j3270.ui.HostDirectoryDialog;
import haus.nightmare.lib3270j.ft.FTConfig; import haus.nightmare.lib3270j.ft.FTConfig;
import haus.nightmare.lib3270j.ft.dir.CMSDirectoryEntry;
import haus.nightmare.lib3270j.ft.dir.TSODirectoryEntry;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -99,4 +102,114 @@ public class FileTransferSessionAndCmsTest {
// Headless environment; verified through state retention // Headless environment; verified through state retention
} }
} }
@Test
public void testPdsSessionStatePersistence() {
FileTransferDialog.TransferSessionState state = new FileTransferDialog.TransferSessionState();
state.hostType = FTConfig.HostType.TSO;
state.isSend = true;
state.hostFile = "'USER.PDS.DATA'";
state.units = FTConfig.AllocationUnit.CYLINDERS;
state.space = "10,5";
state.dirBlocks = "20";
FileTransferDialog.setLastTransferState(state);
FileTransferDialog.TransferSessionState retrieved = FileTransferDialog.getLastTransferState();
assertNotNull(retrieved);
assertEquals(FTConfig.AllocationUnit.CYLINDERS, retrieved.units);
assertEquals("10,5", retrieved.space);
assertEquals("20", retrieved.dirBlocks);
try {
FileTransferDialog dialog = new FileTransferDialog(null, null);
assertEquals(FTConfig.AllocationUnit.CYLINDERS, dialog.getUnitsCombo().getSelectedItem());
assertEquals("10,5", dialog.getSpaceField().getText());
assertEquals("20", dialog.getDirField().getText());
dialog.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
public void testHostDirectoryDialogStructureAndFilter() {
// Test default query resolution
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, null));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, ""));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, " "));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "* * *"));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "*"));
assertEquals("TEST EXEC", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.CMS, "TEST EXEC"));
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, null));
assertEquals("", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, ""));
assertEquals("'MY.DATASET'", haus.nightmare.j3270.ui.HostDirectoryDialog.resolveDefaultQuery(FTConfig.HostType.TSO, "'MY.DATASET'"));
// Test CMS filter formatting
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(null));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter(""));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("* * *"));
assertEquals("* * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("*"));
assertEquals("TEST * A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST"));
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC"));
assertEquals("TEST EXEC B", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST EXEC B"));
assertEquals("TEST EXEC A", haus.nightmare.j3270.ui.HostDirectoryDialog.formatCMSFilter("TEST.EXEC"));
try {
haus.nightmare.j3270.ui.HostDirectoryDialog dialog =
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "");
assertNotNull(dialog);
assertFalse(dialog.isConfirmed());
assertNull(dialog.getSelectedHostFile());
assertEquals("* * A", dialog.getQueryField().getText());
dialog.dispose();
haus.nightmare.j3270.ui.HostDirectoryDialog dialog2 =
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "* * *");
assertEquals("* * A", dialog2.getQueryField().getText());
dialog2.dispose();
haus.nightmare.j3270.ui.HostDirectoryDialog dialog3 =
new haus.nightmare.j3270.ui.HostDirectoryDialog(null, null, FTConfig.HostType.CMS, "TEST EXEC");
assertEquals("TEST EXEC", dialog3.getQueryField().getText());
dialog3.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
public void testMatchesFilterWildcards() {
CMSDirectoryEntry cmsA = new CMSDirectoryEntry("PROFILE", "EXEC", "A1");
CMSDirectoryEntry cmsB = new CMSDirectoryEntry("PROFILE", "EXEC", "B1");
CMSDirectoryEntry cmsSys = new CMSDirectoryEntry("SYSTEM", "CONFIG", "S2");
// * * A matches A disk files (A1, A0, etc.), excludes B and S
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* * A"));
assertFalse(HostDirectoryDialog.matchesFilter(cmsB, "* * A"));
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* * A"));
// Blank, *, * * * matches all
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, ""));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsB, "* * *"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsSys, "* * *"));
// Two tokens (fn ft) and dot tokens
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "* EXEC"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "*.EXEC"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROFILE *"));
assertTrue(HostDirectoryDialog.matchesFilter(cmsA, "PROF*.*"));
assertFalse(HostDirectoryDialog.matchesFilter(cmsSys, "* EXEC"));
// TSO Wildcards
TSODirectoryEntry tso = new TSODirectoryEntry("TSOUSER.TEST.DATA");
assertTrue(HostDirectoryDialog.matchesFilter(tso, ""));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "* * *"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "*.DATA"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TEST"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "TSOUSER.*"));
assertTrue(HostDirectoryDialog.matchesFilter(tso, "'TSOUSER.TEST.*'"));
assertFalse(HostDirectoryDialog.matchesFilter(tso, "*.CNTL"));
}
} }
@@ -0,0 +1,342 @@
package haus.nightmare.j3270.storage;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.j3270.ui.SettingsDialog;
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 javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ConnectionIniStorageTest {
private List<SavedHost> originalSavedHosts;
private String origHost;
private int origPort;
@BeforeEach
public void setUp() {
originalSavedHosts = HostStorage.getSavedHosts();
origHost = Settings.getAutoConnectHost();
origPort = Settings.getAutoConnectPort();
}
@AfterEach
public void tearDown() {
if (originalSavedHosts != null) {
HostStorage.saveAll(originalSavedHosts);
} else {
HostStorage.clearAll();
}
Settings.setAutoConnectHost(origHost);
Settings.setAutoConnectPort(origPort);
}
@Test
@DisplayName("HostStorage exports to and loads from connections.ini with full fidelity")
public void testExportAndLoadConnectionsIniRoundtrip() throws IOException {
List<SavedHost> hosts = new ArrayList<>();
SavedHost h1 = new SavedHost("id-mainframe-1", "Production TSO", "mainframe.corp.com", 23, 4, 62, 160,
"LU01", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
SavedHost h2 = new SavedHost("id-vm-2", "VM CMS Test", "vmcms.lab.net", 992, 2, 24, 80,
"LUCMS", false, "CMS", true, false, true, "GDDM", "1047", "SOCKS5", "10.0.0.1", 1080, "proxyuser", "proxypass");
hosts.add(h1);
hosts.add(h2);
File tmpIni = File.createTempFile("connections_test", ".ini");
tmpIni.deleteOnExit();
try {
HostStorage.exportToIniFile(tmpIni, hosts);
assertTrue(tmpIni.exists());
String content = new String(Files.readAllBytes(tmpIni.toPath()));
assertTrue(content.contains("[Production TSO]"), "Must contain section header [Production TSO]");
assertTrue(content.contains("[VM CMS Test]"), "Must contain section header [VM CMS Test]");
assertTrue(content.contains("host = mainframe.corp.com"));
assertTrue(content.contains("proxyHost = 10.0.0.1"));
assertTrue(content.contains("proxyType = SOCKS5"));
List<SavedHost> loaded = HostStorage.loadFromIniFile(tmpIni);
assertEquals(2, loaded.size(), "Should load 2 connections from INI");
SavedHost loaded1 = loaded.get(0);
assertEquals("id-mainframe-1", loaded1.getId());
assertEquals("Production TSO", loaded1.getName());
assertEquals("mainframe.corp.com", loaded1.getHost());
assertEquals(23, loaded1.getPort());
assertEquals(4, loaded1.getModel());
assertEquals(62, loaded1.getDynamicRows());
assertEquals(160, loaded1.getDynamicCols());
assertEquals("LU01", loaded1.getLuName());
assertTrue(loaded1.isAutoConnect());
assertEquals("TSO", loaded1.getHostType());
assertFalse(loaded1.isUseTls());
assertTrue(loaded1.isTlsVerifyCert());
SavedHost loaded2 = loaded.get(1);
assertEquals("id-vm-2", loaded2.getId());
assertEquals("VM CMS Test", loaded2.getName());
assertEquals("vmcms.lab.net", loaded2.getHost());
assertEquals(992, loaded2.getPort());
assertEquals(2, loaded2.getModel());
assertEquals(24, loaded2.getDynamicRows());
assertEquals(80, loaded2.getDynamicCols());
assertEquals("LUCMS", loaded2.getLuName());
assertFalse(loaded2.isAutoConnect());
assertEquals("CMS", loaded2.getHostType());
assertTrue(loaded2.isUseTls());
assertFalse(loaded2.isTlsVerifyCert());
assertEquals("SOCKS5", loaded2.getProxyType());
assertEquals("10.0.0.1", loaded2.getProxyHost());
assertEquals(1080, loaded2.getProxyPort());
assertEquals("proxyuser", loaded2.getProxyUsername());
assertEquals("proxypass", loaded2.getProxyPassword());
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("HostStorage importFromIniFile merges and updates existing hosts by name or id")
public void testImportFromIniFileMergesAndUpdates() throws IOException {
SavedHost initial = new SavedHost("id-100", "Mainframe Prod", "old.host.org", 23, 4, 62, 160,
"", false, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
List<SavedHost> initList = new ArrayList<>();
initList.add(initial);
HostStorage.saveAll(initList);
File tmpIni = File.createTempFile("connections_merge", ".ini");
tmpIni.deleteOnExit();
try {
try (FileWriter w = new FileWriter(tmpIni)) {
// Update Mainframe Prod with new host address
w.write("[Mainframe Prod]\n");
w.write("id = id-100\n");
w.write("host = new.host.org\n");
w.write("port = 2323\n");
w.write("\n");
// Add a second connection
w.write("[Development CICS]\n");
w.write("host = cics.dev.org\n");
w.write("port = 992\n");
w.write("hostType = CICS\n");
w.write("useTls = true\n");
}
List<SavedHost> result = HostStorage.importFromIniFile(tmpIni);
assertEquals(2, result.size(), "Import should result in 2 saved hosts (1 updated, 1 added)");
SavedHost updated = HostStorage.findById("id-100");
assertNotNull(updated);
assertEquals("new.host.org", updated.getHost());
assertEquals(2323, updated.getPort());
SavedHost added = HostStorage.findByName("Development CICS");
assertNotNull(added);
assertEquals("cics.dev.org", added.getHost());
assertEquals(992, added.getPort());
assertEquals("CICS", added.getHostType());
assertTrue(added.isUseTls());
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("HostStorage parses INI section prefixes and aliases properly")
public void testImportHandlesMissingFieldsAndAliases() throws IOException {
File tmpIni = File.createTempFile("connections_alias", ".ini");
tmpIni.deleteOnExit();
try {
try (FileWriter w = new FileWriter(tmpIni)) {
w.write("[connection:Aliased Host]\n");
w.write("hostname = test.alias.net\n");
w.write("port = 1023\n");
w.write("terminalModel = 2\n");
w.write("type = CMS\n");
w.write("tls = true\n");
w.write("verifyCert = false\n");
w.write("charset = 1047\n");
w.write("rows = 24\n");
w.write("cols = 80\n");
w.write("lu = LUVM\n");
}
List<SavedHost> parsed = HostStorage.loadFromIniFile(tmpIni);
assertEquals(1, parsed.size());
SavedHost h = parsed.get(0);
assertEquals("Aliased Host", h.getName());
assertEquals("test.alias.net", h.getHost());
assertEquals(1023, h.getPort());
assertEquals(2, h.getModel());
assertEquals("CMS", h.getHostType());
assertTrue(h.isUseTls());
assertFalse(h.isTlsVerifyCert());
assertEquals("1047", h.getCodePage());
assertEquals(24, h.getDynamicRows());
assertEquals(80, h.getDynamicCols());
assertEquals("LUVM", h.getLuName());
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("SettingsDialog contains Import Connections and Export Connections buttons and is taller")
public void testSettingsDialogConnectionButtonsAndHeight() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
JButton btnImportConn = dialog.getBtnImportConnections();
JButton btnExportConn = dialog.getBtnExportConnections();
assertNotNull(btnImportConn, "Import Connections button must exist in SettingsDialog");
assertNotNull(btnExportConn, "Export Connections button must exist in SettingsDialog");
assertEquals("Import Connections...", btnImportConn.getText());
assertEquals("Export Connections...", btnExportConn.getText());
// Check window height: must be taller than previous 540 to accommodate extra buttons
assertTrue(dialog.getHeight() > 540,
"Settings window height (" + dialog.getHeight() + ") must be taller than 540 to accommodate the extra buttons");
// Verify config buttons share container row1, and connection buttons share container row2
JButton btnImport = dialog.getBtnImport();
JButton btnExport = dialog.getBtnExport();
assertNotNull(btnImport);
assertNotNull(btnExport);
Container configRow = btnImport.getParent();
assertNotNull(configRow);
assertSame(configRow, btnExport.getParent(), "btnImport and btnExport must share the same parent row");
Container connRow = btnImportConn.getParent();
assertNotNull(connRow);
assertSame(connRow, btnExportConn.getParent(), "btnImportConn and btnExportConn must share the same parent row");
assertNotSame(configRow, connRow, "Connection buttons must be below and NOT in line with config buttons");
// Verify connRow is positioned below configRow in buttonPanel
Container buttonPanel = configRow.getParent();
assertNotNull(buttonPanel);
assertSame(buttonPanel, connRow.getParent(), "Both rows must be in buttonPanel");
int configRowIdx = -1;
int connRowIdx = -1;
Component[] rows = buttonPanel.getComponents();
for (int i = 0; i < rows.length; i++) {
if (rows[i] == configRow) configRowIdx = i;
if (rows[i] == connRow) connRowIdx = i;
}
assertTrue(configRowIdx >= 0, "configRow must be in buttonPanel");
assertTrue(connRowIdx >= 0, "connRow must be in buttonPanel");
assertTrue(connRowIdx > configRowIdx, "connRow must be below configRow in buttonPanel (" + connRowIdx + " > " + configRowIdx + ")");
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
@Test
@DisplayName("SettingsDialog importConnectionsFile updates savedHostBox and Settings")
public void testSettingsDialogImportConnectionsFileUpdatesDropdownAndSettings() throws IOException {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
File tmpIni = File.createTempFile("settings_dialog_import_test", ".ini");
tmpIni.deleteOnExit();
try {
try (FileWriter w = new FileWriter(tmpIni)) {
w.write("[Auto Mainframe]\n");
w.write("host = auto.mainframe.org\n");
w.write("port = 992\n");
w.write("autoConnect = true\n");
w.write("useTls = true\n");
}
SettingsDialog dialog = new SettingsDialog(app);
dialog.importConnectionsFile(tmpIni);
// Check savedHostBox updated
JComboBox<SavedHost> box = dialog.getSavedHostBox();
assertNotNull(box);
assertTrue(box.getItemCount() >= 1);
SavedHost sel = (SavedHost) box.getSelectedItem();
assertNotNull(sel);
assertEquals("auto.mainframe.org", sel.getHost());
assertTrue(sel.isAutoConnect());
// Check Settings updated for autoConnect host
assertEquals("auto.mainframe.org", Settings.getAutoConnectHost());
assertEquals(992, Settings.getAutoConnectPort());
assertTrue(Settings.getAutoConnectTls());
// Test exportConnectionsFile
File exportTarget = File.createTempFile("settings_dialog_export_test", ".ini");
exportTarget.deleteOnExit();
dialog.exportConnectionsFile(exportTarget);
assertTrue(exportTarget.exists());
String exported = new String(Files.readAllBytes(exportTarget.toPath()));
assertTrue(exported.contains("auto.mainframe.org"));
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
} finally {
if (tmpIni.exists()) tmpIni.delete();
}
}
@Test
@DisplayName("SettingsDialog importConnectionsFile throws FileNotFoundException for nonexistent file")
public void testImportNonExistentConnectionsFileThrows() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
File nonExistent = new File("/path/to/nonexistent/connections_" + System.currentTimeMillis() + ".ini");
assertThrows(FileNotFoundException.class, () -> {
dialog.importConnectionsFile(nonExistent);
});
dialog.dispose();
app.dispose();
} catch (HeadlessException ignored) {
}
}
}
@@ -304,4 +304,40 @@ public class HostStorageAndProxyTest {
dialog.dispose(); 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,242 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import haus.nightmare.j3270.config.Settings;
import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost;
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 javax.swing.*;
import java.awt.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class AutoConnectSettingsAndTerminalCenteringTest {
private List<SavedHost> originalSavedHosts;
private Settings.StartupBehavior originalStartupBehavior;
private String originalAutoConnectHost;
private int originalAutoConnectPort;
@BeforeEach
public void setUp() {
originalSavedHosts = new ArrayList<>(HostStorage.getSavedHosts());
originalStartupBehavior = Settings.getStartupBehavior();
originalAutoConnectHost = Settings.getAutoConnectHost();
originalAutoConnectPort = Settings.getAutoConnectPort();
}
@AfterEach
public void tearDown() {
HostStorage.saveAll(originalSavedHosts);
Settings.setStartupBehavior(originalStartupBehavior);
Settings.setAutoConnectHost(originalAutoConnectHost);
Settings.setAutoConnectPort(originalAutoConnectPort);
}
@Test
@DisplayName("TerminalPanel centering: left bound is flush when constrained, centered with left <= right when too big")
public void testTerminalPanelCenteringLogic() {
TerminalPanel panel = new TerminalPanel();
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);
int cols = sb.getDisplayCols(); // 80
int rows = sb.getDisplayRows(); // 24
int gridW = cols * panel.cellWidth; // 800
int gridH = rows * panel.cellHeight; // 480
// 1. Panel is smaller than grid: left bound must be 0 (flush left cells), vertical must be 0
panel.setSize(gridW - 100, gridH - 50);
assertEquals(0, panel.getRenderOffsetX(), "Left bound should be 0 when window width is constrained");
assertEquals(0, panel.getRenderOffsetY(), "Top bound should be 0 when window height is constrained");
// 2. Panel exactly matches grid: offset must be 0
panel.setSize(gridW, gridH);
assertEquals(0, panel.getRenderOffsetX(), "Offset X must be 0 when width exactly matches grid");
assertEquals(0, panel.getRenderOffsetY(), "Offset Y must be 0 when height exactly matches grid");
// 3. Panel is larger with odd extra (extra = 7)
panel.setSize(gridW + 7, gridH + 11);
int ox = panel.getRenderOffsetX();
int leftSpace = ox;
int rightSpace = (gridW + 7) - (ox + gridW);
assertTrue(leftSpace <= rightSpace, "Left blank space (" + leftSpace + ") must be <= right space (" + rightSpace + ")");
assertEquals(3, leftSpace);
assertEquals(4, rightSpace);
int oy = panel.getRenderOffsetY();
int topSpace = oy;
int bottomSpace = (gridH + 11) - (oy + gridH);
assertTrue(topSpace <= bottomSpace, "Top space must be <= bottom space");
assertEquals(5, topSpace);
assertEquals(6, bottomSpace);
// 4. Panel is larger with even extra (extra = 6)
panel.setSize(gridW + 6, gridH + 8);
assertEquals(3, panel.getRenderOffsetX());
assertEquals(4, panel.getRenderOffsetY());
panel.dispose();
}
@Test
@DisplayName("guardedPack does not resize window when window is too small")
public void testGuardedPackDoesNotResizeWhenTooSmall() {
try {
JFrame frame = new JFrame();
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
panel.setClient(client);
frame.getContentPane().add(panel);
frame.setSize(400, 300);
frame.addNotify(); // makes frame displayable
// Set size explicitly smaller than preferred size (800+ x 480+)
frame.setSize(400, 300);
Dimension sizeBefore = frame.getSize();
// Run guardedPack
panel.guardedPack();
// Window should not have expanded
Dimension sizeAfter = frame.getSize();
assertEquals(sizeBefore.width, sizeAfter.width, "Window width should NOT be enlarged when too small");
assertEquals(sizeBefore.height, sizeAfter.height, "Window height should NOT be enlarged when too small");
frame.dispose();
panel.dispose();
} catch (HeadlessException ignored) {
// Environment without X display
}
}
@Test
@DisplayName("SettingsDialog Startup & Auto-Connect presents saved connections dropdown and updates auto-connect host")
public void testSettingsDialogSavedConnectionsDropdown() throws Exception {
SavedHost host1 = new SavedHost("id-test-1", "Host Alpha", "alpha.mainframe.test", 23, 2, 24, 80, "LU1", true, "TSO", false, true, true, "BOTH", "037", "NONE", "", 0, "", "");
SavedHost host2 = new SavedHost("id-test-2", "Host Beta", "beta.mainframe.test", 992, 4, 43, 80, "LU2", false, "VM", true, false, true, "BOTH", "037", "NONE", "", 0, "", "");
List<SavedHost> testHosts = new ArrayList<>();
testHosts.add(host1);
testHosts.add(host2);
HostStorage.saveAll(testHosts);
Settings.setStartupBehavior(Settings.StartupBehavior.AUTO_CONNECT);
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
return;
}
try {
SettingsDialog dialog = new SettingsDialog(app);
JComboBox<SavedHost> combo = dialog.getSavedHostBox();
assertNotNull(combo, "savedHostBox should not be null");
assertEquals(2, combo.getItemCount(), "Dropdown should contain all saved hosts");
// Host Alpha was autoConnect=true, so it should be initially selected
SavedHost initialSel = (SavedHost) combo.getSelectedItem();
assertNotNull(initialSel);
assertEquals("id-test-1", initialSel.getId());
// Switch to Host Beta
combo.setSelectedIndex(1);
// Trigger applySettings via reflection
java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings");
applyMethod.setAccessible(true);
applyMethod.invoke(dialog);
// Verify Host Beta is now the auto-connect host
SavedHost auto = HostStorage.getAutoConnectHost();
assertNotNull(auto, "Auto-connect host should exist");
assertEquals("id-test-2", auto.getId());
assertEquals("Host Beta", auto.getName());
assertEquals("beta.mainframe.test", auto.getHost());
assertEquals(992, auto.getPort());
SavedHost loadedAlpha = HostStorage.findById("id-test-1");
assertNotNull(loadedAlpha);
assertFalse(loadedAlpha.isAutoConnect(), "Host Alpha should no longer have autoConnect = true");
// Verify Settings autoConnectHost/Port synchronized
assertEquals("beta.mainframe.test", Settings.getAutoConnectHost());
assertEquals(992, Settings.getAutoConnectPort());
// Switch startup behavior to SHOW_CONNECT and apply
Field sbField = SettingsDialog.class.getDeclaredField("startupBehaviorBox");
sbField.setAccessible(true);
@SuppressWarnings("unchecked")
JComboBox<Settings.StartupBehavior> sbBox = (JComboBox<Settings.StartupBehavior>) sbField.get(dialog);
sbBox.setSelectedItem(Settings.StartupBehavior.SHOW_CONNECT);
applyMethod.invoke(dialog);
assertNull(HostStorage.getAutoConnectHost(), "When startup behavior is SHOW_CONNECT, auto-connect host should be cleared");
dialog.dispose();
app.dispose();
} finally {
HostStorage.saveAll(originalSavedHosts);
}
}
@Test
@DisplayName("Window size is adjusted once on connect for selected model and left untouched on onScreenSizeChanged")
public void testWindowSizedOnceForModelAndNotOnScreenSizeChanged() {
try {
JFrame frame = new JFrame();
TerminalPanel panel = new TerminalPanel();
panel.cellWidth = 10;
panel.cellHeight = 20;
frame.getContentPane().add(panel);
frame.addNotify();
// Pack for Model 4 (43 rows, 80 cols)
panel.packForModel(43, 80);
Dimension model4Size = frame.getSize();
assertTrue(model4Size.height >= 43 * 20, "Height should accommodate 43 rows");
assertTrue(model4Size.width >= 80 * 10, "Width should accommodate 80 cols");
// Pack for Model 5 (27 rows, 132 cols)
panel.packForModel(27, 132);
Dimension model5Size = frame.getSize();
assertTrue(model5Size.width >= 132 * 10, "Width should accommodate 132 cols");
// Set custom size and verify guardedPack leaves the window alone
frame.setSize(600, 400);
Dimension fixedSize = frame.getSize();
panel.guardedPack();
assertEquals(fixedSize.width, frame.getWidth());
assertEquals(fixedSize.height, frame.getHeight());
frame.dispose();
panel.dispose();
} catch (HeadlessException ignored) {}
}
}
@@ -0,0 +1,153 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.ConnectionState;
import haus.nightmare.lib3270j.Telnet3270Client;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.awt.*;
import java.awt.event.InputMethodEvent;
import java.text.AttributedString;
import static org.junit.jupiter.api.Assertions.*;
public class DBCSInputAndClipboardTest {
@Test
@DisplayName("Clipboard selection skips DB_RIGHT cells to avoid duplicating wide characters")
public void testDBCSClipboardCopyNoDoubling() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'A' at cell 2
ExtendedAttribute cell0 = sb.getCell(0);
cell0.db = ExtendedAttribute.DB_LEFT;
cell0.ucs4 = '\u3000';
ExtendedAttribute cell1 = sb.getCell(1);
cell1.db = ExtendedAttribute.DB_RIGHT;
cell1.ucs4 = '\u3000';
ExtendedAttribute cell2 = sb.getCell(2);
cell2.db = ExtendedAttribute.DB_NONE;
cell2.ucs4 = 'A';
panel.setClient(client);
// Stream selection covering cells 0..2 (row 0, col 0..2)
panel.select(0, 0, 0, 2);
String selected = panel.getSelectedText();
assertEquals("\u3000A", selected, "Stream selection must not duplicate the DBCS character");
}
@Test
@DisplayName("Block selection skips DB_RIGHT cells to avoid duplicating wide characters")
public void testDBCSBlockSelectionNoDoubling() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
panel.setBlockSelectMode(true);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
// Put a 2-cell DBCS character at cells 0 and 1, followed by SBCS 'B' at cell 2
ExtendedAttribute cell0 = sb.getCell(0);
cell0.db = ExtendedAttribute.DB_LEFT;
cell0.ucs4 = '\u3000';
ExtendedAttribute cell1 = sb.getCell(1);
cell1.db = ExtendedAttribute.DB_RIGHT;
cell1.ucs4 = '\u3000';
ExtendedAttribute cell2 = sb.getCell(2);
cell2.db = ExtendedAttribute.DB_NONE;
cell2.ucs4 = 'B';
panel.setClient(client);
panel.select(0, 0, 0, 2);
String selected = panel.getSelectedText();
assertEquals("\u3000B", selected, "Block selection must not duplicate the DBCS character");
}
@Test
@DisplayName("InputMethodRequests returns cursor bounds and processes committed text")
public void testDBCSInputMethodRequestsAndEvents() {
TerminalPanel panel = new TerminalPanel();
panel.setSize(800, 600);
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
panel.setClient(client);
assertNotNull(panel.getInputMethodRequests(), "getInputMethodRequests must return this");
Rectangle loc = panel.getTextLocation(null);
assertNotNull(loc, "Text location must not be null");
assertTrue(loc.width > 0, "Cursor width must be positive");
assertTrue(loc.height > 0, "Cursor height must be positive");
// Simulate committed IME text '\u3000'
AttributedString as = new AttributedString("\u3000");
InputMethodEvent imeEvent = new InputMethodEvent(panel, InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
as.getIterator(), 1, null, null);
panel.inputMethodTextChanged(imeEvent);
ScreenBuffer sb = client.getScreenBuffer();
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
assertEquals('\u3000', sb.getCell(0).ucs4);
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
assertEquals('\u3000', sb.getCell(1).ucs4);
assertEquals(2, sb.getCursorAddress(), "Cursor should advance by 2 after typing 2-cell DBCS");
}
@Test
@DisplayName("InputProcessor backspace over DBCS character deletes both cells")
public void testInputProcessorDBCSTypingAndBackspace() {
ConnectionConfig config = new ConnectionConfig("localhost", 23);
config.setCodePage("930");
Telnet3270Client client = new Telnet3270Client(config);
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
InputProcessor input = client.getInputProcessor();
ScreenBuffer sb = client.getScreenBuffer();
sb.erase(false);
sb.setCursorAddress(0);
// Type DBCS character
input.typeCharacter('\u3000');
assertEquals(2, sb.getCursorAddress());
assertEquals(ExtendedAttribute.DB_LEFT, sb.getCell(0).db);
assertEquals(ExtendedAttribute.DB_RIGHT, sb.getCell(1).db);
// Backspace over DBCS character
input.backspace();
assertEquals(0, sb.getCursorAddress(), "Cursor should back up 2 cells");
assertEquals(0, sb.getCell(0).ucs4, "Cell 0 should be deleted (null)");
assertEquals(0, sb.getCell(1).ucs4, "Cell 1 should be deleted (null)");
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(0).db);
assertEquals(ExtendedAttribute.DB_NONE, sb.getCell(1).db);
}
}
@@ -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 // 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,122 @@
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
}
}
@Test
@DisplayName("Mouse press and release on TerminalPanel requests focus")
public void testTerminalPanelMousePressRequestsFocus() {
try {
TerminalPanel panel = new TerminalPanel();
assertTrue(panel.isFocusable());
MouseListener[] mouseListeners = panel.getMouseListeners();
assertTrue(mouseListeners.length > 0);
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
0, 50, 50, 1, false, MouseEvent.BUTTON1);
mouseListeners[0].mousePressed(pressEvent);
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
0, 50, 50, 1, false, MouseEvent.BUTTON1);
mouseListeners[0].mouseReleased(releaseEvent);
} catch (HeadlessException ignored) {
}
}
}
@@ -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,148 @@
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 boolean IsDBCS() {
return delegate != null && delegate.isDBCS();
}
public boolean isDBCS() { return IsDBCS(); }
public boolean IsDBCSOnly() {
return delegate != null && delegate.isDBCSOnly();
}
public boolean isDBCSOnly() { return IsDBCSOnly(); }
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,571 @@
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);
}
// ========== DBCS Character Status ==========
public boolean IsDBCSChar(int pos) {
return delegate != null && delegate.IsDBCSChar(pos);
}
public boolean isDBCSChar(int pos) { return IsDBCSChar(pos); }
public boolean IsDBCSChar(int row, int col) {
return delegate != null && delegate.IsDBCSChar(row, col);
}
public boolean isDBCSChar(int row, int col) { return IsDBCSChar(row, col); }
public boolean IsDBCS1stChar(int pos) {
return delegate != null && delegate.IsDBCS1stChar(pos);
}
public boolean isDBCS1stChar(int pos) { return IsDBCS1stChar(pos); }
public boolean IsDBCS1stChar(int row, int col) {
return delegate != null && delegate.IsDBCS1stChar(row, col);
}
public boolean isDBCS1stChar(int row, int col) { return IsDBCS1stChar(row, col); }
public boolean IsDBCS2ndChar(int pos) {
return delegate != null && delegate.IsDBCS2ndChar(pos);
}
public boolean isDBCS2ndChar(int pos) { return IsDBCS2ndChar(pos); }
public boolean IsDBCS2ndChar(int row, int col) {
return delegate != null && delegate.IsDBCS2ndChar(row, col);
}
public boolean isDBCS2ndChar(int row, int col) { return IsDBCS2ndChar(row, col); }
public boolean IsSOChar(int pos) {
return delegate != null && delegate.IsSOChar(pos);
}
public boolean isSOChar(int pos) { return IsSOChar(pos); }
public boolean IsSIChar(int pos) {
return delegate != null && delegate.IsSIChar(pos);
}
public boolean isSIChar(int pos) { return IsSIChar(pos); }
public boolean IsSOSIChar(int pos) {
return delegate != null && delegate.IsSOSIChar(pos);
}
public boolean isSOSIChar(int pos) { return IsSOSIChar(pos); }
}
@@ -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();
}
}
@@ -69,6 +69,12 @@ public class ConnectionConfig {
private int reconnectMaxRetries = 5; private int reconnectMaxRetries = 5;
private int tcpUserTimeoutMs = 0; private int tcpUserTimeoutMs = 0;
// Host operating system type (TSO, CMS, CICS)
private String hostType = "TSO";
private boolean savedConnection = false;
private String savedProfileId = null;
private String savedProfileName = null;
public ConnectionConfig() {} public ConnectionConfig() {}
public ConnectionConfig(String host, int port) { public ConnectionConfig(String host, int port) {
@@ -312,6 +318,51 @@ public class ConnectionConfig {
public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; } public int getTcpUserTimeoutMs() { return tcpUserTimeoutMs; }
public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); } public void setTcpUserTimeoutMs(int timeoutMs) { this.tcpUserTimeoutMs = Math.max(0, timeoutMs); }
public String getHostType() {
return hostType != null ? hostType : "TSO";
}
public void setHostType(String hostType) {
this.hostType = (hostType != null && !hostType.trim().isEmpty()) ? hostType.trim().toUpperCase() : "TSO";
}
public haus.nightmare.lib3270j.ft.FTConfig.HostType getFtHostType() {
if (hostType != null) {
try {
return haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(hostType.trim().toUpperCase());
} catch (Exception ignored) {}
}
return haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
}
public void setFtHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType ftHostType) {
this.hostType = (ftHostType != null) ? ftHostType.name() : "TSO";
}
public boolean isSavedConnection() {
return savedConnection;
}
public void setSavedConnection(boolean savedConnection) {
this.savedConnection = savedConnection;
}
public String getSavedProfileId() {
return savedProfileId;
}
public void setSavedProfileId(String savedProfileId) {
this.savedProfileId = savedProfileId;
}
public String getSavedProfileName() {
return savedProfileName;
}
public void setSavedProfileName(String savedProfileName) {
this.savedProfileName = savedProfileName;
}
/** /**
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"), * Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"), * plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), proxy flags (e.g. "--proxy=http://proxy:8080 host:23"),
@@ -11,6 +11,7 @@ import haus.nightmare.lib3270j.telnet.TelnetFSM;
import java.io.IOException; import java.io.IOException;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.logging.Level;
/** /**
* Main API entry point for lib3270j. * Main API entry point for lib3270j.
@@ -46,6 +47,7 @@ public class Telnet3270Client {
private final java.util.concurrent.atomic.AtomicBoolean reconnecting = new java.util.concurrent.atomic.AtomicBoolean(false); private final java.util.concurrent.atomic.AtomicBoolean reconnecting = new java.util.concurrent.atomic.AtomicBoolean(false);
private volatile Thread reconnectThread; private volatile Thread reconnectThread;
private volatile boolean intentionallyDisconnected = false;
public Telnet3270Client(ConnectionConfig config) { public Telnet3270Client(ConnectionConfig config) {
this.config = config; this.config = config;
@@ -92,6 +94,9 @@ public class Telnet3270Client {
ps.notifyAlarm(); ps.notifyAlarm();
} }
@Override public void onKeyboardUnlocked() { @Override public void onKeyboardUnlocked() {
if (screenBuffer != null) {
screenBuffer.getHistoryBuffer().snapshot(screenBuffer);
}
ps.notifyKeyUnlocked(); ps.notifyKeyUnlocked();
} }
}); });
@@ -127,6 +132,7 @@ public class Telnet3270Client {
* then returns while telnet/TN3270E negotiation continues asynchronously. * then returns while telnet/TN3270E negotiation continues asynchronously.
*/ */
public void connect() throws IOException { public void connect() throws IOException {
intentionallyDisconnected = false;
log.info("Connecting to " + config.getHost() + ":" + config.getPort() + log.info("Connecting to " + config.getHost() + ":" + config.getPort() +
" model=" + config.getModel()); " model=" + config.getModel());
@@ -190,8 +196,16 @@ public class Telnet3270Client {
* Disconnect from the host. * Disconnect from the host.
*/ */
public void disconnect() { public void disconnect() {
intentionallyDisconnected = true;
cancelAutoReconnect(); cancelAutoReconnect();
if (connection != null) { if (connection != null) {
try {
if (fsm != null) {
fsm.sendTelnetDisconnect();
}
} catch (Exception e) {
log.log(Level.FINE, "Error during Telnet disconnect negotiation", e);
}
connection.disconnect(); connection.disconnect();
connection = null; connection = null;
} }
@@ -200,14 +214,20 @@ public class Telnet3270Client {
public void cancelAutoReconnect() { public void cancelAutoReconnect() {
reconnecting.set(false); reconnecting.set(false);
if (reconnectThread != null) { Thread t = reconnectThread;
reconnectThread.interrupt(); if (t != null) {
t.interrupt();
try {
t.join(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
reconnectThread = null; reconnectThread = null;
} }
} }
public void initiateAutoReconnect() { public void initiateAutoReconnect() {
if (config == null || !config.isAutoReconnect()) { if (intentionallyDisconnected || config == null || !config.isAutoReconnect()) {
return; return;
} }
if (!reconnecting.compareAndSet(false, true)) { if (!reconnecting.compareAndSet(false, true)) {
@@ -658,6 +678,9 @@ public class Telnet3270Client {
public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); } public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); }
public void toggleWordWrap() { setWordWrap(!isWordWrap()); } 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. */ /** APL Keyboard Mode operations. */
public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); } public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); }
public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); } public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); }
@@ -705,4 +728,8 @@ public class Telnet3270Client {
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() { public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
return dsProcessor.getGocaDecoder(); return dsProcessor.getGocaDecoder();
} }
public haus.nightmare.lib3270j.screen.ScreenHistoryBuffer getScreenHistoryBuffer() {
return screenBuffer != null ? screenBuffer.getHistoryBuffer() : null;
}
} }
@@ -27,11 +27,15 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
int[] sbcsMapping, String nioCharsetName) { int[] sbcsMapping, String nioCharsetName) {
super(id, description, cpgid, cgcsgid, sbcsMapping); super(id, description, cpgid, cgcsgid, sbcsMapping);
if (nioCharsetName != null) { if (nioCharsetName != null) {
try { for (String candidate : nioCharsetName.split(",")) {
if (Charset.isSupported(nioCharsetName)) { candidate = candidate.trim();
this.nioCharset = Charset.forName(nioCharsetName); try {
} if (Charset.isSupported(candidate)) {
} catch (Exception ignored) {} this.nioCharset = Charset.forName(candidate);
break;
}
} catch (Exception ignored) {}
}
} }
// Initialize ideographic space (0x4040 <-> \u3000) // Initialize ideographic space (0x4040 <-> \u3000)
registerDbcsPair(0x4040, '\u3000'); registerDbcsPair(0x4040, '\u3000');
@@ -250,4 +254,78 @@ public abstract class AbstractDBCSCodePage extends AbstractCodePage {
public byte[] utf8WithSOSIToEbcdic(String s) { public byte[] utf8WithSOSIToEbcdic(String s) {
return stringToEbcdic(s, true); return stringToEbcdic(s, true);
} }
/**
* Double-byte character set Graphic Character Set Global ID (DBGID/CGCSGID) matching HoD DBGIDTable.
*/
public int getDbCgcsgid() {
switch (getCpgid()) {
case 930:
case 939:
case 290:
return 370; // 0x0172
case 933:
return 934; // 0x03A6
case 935:
return 937; // 0x03A9
case 937:
return 935; // 0x03A7
case 1390:
case 1399:
case 1364:
case 1371:
case 1388:
return 65535; // 0xFFFF
default:
return 370;
}
}
/**
* Double-byte character set Code Page Global ID (DBCS CPGID) matching HoD DBGIDTable.
*/
public int getDbCpgid() {
switch (getCpgid()) {
case 930:
case 939:
case 290:
case 1390:
case 1399:
return 300; // 0x012C
case 933:
case 1364:
return 834; // 0x0342
case 935:
case 1388:
return 837; // 0x0345
case 937:
case 1371:
return 835; // 0x0343
default:
return 300;
}
}
/**
* Double-byte character set section start byte matching HoD DBCSSectTable.
*/
public byte getDbcsSectionStart() {
return 0x41;
}
/**
* Double-byte character set section end byte matching HoD DBCSSectTable.
*/
public byte getDbcsSectionEnd() {
switch (getCpgid()) {
case 290:
case 930:
case 935:
case 939:
return 0x7F;
default:
return (byte) 0xFE;
}
}
} }
@@ -311,6 +311,12 @@ public class CodePageRegistry {
register(new Cp1388()); register(new Cp1388());
register(new Cp1371()); register(new Cp1371());
// Japanese Katakana & Latin SBCS / DBCS Euro
register(new Cp290());
register(new Cp1027());
register(new Cp1390());
register(new Cp1399());
// Setup Extended Aliases // Setup Extended Aliases
addAlias("ar", "420"); addAlias("ar", "420");
addAlias("arabic", "420"); addAlias("arabic", "420");
@@ -360,10 +366,12 @@ public class CodePageRegistry {
addAlias("zh-tw-ext", "1371"); addAlias("zh-tw-ext", "1371");
// HoD Converter aliases mapping all HoD converter names to CodePage / Charset // HoD Converter aliases mapping all HoD converter names to CodePage / Charset
addAlias("1390", "930"); addAlias("1390", "1390");
addAlias("1390jis2004", "930"); addAlias("1390jis2004", "1390");
addAlias("1399", "939"); addAlias("1399", "1399");
addAlias("1399jis2004", "939"); addAlias("1399jis2004", "1399");
addAlias("290", "290");
addAlias("1027", "1027");
addAlias("937macau", "937"); addAlias("937macau", "937");
addAlias("1364", "933"); addAlias("1364", "933");
addAlias("1379", "937"); addAlias("1379", "937");
@@ -0,0 +1,43 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1027 (Japanese Latin Extended SBCS).
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp939.singleByteToChar.
* CCSID / CPGID: 1027, GCSGID: 1172.
*/
public class Cp1027 extends AbstractCodePage {
public static final String ID = "1027";
public static final String DESCRIPTION = "Japanese Latin Extended - EBCDIC";
public static final int CPGID = 1027;
public static final int GCSGID = 1172;
private static final String SBCS_STR =
"\u0000\u0001\u0002\u0003\u009c\t\u0086\u007f\u0097\u008d\u008e\u000b\f\r\u000e\u000f" +
"\u0010\u0011\u0012\u0013\u009d\u0085\b\u0087\u0018\u0019\u0092\u008f\u001c\u001d\u001e\u001f" +
"\u0080\u0081\u0082\u0083\u0084\n\u0017\u001b\u0088\u0089\u008a\u008b\u008c\u0005\u0006\u0007" +
"\u0090\u0091\u0016\u0093\u0094\u0095\u0096\u0004\u0098\u0099\u009a\u009b\u0014\u0015\u009e\u001a" +
" \ufffd\uff61\uff62\uff63\uff64\uff65\uff66\uff67\uff68\u00a2.<(+|" +
"&\uff69\uff6a\uff6b\uff6c\uff6d\uff6e\uff6f\uff70\uff71!$*);\u00ac" +
"-/\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79\ufffd,%_>?" +
"\uff7a\uff7b\uff7c\uff7d\uff7e\uff7f\uff80\uff81\uff82`:#@'=\"" +
"\ufffdabcdefghi\uff83\uff84\uff85\uff86\uff87\uff88" +
"\ufffdjklmnopqr\uff89\uff8a\uff8b\uff8c\uff8d\uff8e" +
"\u203e~stuvwxyz\uff8f\uff90\uff91[\uff92\uff93^" +
"\u00a3\u00a5\uff94\uff95\uff96\uff97\uff98\uff99\uff9a\uff9b\uff9c\uff9d]\uff9e\uff9f" +
"{ABCDEFGHI\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"}JKLMNOPQR\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"\\\ufffdSTUVWXYZ\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"0123456789\ufffd\ufffd\ufffd\ufffd\ufffd\u009f";
public static final int[] MAPPING = new int[256];
static {
for (int i = 0; i < 256; i++) {
MAPPING[i] = (i < SBCS_STR.length()) ? SBCS_STR.charAt(i) : i;
}
}
public Cp1027() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING);
}
}
@@ -0,0 +1,23 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1390 (Japanese Katakana Extended Mixed DBCS with Euro support: host SBCS CP290+Euro / DBCS CP16684).
* CCSID / CPGID: 1390, GCSGID: 1172.
*/
public class Cp1390 extends AbstractDBCSCodePage {
public static final String ID = "1390";
public static final String DESCRIPTION = "Japanese Katakana Extended Mixed DBCS (with Euro)";
public static final int CPGID = 1390;
public static final int GCSGID = 1172;
public static final int[] MAPPING = Cp290.MAPPING.clone();
static {
// Euro symbol at 0xE1
MAPPING[0xE1] = 0x20AC;
}
public Cp1390() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING, "x-IBM1390,IBM1390,Cp1390,x-IBM930,IBM930,Cp930");
}
}
@@ -0,0 +1,23 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 1399 (Japanese Latin Extended Mixed DBCS with Euro support: host SBCS CP1027+Euro / DBCS CP16684).
* CCSID / CPGID: 1399, GCSGID: 1172.
*/
public class Cp1399 extends AbstractDBCSCodePage {
public static final String ID = "1399";
public static final String DESCRIPTION = "Japanese Latin Extended Mixed DBCS (with Euro)";
public static final int CPGID = 1399;
public static final int GCSGID = 1172;
public static final int[] MAPPING = Cp1027.MAPPING.clone();
static {
// Euro symbol at 0xE1
MAPPING[0xE1] = 0x20AC;
}
public Cp1399() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING, "x-IBM1399,IBM1399,Cp1399,x-IBM939,IBM939,Cp939");
}
}
@@ -0,0 +1,43 @@
package haus.nightmare.lib3270j.charset;
/**
* IBM Code Page 290 (Japanese Katakana Extended SBCS).
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.HOD.converters.ja.ByteToCharCp290.
* CCSID / CPGID: 290, GCSGID: 1172.
*/
public class Cp290 extends AbstractCodePage {
public static final String ID = "290";
public static final String DESCRIPTION = "Japanese Katakana Extended - EBCDIC";
public static final int CPGID = 290;
public static final int GCSGID = 1172;
private static final String SBCS_STR =
"\u0000\u0001\u0002\u0003\u009c\t\u0086\u007f\u0097\u008d\u008e\u000b\f\r\u000e\u000f" +
"\u0010\u0011\u0012\u0013\u009d\u0085\b\u0087\u0018\u0019\u0092\u008f\u001c\u001d\u001e\u001f" +
"\u0080\u0081\u0082\u0083\u0084\n\u0017\u001b\u0088\u0089\u008a\u008b\u008c\u0005\u0006\u0007" +
"\u0090\u0091\u0016\u0093\u0094\u0095\u0096\u0004\u0098\u0099\u009a\u009b\u0014\u0015\u009e\u001a" +
" \uff61\uff62\uff63\uff64\uff65\uff66\uff67\uff68\uff69\u00a3.<(+|" +
"&\uff6a\uff6b\uff6c\uff6d\uff6e\uff6f\ufffd\uff70\ufffd!\u00a5*);\u00ac" +
"-/\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ufffd,%_>?" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000:#@'=\"" +
"\u0000\uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78\uff79\uff7a\u0000\uff7b\uff7c\uff7d\uff7e" +
"\uff7f\uff80\uff81\uff82\uff83\uff84\uff85\uff86\uff87\uff88\uff89\u0000\ufffd\uff8a\uff8b\uff8c" +
"\u0000\u203e\uff8d\uff8e\uff8f\uff90\uff91\uff92\uff93\uff94\uff95\u0000\uff96\uff97\uff98\uff99" +
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\uff9a\uff9b\uff9c\uff9d\uff9e\uff9f" +
"\u0000ABCDEFGHI\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"\u0000JKLMNOPQR\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"$\ufffdSTUVWXYZ\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" +
"0123456789\ufffd\ufffd\ufffd\ufffd\ufffd\u009f";
public static final int[] MAPPING = new int[256];
static {
for (int i = 0; i < 256; i++) {
MAPPING[i] = (i < SBCS_STR.length()) ? SBCS_STR.charAt(i) : i;
}
}
public Cp290() {
super(ID, DESCRIPTION, CPGID, GCSGID, MAPPING);
}
}
@@ -12,6 +12,6 @@ public class Cp930 extends AbstractDBCSCodePage {
public static final int GCSGID = 1172; public static final int GCSGID = 1172;
public Cp930() { public Cp930() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM930"); super(ID, DESCRIPTION, CPGID, GCSGID, Cp290.MAPPING, "x-IBM930,IBM930,Cp930");
} }
} }
@@ -12,6 +12,6 @@ public class Cp939 extends AbstractDBCSCodePage {
public static final int GCSGID = 1172; public static final int GCSGID = 1172;
public Cp939() { public Cp939() {
super(ID, DESCRIPTION, CPGID, GCSGID, Cp037.MAPPING, "x-IBM939"); super(ID, DESCRIPTION, CPGID, GCSGID, Cp1027.MAPPING, "x-IBM939,IBM939,Cp939");
} }
} }
@@ -90,6 +90,35 @@ public class EbcdicTranslator {
return isDBCSCodePage(); return isDBCSCodePage();
} }
public synchronized int getDbCgcsgid() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbCgcsgid();
}
return 370;
}
public synchronized int getDbCpgid() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbCpgid();
}
return 300;
}
public synchronized byte getDbcsSectionStart() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbcsSectionStart();
}
return 0x41;
}
public synchronized byte getDbcsSectionEnd() {
if (activeCodePage instanceof AbstractDBCSCodePage) {
return ((AbstractDBCSCodePage) activeCodePage).getDbcsSectionEnd();
}
return 0x7F;
}
// ========================================================================= // =========================================================================
// Custom Character Translation Overrides // Custom Character Translation Overrides
// ========================================================================= // =========================================================================
@@ -53,12 +53,16 @@ public class DataStreamProcessor {
private boolean unlockPending = false; private boolean unlockPending = false;
private boolean unlockSysPending = false; private boolean unlockSysPending = false;
private boolean rcvdRead = false; private boolean rcvdRead = false;
private boolean qreplyPending = false;
// Modal SA (set attribute) character attributes // Modal SA (set attribute) character attributes
private byte currentFg = 0; private byte currentFg = 0;
private byte currentBg = 0; private byte currentBg = 0;
private byte currentGr = 0; private byte currentGr = 0;
private byte currentCs = 0; private byte currentCs = 0;
private byte currentOl = 0;
private byte currentTr = 0;
private boolean inDBCS = false;
/** Functional interface for sending output back through the telnet stack. */ /** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface @FunctionalInterface
@@ -180,11 +184,13 @@ public class DataStreamProcessor {
switch (cmd) { switch (cmd) {
case CMD_W: case CMD_W:
case SNA_CMD_W: case SNA_CMD_W:
qreplyPending = false;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
processWrite(data, offset, length, false); processWrite(data, offset, length, false);
break; break;
case CMD_EW: case CMD_EW:
case SNA_CMD_EW: case SNA_CMD_EW:
qreplyPending = false;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
if (log.isLoggable(Level.FINE)) { if (log.isLoggable(Level.FINE)) {
log.fine(">>> ERASE/WRITE: clearing screen (default size)"); log.fine(">>> ERASE/WRITE: clearing screen (default size)");
@@ -205,6 +211,7 @@ public class DataStreamProcessor {
break; break;
case CMD_EWA: case CMD_EWA:
case SNA_CMD_EWA: case SNA_CMD_EWA:
qreplyPending = false;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
if (log.isLoggable(Level.FINE)) { if (log.isLoggable(Level.FINE)) {
log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)");
@@ -233,7 +240,12 @@ public class DataStreamProcessor {
case SNA_CMD_RM: case SNA_CMD_RM:
rcvdRead = true; rcvdRead = true;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
processReadModified(false); if (qreplyPending) {
qreplyPending = false;
sendAllQueryReplies();
} else {
processReadModified(false);
}
break; break;
case CMD_RMA: case CMD_RMA:
case SNA_CMD_RMA: case SNA_CMD_RMA:
@@ -243,13 +255,19 @@ public class DataStreamProcessor {
break; break;
case CMD_EAU: case CMD_EAU:
case SNA_CMD_EAU: case SNA_CMD_EAU:
qreplyPending = false;
programSymbolManager.commitStagedSymbols(); programSymbolManager.commitStagedSymbols();
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
}
log.info(">>> EAU: erasing all unprotected fields"); log.info(">>> EAU: erasing all unprotected fields");
screen.eraseAllUnprotected(); screen.eraseAllUnprotected();
currentFg = 0; currentFg = 0;
currentBg = 0; currentBg = 0;
currentGr = 0; currentGr = 0;
currentCs = 0; currentCs = 0;
currentOl = 0;
currentTr = 0;
break; break;
case CMD_WSF: case CMD_WSF:
case SNA_CMD_WSF: case SNA_CMD_WSF:
@@ -275,6 +293,7 @@ public class DataStreamProcessor {
cmd == CMD_EWA || cmd == SNA_CMD_EWA); cmd == CMD_EWA || cmd == SNA_CMD_EWA);
if (!isWriteCmd && keyboardRestore && inputProcessor != null && !contentionResolution) { if (!isWriteCmd && keyboardRestore && inputProcessor != null && !contentionResolution) {
inputProcessor.setKeyboardLocked(false); inputProcessor.setKeyboardLocked(false);
inputProcessor.setLastAid(AID_NO);
} }
// Debug: dump non-empty screen lines // Debug: dump non-empty screen lines
@@ -374,10 +393,14 @@ public class DataStreamProcessor {
if (kbdRestore) { if (kbdRestore) {
unlockPending = true; unlockPending = true;
unlockSysPending = true; unlockSysPending = true;
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
}
} }
if (!contentionResolution && kbdRestore && inputProcessor != null) { if (!contentionResolution && kbdRestore && inputProcessor != null) {
inputProcessor.setKeyboardLocked(false); inputProcessor.setKeyboardLocked(false);
inputProcessor.setLastAid(AID_NO);
} }
if (resetMdt) { if (resetMdt) {
@@ -392,6 +415,9 @@ public class DataStreamProcessor {
currentBg = 0; currentBg = 0;
currentGr = 0; currentGr = 0;
currentCs = 0; currentCs = 0;
currentOl = 0;
currentTr = 0;
inDBCS = false;
} }
if (eraseFirst) { if (eraseFirst) {
@@ -401,6 +427,9 @@ public class DataStreamProcessor {
currentBg = 0; currentBg = 0;
currentGr = 0; currentGr = 0;
currentCs = 0; currentCs = 0;
currentOl = 0;
currentTr = 0;
inDBCS = false;
} }
// Process orders and data starting at byte 2 // Process orders and data starting at byte 2
@@ -431,6 +460,7 @@ public class DataStreamProcessor {
} }
case ORDER_SF: { // Start Field case ORDER_SF: { // Start Field
inDBCS = false;
if (pos + 1 >= end) { if (pos + 1 >= end) {
pos = end; pos = end;
break; break;
@@ -452,6 +482,7 @@ public class DataStreamProcessor {
} }
case ORDER_SFE: { // Start Field Extended case ORDER_SFE: { // Start Field Extended
inDBCS = false;
if (pos + 1 >= end) { if (pos + 1 >= end) {
pos = end; pos = end;
break; break;
@@ -497,6 +528,8 @@ public class DataStreamProcessor {
currentBg = 0; currentBg = 0;
currentGr = 0; currentGr = 0;
currentCs = 0; currentCs = 0;
currentOl = 0;
currentTr = 0;
break; break;
case XA_FOREGROUND: case XA_FOREGROUND:
currentFg = (byte) attrValue; currentFg = (byte) attrValue;
@@ -521,6 +554,13 @@ public class DataStreamProcessor {
case XA_CHARSET: case XA_CHARSET:
currentCs = (byte) attrValue; currentCs = (byte) attrValue;
break; break;
case XA_OUTLINING:
case 0x84:
currentOl = (byte) attrValue;
break;
case XA_TRANSPARENCY:
currentTr = (byte) attrValue;
break;
} }
pos += 3; pos += 3;
lastWasOrder = true; lastWasOrder = true;
@@ -582,6 +622,44 @@ public class DataStreamProcessor {
if (toAddr >= size) if (toAddr >= size)
toAddr = toAddr % size; toAddr = toAddr % size;
boolean isDbcsSession = (translator != null && translator.isDBCS());
if (isDbcsSession && (inDBCS || currentCs == ExtendedAttribute.CS_DBCS) && pos + 4 < end) {
int fillChar2 = data[pos + 4] & 0xFF;
char uni = (translator.getCodePage() != null) ? translator.getCodePage().dbcsToUnicode(fillChar, fillChar2) : '?';
do {
ExtendedAttribute ea1 = screen.getCell(baddr);
ea1.fa = 0;
ea1.ec = (byte) fillChar;
ea1.ucs4 = uni;
ea1.fg = currentFg;
ea1.bg = currentBg;
ea1.gr = (byte) currentGr;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ea1.ol = currentOl;
ea1.tr = currentTr;
baddr = (baddr + 1) % size;
if (baddr == toAddr) break;
ExtendedAttribute ea2 = screen.getCell(baddr);
ea2.fa = 0;
ea2.ec = (byte) fillChar2;
ea2.ucs4 = uni;
ea2.fg = currentFg;
ea2.bg = currentBg;
ea2.gr = (byte) currentGr;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
ea2.ol = currentOl;
ea2.tr = currentTr;
baddr = (baddr + 1) % size;
} while (baddr != toAddr);
screen.setBufferAddress(baddr);
pos += 5;
lastWasOrder = true;
break;
}
// Handle GE (graphic escape) prefix // Handle GE (graphic escape) prefix
byte fillCs = currentCs; byte fillCs = currentCs;
char fillUcs4 = 0; char fillUcs4 = 0;
@@ -605,6 +683,9 @@ public class DataStreamProcessor {
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
ea.cs = fillCs; ea.cs = fillCs;
ea.db = ExtendedAttribute.DB_NONE;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
} while (baddr != toAddr); } while (baddr != toAddr);
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
@@ -633,6 +714,8 @@ public class DataStreamProcessor {
ea.bg = 0; ea.bg = 0;
ea.gr = 0; ea.gr = 0;
ea.cs = 0; ea.cs = 0;
ea.ol = 0;
ea.tr = 0;
} }
} }
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
@@ -657,6 +740,8 @@ public class DataStreamProcessor {
ea.fg = currentFg; ea.fg = currentFg;
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
pos += 2; pos += 2;
@@ -665,6 +750,98 @@ public class DataStreamProcessor {
} }
default: { default: {
boolean isDbcsSession = (translator != null && translator.isDBCS());
if (isDbcsSession && b == 0x0E) { // Shift-Out (enter DBCS mode)
inDBCS = true;
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0;
ea.ec = (byte) 0x0E;
ea.ucs4 = ' ';
ea.cs = ExtendedAttribute.CS_BASE;
ea.db = ExtendedAttribute.DB_SO;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = false;
break;
} else if (isDbcsSession && b == 0x0F) { // Shift-In (exit DBCS mode)
inDBCS = false;
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0;
ea.ec = (byte) 0x0F;
ea.ucs4 = ' ';
ea.cs = ExtendedAttribute.CS_BASE;
ea.db = ExtendedAttribute.DB_SI;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = false;
break;
} else if (isDbcsSession && (inDBCS || currentCs == ExtendedAttribute.CS_DBCS)) {
if (pos + 1 < end) {
int b2 = data[pos + 1] & 0xFF;
if (b2 == 0x0F) { // Orphaned single byte before SI
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0;
ea.ec = (byte) b;
ea.ucs4 = translator.ebcdicToUnicode(b);
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.cs = currentCs;
ea.ol = currentOl;
ea.tr = currentTr;
ea.db = ExtendedAttribute.DB_NONE;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = false;
break;
}
char uni = (translator.getCodePage() != null) ? translator.getCodePage().dbcsToUnicode(b, b2) : '?';
ExtendedAttribute ea1 = screen.getCell(baddr);
ea1.fa = 0;
ea1.ec = (byte) b;
ea1.ucs4 = uni;
ea1.fg = currentFg;
ea1.bg = currentBg;
ea1.gr = (byte) currentGr;
ea1.cs = ExtendedAttribute.CS_DBCS;
ea1.db = ExtendedAttribute.DB_LEFT;
ea1.ol = currentOl;
ea1.tr = currentTr;
int nextAddr = (baddr + 1) % size;
ExtendedAttribute ea2 = screen.getCell(nextAddr);
ea2.fa = 0;
ea2.ec = (byte) b2;
ea2.ucs4 = uni;
ea2.fg = currentFg;
ea2.bg = currentBg;
ea2.gr = (byte) currentGr;
ea2.cs = ExtendedAttribute.CS_DBCS;
ea2.db = ExtendedAttribute.DB_RIGHT;
ea2.ol = currentOl;
ea2.tr = currentTr;
baddr = (baddr + 2) % size;
screen.setBufferAddress(baddr);
pos += 2;
lastWasOrder = false;
break;
}
}
// Regular data byte (EBCDIC character) // Regular data byte (EBCDIC character)
// Format control codes display as nulls/blanks per 3270 spec // Format control codes display as nulls/blanks per 3270 spec
boolean isFCOrder = (b == FCORDER_NULL || b == FCORDER_FF || boolean isFCOrder = (b == FCORDER_NULL || b == FCORDER_FF ||
@@ -676,13 +853,18 @@ public class DataStreamProcessor {
if (isFCOrder) { if (isFCOrder) {
ea.ec = 0; // Display as null/blank ea.ec = 0; // Display as null/blank
ea.ucs4 = 0;
} else { } else {
ea.ec = (byte) b; ea.ec = (byte) b;
ea.ucs4 = (translator != null) ? translator.ebcdicToUnicode(b) : (char) b;
} }
ea.fg = currentFg; ea.fg = currentFg;
ea.bg = currentBg; ea.bg = currentBg;
ea.gr = (byte) currentGr; ea.gr = (byte) currentGr;
ea.cs = currentCs; ea.cs = currentCs;
ea.db = ExtendedAttribute.DB_NONE;
ea.ol = currentOl;
ea.tr = currentTr;
baddr = (baddr + 1) % size; baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr); screen.setBufferAddress(baddr);
@@ -760,13 +942,26 @@ public class DataStreamProcessor {
// ========== Read Buffer ========== // ========== Read Buffer ==========
public void processReadBuffer() { public void processReadBuffer() {
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
processReadBuffer(aid);
}
public void processReadBuffer(int aid) {
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
byte[] data = inputProcessor.buildReadBufferInboundData(aid);
if (outputSender != null) {
outputSender.send3270Data(data);
}
return;
}
outputPos = 0; outputPos = 0;
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
if (inputProcessor != null) { if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO); inputProcessor.setLastAid(AID_NO);
} }
@@ -783,7 +978,7 @@ public class DataStreamProcessor {
// Buffer contents depending on Reply Mode // Buffer contents depending on Reply Mode
if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) { if (mode == SF_SRM_XFIELD || mode == SF_SRM_CHAR) {
// Extended Field or Character Mode // Extended Field or Character Mode
byte curFg = 0, curBg = 0, curGr = 0, curCs = 0; byte curFg = 0, curBg = 0, curGr = 0, curCs = 0, curTr = 0;
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i); ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) { if (ea.isFieldAttribute()) {
@@ -794,6 +989,7 @@ public class DataStreamProcessor {
if (ea.gr != 0) count++; if (ea.gr != 0) count++;
if (ea.cs != 0) count++; if (ea.cs != 0) count++;
if (ea.ol != 0) count++; if (ea.ol != 0) count++;
if (ea.tr != 0) count++;
outputWrite(ORDER_SFE); outputWrite(ORDER_SFE);
outputWrite(count); outputWrite(count);
@@ -812,6 +1008,7 @@ public class DataStreamProcessor {
} }
if (ea.cs != 0) { outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); } if (ea.cs != 0) { outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); }
if (ea.ol != 0) { outputWrite(XA_OUTLINING); outputWrite(ea.ol & 0xFF); } if (ea.ol != 0) { outputWrite(XA_OUTLINING); outputWrite(ea.ol & 0xFF); }
if (ea.tr != 0) { outputWrite(XA_TRANSPARENCY); outputWrite(ea.tr & 0xFF); }
} else { } else {
if (mode == SF_SRM_CHAR) { if (mode == SF_SRM_CHAR) {
// In character mode, output SA if character attributes differ // In character mode, output SA if character attributes differ
@@ -837,6 +1034,10 @@ public class DataStreamProcessor {
outputWrite(ORDER_SA); outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF); outputWrite(ORDER_SA); outputWrite(XA_CHARSET); outputWrite(ea.cs & 0xFF);
curCs = ea.cs; curCs = ea.cs;
} }
if (ea.tr != curTr) {
outputWrite(ORDER_SA); outputWrite(XA_TRANSPARENCY); outputWrite(ea.tr & 0xFF);
curTr = ea.tr;
}
} }
outputWrite(ea.ec & 0xFF); outputWrite(ea.ec & 0xFF);
} }
@@ -860,12 +1061,18 @@ public class DataStreamProcessor {
// ========== Read Modified ========== // ========== Read Modified ==========
public void processReadModified(boolean all) { public void processReadModified(boolean all) {
int aid = (inputProcessor != null && inputProcessor.isKeyboardLocked() && inputProcessor.getLastAid() != 0 && inputProcessor.getLastAid() != AID_NO)
? inputProcessor.getLastAid()
: AID_NO;
processReadModified(aid, all);
}
public void processReadModified(int aid, boolean all) {
if (ftDft != null && ftDft.readModified()) { if (ftDft != null && ftDft.readModified()) {
return; return;
} }
if (inputProcessor != null) { if (inputProcessor != null) {
int aid = (inputProcessor.getLastAid() != 0) ? inputProcessor.getLastAid() : AID_NO;
inputProcessor.setLastAid(AID_NO); inputProcessor.setLastAid(AID_NO);
byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all); byte[] data = inputProcessor.buildReadModifiedInboundData(aid, all);
if (outputSender != null) { if (outputSender != null) {
@@ -876,9 +1083,6 @@ public class DataStreamProcessor {
outputPos = 0; outputPos = 0;
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
if (inputProcessor != null) { if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO); inputProcessor.setLastAid(AID_NO);
} }
@@ -995,6 +1199,7 @@ public class DataStreamProcessor {
byte[] psData = new byte[fieldLen - 3]; byte[] psData = new byte[fieldLen - 3];
System.arraycopy(data, pos + 3, psData, 0, psData.length); System.arraycopy(data, pos + 3, psData, 0, psData.length);
programSymbolManager.loadps(psData); programSymbolManager.loadps(psData);
notifyScreenUpdated();
} }
break; break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
@@ -1040,9 +1245,11 @@ public class DataStreamProcessor {
log.info("ReadPartition Query — sending base query replies"); log.info("ReadPartition Query — sending base query replies");
graphicsPlane.clear(); graphicsPlane.clear();
gocaDecoder.resetDefaults(); gocaDecoder.resetDefaults();
qreplyPending = true;
sendAllQueryReplies(); sendAllQueryReplies();
break; break;
case SF_RP_QLIST: case SF_RP_QLIST:
qreplyPending = true;
if (fieldLen >= 6) { if (fieldLen >= 6) {
int listType = data[offset + 5] & 0xFF; int listType = data[offset + 5] & 0xFF;
log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType)); log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType));
@@ -1065,14 +1272,16 @@ public class DataStreamProcessor {
sendAllQueryReplies(); sendAllQueryReplies();
} }
break; break;
case CMD_RMA:
case SNA_CMD_RMA: case SNA_CMD_RMA:
processReadModified(true); processReadModified(AID_QREPLY, true);
break; break;
case SNA_CMD_RB: case SNA_CMD_RB:
processReadBuffer(); processReadBuffer(AID_QREPLY);
break; break;
case CMD_RM:
case SNA_CMD_RM: case SNA_CMD_RM:
processReadModified(false); processReadModified(AID_QREPLY, false);
break; break;
default: default:
log.fine("Unknown ReadPartition type: " + String.format("0x%02x", type)); log.fine("Unknown ReadPartition type: " + String.format("0x%02x", type));
@@ -1305,6 +1514,7 @@ public class DataStreamProcessor {
byte[] psData = new byte[fieldLen - 4]; byte[] psData = new byte[fieldLen - 4];
System.arraycopy(data, offset + 4, psData, 0, psData.length); System.arraycopy(data, offset + 4, psData, 0, psData.length);
programSymbolManager.loadps(psData); programSymbolManager.loadps(psData);
notifyScreenUpdated();
} }
break; break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
@@ -1502,6 +1712,14 @@ public class DataStreamProcessor {
processReadModified(true); processReadModified(true);
} }
public boolean isQreplyPending() {
return qreplyPending;
}
public void setQreplyPending(boolean qreplyPending) {
this.qreplyPending = qreplyPending;
}
public void processWriteStructuredField(byte[] data) { public void processWriteStructuredField(byte[] data) {
if (data != null && data.length > 0) { if (data != null && data.length > 0) {
processWriteStructuredField(data, 0, data.length); processWriteStructuredField(data, 0, data.length);
@@ -129,6 +129,7 @@ public class QueryReplyBuilder {
QR_DDM, // 0x95 - Distributed Data Management (file transfer) QR_DDM, // 0x95 - Distributed Data Management (file transfer)
QR_AUXDA, // 0x99 - Auxiliary Devices QR_AUXDA, // 0x99 - Auxiliary Devices
QR_IMP_PART, // 0xa6 - Implicit Partition Sizes QR_IMP_PART, // 0xa6 - Implicit Partition Sizes
QR_TRANSPARENCY, // 0xa8 - Background Transparency
}; };
// Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING // Vector graphics query reply codes matching HOD QueryReply3270Constants.java QR_3270_WITHOUT_DCBS_SUMMARY_STRING
@@ -218,6 +219,9 @@ public class QueryReplyBuilder {
// Implicit Partition (0xA6) // Implicit Partition (0xA6)
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
// Background Transparency (0xA8)
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")"); log.info("Built " + out.size() + " bytes of base query replies (graphicsMode=" + graphicsMode + ")");
return out.toByteArray(); return out.toByteArray();
} }
@@ -245,9 +249,9 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA); appendQueryReply(out, QR_AUXDA, STATIC_QR_AUXDA);
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY); // 0xA8
appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0 appendQueryReply(out, QR_SEGMENT, STATIC_QR_SEGMENT); // 0xB0
appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1 appendQueryReply(out, QR_PROCEDURE, STATIC_QR_PROCEDURE); // 0xB1
appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2 appendQueryReply(out, QR_LINETYPE, STATIC_QR_LINETYPE); // 0xB2
@@ -317,11 +321,7 @@ public class QueryReplyBuilder {
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
break; break;
case QR_TRANSPARENCY: // 0xA8 case QR_TRANSPARENCY: // 0xA8
if (graphicsMode.isVectorGraphicsEnabled()) { appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
appendQueryReply(out, QR_TRANSPARENCY, STATIC_QR_TRANSPARENCY);
} else {
appendQueryReply(out, QR_NULL, new byte[0]);
}
break; break;
case QR_SEGMENT: // 0xB0 case QR_SEGMENT: // 0xB0
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
@@ -390,10 +390,17 @@ public class QueryReplyBuilder {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE; int[] codes = graphicsMode.isVectorGraphicsEnabled() ? SUPPORTED_QR_VECTOR : SUPPORTED_QR_BASE;
boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()); boolean isDbcs = (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS());
boolean wroteDbcs = false;
for (int code : codes) { for (int code : codes) {
if (isDbcs && !wroteDbcs && code == QR_DDM) {
out.write(QR_OUTLINING);
out.write(QR_DBCS_ASIA);
wroteDbcs = true;
}
out.write(code); out.write(code);
if (isDbcs && code == QR_OUTLINING) { if (isDbcs && code == QR_OUTLINING && !wroteDbcs) {
out.write(QR_DBCS_ASIA); // 0x91 out.write(QR_DBCS_ASIA); // 0x91
wroteDbcs = true;
} }
} }
return out.toByteArray(); return out.toByteArray();
@@ -441,7 +448,7 @@ public class QueryReplyBuilder {
public int getCharWidth(int rows) { public int getCharWidth(int rows) {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 12; return 12; // 0x0C
} }
return SW_3279_2; // 9 return SW_3279_2; // 9
} }
@@ -452,6 +459,9 @@ public class QueryReplyBuilder {
} }
public int getCharHeight(int rows) { public int getCharHeight(int rows) {
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
return 24; // 0x18
}
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
} }
@@ -477,13 +487,58 @@ public class QueryReplyBuilder {
int cgcsgid = 0x02B9; int cgcsgid = 0x02B9;
int cpgid = 0x0025; int cpgid = 0x0025;
boolean isDbcs = false;
if (screen != null && screen.getTranslator() != null) { if (screen != null && screen.getTranslator() != null) {
cgcsgid = screen.getTranslator().getCgcsgid(); cgcsgid = screen.getTranslator().getCgcsgid();
cpgid = screen.getTranslator().getCpgid(); cpgid = screen.getTranslator().getCpgid();
isDbcs = screen.getTranslator().isDBCS();
} }
if (graphicsMode == GraphicsMode.PROGRAMMED_SYMBOLS) { if (isDbcs) {
// Programmed Symbols only mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID) int dbCgcsgid = screen.getTranslator().getDbCgcsgid();
int dbCpgid = screen.getTranslator().getDbCpgid();
byte secStart = screen.getTranslator().getDbcsSectionStart();
byte secEnd = screen.getTranslator().getDbcsSectionEnd();
// 42-byte DBCS Character Sets QR payload (total 46 bytes structured field, matching HoD DS3270.java xxxqrcs)
ByteArrayOutputStream out = new ByteArrayOutputStream(42);
out.write(0x8E); // flags: GE | DBCS / 2-byte charsets (0x8E)
out.write(0x00); // flags 2
out.write(charW); // SDW (12)
out.write(charH); // SDH (24)
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00); // 4 reserved bytes
out.write(0x0B); // DL = 11 bytes per descriptor
// Descriptor 1: Base SBCS (11 bytes)
out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write((cgcsgid >> 8) & 0xFF); out.write(cgcsgid & 0xFF);
out.write((cpgid >> 8) & 0xFF); out.write(cpgid & 0xFF);
// Descriptor 2: DBCS Set 1 (11 bytes)
out.write(0x01); // Set ID = 1
out.write(0x00); // Flags
out.write(0x80); // Local ID = 0x80 (DBCS)
out.write((charW * 2) & 0xFF); // Double cell width (24)
out.write(charH & 0xFF); // Cell height (24)
out.write(secStart); // Section start (e.g. 0x41)
out.write(secEnd); // Section end (e.g. 0xFE or 0x7F)
out.write((dbCgcsgid >> 8) & 0xFF); out.write(dbCgcsgid & 0xFF);
out.write((dbCpgid >> 8) & 0xFF); out.write(dbCpgid & 0xFF);
// Descriptor 3: APL / Text (11 bytes)
out.write(0x01); out.write(0x00); out.write(0xF1);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write(0x03); out.write(0xC3); // CGCSGID 963
out.write(0x01); out.write(0x36); // CPGID 310
return out.toByteArray();
}
if (graphicsMode.isProgrammedSymbolsEnabled()) {
// Programmed Symbols mode (3279 PS / 3179G with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
ByteArrayOutputStream out = new ByteArrayOutputStream(65); ByteArrayOutputStream out = new ByteArrayOutputStream(65);
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02) out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
out.write(0x00); // more flags out.write(0x00); // more flags
@@ -531,6 +586,7 @@ public class QueryReplyBuilder {
return out.toByteArray(); return out.toByteArray();
} }
public byte[] buildColor() { public byte[] buildColor() {
return STATIC_QR_COLOR.clone(); return STATIC_QR_COLOR.clone();
} }
@@ -681,16 +737,18 @@ public class QueryReplyBuilder {
public byte[] buildGrSymbolSet() { public byte[] buildGrSymbolSet() {
int charW = getCharWidth(); int charW = getCharWidth();
int charH = getCharHeight(); int charH = getCharHeight();
int cgcsgid = 0x02B9; if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
int cpgid = 0x0025; return new byte[]{
if (screen != null && screen.getTranslator() != null) { 0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
cgcsgid = screen.getTranslator().getCgcsgid(); 0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00, (byte) charW, (byte) charH,
cpgid = screen.getTranslator().getCpgid(); 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[]{ return new byte[]{
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12, 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, 0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
(byte) charW, (byte) charH, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00 (byte) charW, (byte) charH, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
}; };
} }
@@ -77,6 +77,34 @@ public class ECLConnection {
if (props != null) { if (props != null) {
this.properties.putAll(props); this.properties.putAll(props);
convertData(this.properties); 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); String asu = this.properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK);
if (asu != null) { if (asu != null) {
this.autoSysUnlock = "true".equalsIgnoreCase(asu) || "1".equals(asu); this.autoSysUnlock = "true".equalsIgnoreCase(asu) || "1".equals(asu);
@@ -5,13 +5,27 @@ package haus.nightmare.lib3270j.ecl;
*/ */
public interface ECLConstants { public interface ECLConstants {
// Presentation space logical planes // Presentation space logical planes (Bitmask & ordinal definitions matching IBM Host On-Demand)
int PLANE_TEXT = 1; // Unicode/EBCDIC characters int PLANE_TEXT = 1; // Unicode/EBCDIC characters
int PLANE_COLOR = 2; // Color attributes int PLANE_COLOR = 2; // Color attributes
int PLANE_HILITE = 3; // Extended highlighting int PLANE_HILITE = 3; // Extended highlighting
int PLANE_EXTENDED = 4; // Extended character sets / outlining int PLANE_EXTENDED = 4; // Extended character sets / outlining
int PLANE_FIELD = 5; // Field attribute definition bytes int PLANE_FIELD = 5; // Field attribute definition bytes
int PLANE_DBCS = 6; // Double-byte character plane int PLANE_DBCS = 6; // Double-byte character plane (lib3270j convention)
int PLANE_GRID = 8; // Grid lines / outlining plane
// Canonical IBM Host On-Demand ECLConstants plane bitmasks
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;
// Color definitions (IBM 3279 standard 16-color palette) // Color definitions (IBM 3279 standard 16-color palette)
char COLOR_NEUTRAL_BLACK = 0; char COLOR_NEUTRAL_BLACK = 0;
@@ -11,6 +11,8 @@ public class ECLErr extends Exception {
private String id = ""; private String id = "";
private String text = ""; private String text = "";
private int reasonCode = 0;
public ECLErr() { public ECLErr() {
super(); super();
} }
@@ -20,6 +22,21 @@ public class ECLErr extends Exception {
this.text = text; 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) { public ECLErr(String tag, String id, String text) {
super(formatMsg(tag, id, text)); super(formatMsg(tag, id, text));
this.tag = tag; this.tag = tag;
@@ -27,11 +44,30 @@ public class ECLErr extends Exception {
this.text = text; this.text = text;
} }
public ECLErr(String tag, String id, String text, String extra) { public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
super(formatMsg(tag, id, text + " " + extra)); super(formatMsg(location, msgNum, insert1 + " (Reason " + reasonCode + ")"));
this.tag = tag; this.tag = location;
this.id = id; this.id = msgNum;
this.text = text + " " + extra; 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) { 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 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 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(); }
public String GetErrorText() { return getMessage(); } public String GetErrorText() { return getMessage(); }
@Override @Override
public String toString() { 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; 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. */ /** Buffer address of the field attribute character. */
public int getStart() { return startPos; } public int getStart() { return startPos; }
public int GetStart() { return getStart(); } public int GetStart() { return getStart(); }
@@ -154,6 +158,8 @@ public class ECLField {
return ps.getString(dataStart, length); return ps.getString(dataStart, length);
} }
public String GetText() { return getText(); } public String GetText() { return getText(); }
public String getString() { return getText(); }
public String GetString() { return getText(); }
/** /**
* Set the text contents of this field. * Set the text contents of this field.
@@ -162,7 +168,13 @@ public class ECLField {
if (isProtected() || length <= 0) return; if (isProtected() || length <= 0) return;
ps.setText(text, dataStart); 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. * Get selector light pen type.
@@ -283,6 +295,54 @@ public class ECLField {
} }
public void Erase() { erase(); } public void Erase() { erase(); }
/**
* Check whether this field contains any DBCS characters or SO/SI delimiters.
*/
public boolean isDBCS() {
if (ps == null || length <= 0) return false;
ScreenBuffer sb = ps.getScreenBuffer();
if (sb == null) return false;
int size = sb.getRows() * sb.getCols();
if (size <= 0) return false;
for (int i = 0; i < length; i++) {
int addr = (dataStart + i) % size;
ExtendedAttribute ea = sb.getCell(addr);
if (ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT
|| ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI)) {
return true;
}
}
return false;
}
public boolean IsDBCS() { return isDBCS(); }
/**
* Check whether this field contains only DBCS characters (excluding nulls / spaces).
*/
public boolean isDBCSOnly() {
if (ps == null || length <= 0) return false;
ScreenBuffer sb = ps.getScreenBuffer();
if (sb == null) return false;
int size = sb.getRows() * sb.getCols();
if (size <= 0) return false;
boolean hasDBCS = false;
for (int i = 0; i < length; i++) {
int addr = (dataStart + i) % size;
ExtendedAttribute ea = sb.getCell(addr);
if (ea != null) {
if (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT
|| ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
hasDBCS = true;
} else if (ea.ec != 0 && ea.ec != 0x40 && ea.ucs4 != 0 && ea.ucs4 != ' ') {
return false;
}
}
}
return hasDBCS;
}
public boolean IsDBCSOnly() { return isDBCSOnly(); }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@@ -7,15 +7,17 @@ import java.util.List;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.screen.ExtendedAttribute; import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import java.util.Vector;
/** /**
* Manages the collection of all 3270 fields currently present in the Presentation Space. * Manages the collection of all 3270 fields currently present in the Presentation Space.
* Conforms to IBM ECL ECLFieldList specification. * 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 ECLPS ps;
private final ScreenBuffer screen; private final ScreenBuffer screen;
private final List<ECLField> fields = new ArrayList<>();
public ECLFieldList(ECLPS ps, ScreenBuffer screen) { public ECLFieldList(ECLPS ps, ScreenBuffer screen) {
this.ps = ps; this.ps = ps;
@@ -23,11 +25,27 @@ public class ECLFieldList {
refresh(); refresh();
} }
public ECLFieldList(ECLPS ps) {
this(ps, ps != null ? ps.getScreenBuffer() : null);
}
public ECLField getField(int index) {
return (index >= 0 && index < size()) ? get(index) : null;
}
public ECLField GetField(int index) {
return getField(index);
}
public ECLPS getPS() { return ps; }
public ScreenBuffer getScreenBuffer() { return screen; }
/** /**
* Refresh the field list by scanning the presentation space buffer. * Refresh the field list by scanning the presentation space buffer.
*/ */
public synchronized void refresh() { public synchronized void refresh() {
fields.clear(); clear();
if (screen == null || !screen.isFormatted()) { if (screen == null || !screen.isFormatted()) {
return; return;
} }
@@ -66,35 +84,35 @@ public class ECLFieldList {
} }
byte faVal = screen.getCell(startPos).fa; 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 void Refresh() { refresh(); }
public synchronized int getFieldCount() { public synchronized int getFieldCount() {
return fields.size(); return size();
} }
public int GetFieldCount() { return getFieldCount(); } public int GetFieldCount() { return getFieldCount(); }
public synchronized List<ECLField> getFields() { public synchronized List<ECLField> getFields() {
return Collections.unmodifiableList(new ArrayList<>(fields)); return Collections.unmodifiableList(new ArrayList<>(this));
} }
public List<ECLField> GetFields() { return getFields(); } public List<ECLField> GetFields() { return getFields(); }
public synchronized ECLField getFirstField() { public synchronized ECLField getFirstField() {
if (fields.isEmpty()) return null; if (isEmpty()) return null;
return fields.get(0); return get(0);
} }
public ECLField GetFirstField() { return getFirstField(); } public ECLField GetFirstField() { return getFirstField(); }
public synchronized ECLField getNextField(ECLField prev) { public synchronized ECLField getNextField(ECLField prev) {
if (prev == null || fields.isEmpty()) return getFirstField(); if (prev == null || isEmpty()) return getFirstField();
for (int i = 0; i < fields.size(); i++) { for (int i = 0; i < size(); i++) {
ECLField f = fields.get(i); ECLField f = get(i);
if (f.equals(prev) || f.getStart() == prev.getStart()) { if (f.equals(prev) || f.getStart() == prev.getStart()) {
if (i + 1 < fields.size()) { if (i + 1 < size()) {
return fields.get(i + 1); return get(i + 1);
} }
return null; return null;
} }
@@ -107,12 +125,12 @@ public class ECLFieldList {
* Find the field that contains the specified buffer position. * Find the field that contains the specified buffer position.
*/ */
public synchronized ECLField findField(int pos) { 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(); int size = screen.getRows() * screen.getCols();
if (size <= 0) return null; if (size <= 0) return null;
pos = ((pos % size) + size) % size; pos = ((pos % size) + size) % size;
for (ECLField f : fields) { for (ECLField f : this) {
int start = f.getStart(); int start = f.getStart();
int end = f.getEnd(); int end = f.getEnd();
if (start <= end) { if (start <= end) {
@@ -140,14 +158,14 @@ public class ECLFieldList {
* Get the field preceding the given field in the field list. * Get the field preceding the given field in the field list.
*/ */
public synchronized ECLField getPreviousField(ECLField next) { public synchronized ECLField getPreviousField(ECLField next) {
if (next == null || fields.isEmpty()) return null; if (next == null || isEmpty()) return null;
for (int i = 0; i < fields.size(); i++) { for (int i = 0; i < size(); i++) {
ECLField f = fields.get(i); ECLField f = get(i);
if (f.equals(next) || f.getStart() == next.getStart()) { if (f.equals(next) || f.getStart() == next.getStart()) {
if (i > 0) { if (i > 0) {
return fields.get(i - 1); return get(i - 1);
} else { } else {
return fields.get(fields.size() - 1); return get(size() - 1);
} }
} }
} }
@@ -195,29 +213,29 @@ public class ECLFieldList {
* Find field containing the given text string. * Find field containing the given text string.
*/ */
public synchronized ECLField findField(String text, int startPos) { 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(); int size = screen.getRows() * screen.getCols();
if (size <= 0) return null; if (size <= 0) return null;
startPos = ((startPos % size) + size) % size; startPos = ((startPos % size) + size) % size;
// Find starting index in field list // Find starting index in field list
int startIdx = 0; int startIdx = 0;
for (int i = 0; i < fields.size(); i++) { for (int i = 0; i < size(); i++) {
if (fields.get(i).getStart() >= startPos) { if (get(i).getStart() >= startPos) {
startIdx = i; startIdx = i;
break; break;
} }
} }
for (int i = startIdx; i < fields.size(); i++) { for (int i = startIdx; i < size(); i++) {
ECLField f = fields.get(i); ECLField f = get(i);
if (f.getText().contains(text)) { if (f.getText().contains(text)) {
return f; return f;
} }
} }
// Wrap around search to beginning of field list // Wrap around search to beginning of field list
for (int i = 0; i < startIdx; i++) { for (int i = 0; i < startIdx; i++) {
ECLField f = fields.get(i); ECLField f = get(i);
if (f.getText().contains(text)) { if (f.getText().contains(text)) {
return f; return f;
} }
@@ -265,18 +283,18 @@ public class ECLFieldList {
public boolean MatchAttributes(ECLField f, int mask) { return matchAttributes(f, mask); } public boolean MatchAttributes(ECLField f, int mask) { return matchAttributes(f, mask); }
public synchronized ECLField locateField(int attrMask, ECLField prev) { public synchronized ECLField locateField(int attrMask, ECLField prev) {
if (fields.isEmpty()) return null; if (isEmpty()) return null;
int startIndex = 0; int startIndex = 0;
if (prev != null) { if (prev != null) {
int idx = fields.indexOf(prev); int idx = indexOf(prev);
if (idx >= 0 && idx + 1 < fields.size()) { if (idx >= 0 && idx + 1 < size()) {
startIndex = idx + 1; startIndex = idx + 1;
} else { } else {
return null; return null;
} }
} }
for (int i = startIndex; i < fields.size(); i++) { for (int i = startIndex; i < size(); i++) {
ECLField f = fields.get(i); ECLField f = get(i);
if (matchAttributes(f, attrMask)) { if (matchAttributes(f, attrMask)) {
return f; return f;
} }
@@ -2,6 +2,7 @@ package haus.nightmare.lib3270j.ecl;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -109,6 +110,8 @@ public class ECLOIA implements ECLConstants {
session != null && session.getClient() != null ? session.getClient().getTelnetFSM() : null); 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) { public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIANotify listener) {
if (listener != null && !listeners.contains(listener)) { if (listener != null && !listeners.contains(listener)) {
listeners.add(listener); listeners.add(listener);
@@ -132,6 +135,29 @@ public class ECLOIA implements ECLConstants {
UnregisterOIAEvent(listener); 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) { public void RegisterOIAEvent(ECLOIAListener listener) {
if (listener != null && !oiaListeners.contains(listener)) { if (listener != null && !oiaListeners.contains(listener)) {
oiaListeners.add(listener); oiaListeners.add(listener);
@@ -166,6 +192,14 @@ public class ECLOIA implements ECLConstants {
l.OIANotifyError(this, err); l.OIANotifyError(this, err);
} catch (Exception ignored) {} } 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) { public void notifyOIAStop(int reason) {
@@ -179,6 +213,11 @@ public class ECLOIA implements ECLConstants {
l.OIANotifyStop(this, reason); l.OIANotifyStop(this, reason);
} catch (Exception ignored) {} } 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(); private final ReentrantLock oiaLock = new ReentrantLock();
@@ -210,6 +249,11 @@ public class ECLOIA implements ECLConstants {
} }
} catch (Exception ignored) {} } 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() { public boolean isInsertMode() {
@@ -267,6 +311,11 @@ public class ECLOIA implements ECLConstants {
public void setInputInhibited(int reason) { public void setInputInhibited(int reason) {
if (this.inhibitOverride != reason) { if (this.inhibitOverride != reason) {
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(); notifyOIAChanged();
} }
} }
@@ -424,6 +473,14 @@ public class ECLOIA implements ECLConstants {
return getInputInhibited(); 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. * Block until the keyboard becomes unlocked and ready for user input, or timeout expires.
* @return true if keyboard unlocked, false if timeout occurred. * @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.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.screen.ExtendedAttribute; import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -155,20 +156,94 @@ public class ECLPS implements ECLConstants {
public ECLPSTHAIServices GetECLPSTHAIServices() { return thaiServices; } public ECLPSTHAIServices GetECLPSTHAIServices() { return thaiServices; }
public ECLPSTHAIServices getTHAIServices() { return thaiServices; } public ECLPSTHAIServices getTHAIServices() { return thaiServices; }
public int getSize() { return screen.getRows() * screen.getCols(); } public int getSize() { return screen != null ? screen.getRows() * screen.getCols() : 0; }
public int getRows() { return screen.getRows(); } public int getRows() { return screen != null ? screen.getRows() : 0; }
public int getCols() { return screen.getCols(); } public int getCols() { return screen != null ? screen.getCols() : 0; }
public int getCursorPos(){ return screen.getCursorAddress(); } public int GetSize() { return getSize(); }
public int getCursorRow(){ return screen.getCursorRow(); } public int GetRows() { return getRows(); }
public int getCursorCol(){ return screen.getCursorCol(); } 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) { public void setCursorPos(int pos) {
screen.setCursorAddress(pos); if (screen != null) {
screen.setCursorAddress(pos);
}
} }
public void setCursorPos(int row, int col) { public void setCursorPos(int row, int col) {
int pos = screen.rowColToAddress(row, col); if (screen != null) {
screen.setCursorAddress(pos); 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;
} }
/** /**
@@ -189,10 +264,12 @@ public class ECLPS implements ECLConstants {
case PLANE_TEXT: case PLANE_TEXT:
if (ea.isFieldAttribute()) { if (ea.isFieldAttribute()) {
destBuffer[i] = ' '; destBuffer[i] = ' ';
} else if (ea.db == ExtendedAttribute.DB_SO || ea.db == ExtendedAttribute.DB_SI) {
destBuffer[i] = ' ';
} else if (ea.ucs4 != 0) { } else if (ea.ucs4 != 0) {
destBuffer[i] = (char) ea.ucs4; destBuffer[i] = (char) ea.ucs4;
} else if (ea.ec != 0) { } else if (ea.ec != 0) {
destBuffer[i] = translator.ebcdicToUnicode(ea.ec & 0xFF); destBuffer[i] = (translator != null) ? translator.ebcdicToUnicode(ea.ec & 0xFF) : (char) (ea.ec & 0xFF);
} else { } else {
destBuffer[i] = ' '; destBuffer[i] = ' ';
} }
@@ -203,12 +280,43 @@ public class ECLPS implements ECLConstants {
case PLANE_HILITE: case PLANE_HILITE:
destBuffer[i] = (char) (ea.gr & 0xFF); destBuffer[i] = (char) (ea.gr & 0xFF);
break; break;
case PLANE_EXTENDED: case DBCS_PLANE: // 4 (HoD DBCS_PLANE) or PLANE_EXTENDED for SBCS
destBuffer[i] = (char) (ea.cs & 0xFF); if (translator != null && translator.isDBCS()) {
if (ea.db == ExtendedAttribute.DB_LEFT) {
destBuffer[i] = (char) 0xC0; // DB_DBHIGH (first half of DBCS)
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
destBuffer[i] = (char) 0x80; // DB_DBCS (second half of DBCS)
} else if (ea.db == ExtendedAttribute.DB_SO) {
destBuffer[i] = (char) 0x20; // DB_SO
} else if (ea.db == ExtendedAttribute.DB_SI) {
destBuffer[i] = (char) 0x10; // DB_SI
} else {
destBuffer[i] = 0;
}
} else {
destBuffer[i] = (char) (ea.cs & 0xFF);
}
break; break;
case PLANE_FIELD: case PLANE_FIELD:
case FIELD_PLANE: // 16
destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0; destBuffer[i] = ea.isFieldAttribute() ? (char) (ea.fa & 0xFF) : 0;
break; break;
case PLANE_DBCS: // 6
if (ea.db == ExtendedAttribute.DB_LEFT) {
destBuffer[i] = (char) 0xC0;
} else if (ea.db == ExtendedAttribute.DB_RIGHT) {
destBuffer[i] = (char) 0x80;
} else if (ea.db == ExtendedAttribute.DB_SO) {
destBuffer[i] = (char) 0x20;
} else if (ea.db == ExtendedAttribute.DB_SI) {
destBuffer[i] = (char) 0x10;
} else {
destBuffer[i] = 0;
}
break;
case GRID_PLANE: // 8 (HoD GRID_PLANE / Outlining)
destBuffer[i] = (char) (ea.ol & 0xFF);
break;
default: default:
destBuffer[i] = ' '; destBuffer[i] = ' ';
break; break;
@@ -217,6 +325,107 @@ public class ECLPS implements ECLConstants {
return copyLen; return copyLen;
} }
public synchronized boolean IsDBCSChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && (ea.db == ExtendedAttribute.DB_LEFT || ea.db == ExtendedAttribute.DB_RIGHT);
}
public synchronized boolean isDBCSChar(int pos) {
return IsDBCSChar(pos);
}
public synchronized boolean IsDBCSChar(int row, int col) {
if (screen == null) return false;
return IsDBCSChar(screen.rowColToAddress(row, col));
}
public synchronized boolean isDBCSChar(int row, int col) {
return IsDBCSChar(row, col);
}
public synchronized boolean IsDBCS1stChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && ea.db == ExtendedAttribute.DB_LEFT;
}
public synchronized boolean isDBCS1stChar(int pos) {
return IsDBCS1stChar(pos);
}
public synchronized boolean IsDBCS1stChar(int row, int col) {
if (screen == null) return false;
return IsDBCS1stChar(screen.rowColToAddress(row, col));
}
public synchronized boolean isDBCS1stChar(int row, int col) {
return IsDBCS1stChar(row, col);
}
public synchronized boolean IsDBCS2ndChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && ea.db == ExtendedAttribute.DB_RIGHT;
}
public synchronized boolean isDBCS2ndChar(int pos) {
return IsDBCS2ndChar(pos);
}
public synchronized boolean IsDBCS2ndChar(int row, int col) {
if (screen == null) return false;
return IsDBCS2ndChar(screen.rowColToAddress(row, col));
}
public synchronized boolean isDBCS2ndChar(int row, int col) {
return IsDBCS2ndChar(row, col);
}
public synchronized boolean IsSOChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && (ea.db == ExtendedAttribute.DB_SO || (ea.ec & 0xFF) == 0x0E);
}
public synchronized boolean isSOChar(int pos) {
return IsSOChar(pos);
}
public synchronized boolean IsSIChar(int pos) {
if (screen == null) return false;
int size = getSize();
if (size <= 0) return false;
pos = ((pos % size) + size) % size;
ExtendedAttribute ea = screen.getCell(pos);
return ea != null && (ea.db == ExtendedAttribute.DB_SI || (ea.ec & 0xFF) == 0x0F);
}
public synchronized boolean isSIChar(int pos) {
return IsSIChar(pos);
}
public synchronized boolean IsSOSIChar(int pos) {
return IsSOChar(pos) || IsSIChar(pos);
}
public synchronized boolean isSOSIChar(int pos) {
return IsSOSIChar(pos);
}
public synchronized int GetScreenRect(char[] cArray, int len, int sRow, int sCol, int eRow, int eCol, int plane) { public synchronized int GetScreenRect(char[] cArray, int len, int sRow, int sCol, int eRow, int eCol, int plane) {
if (cArray == null || len <= 0 || screen == null) return 0; if (cArray == null || len <= 0 || screen == null) return 0;
int minR = Math.max(1, Math.min(sRow, eRow)); int minR = Math.max(1, Math.min(sRow, eRow));
@@ -277,6 +486,60 @@ public class ECLPS implements ECLConstants {
return getString(pos, length); 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. * Insert text directly into unprotected fields in the presentation space starting at pos.
*/ */
@@ -319,6 +582,34 @@ public class ECLPS implements ECLConstants {
setText(text, p); 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). * Search for a string in the presentation space (0-based indexing).
* Returns 0-based position, or -1 if not found. * Returns 0-based position, or -1 if not found.
@@ -500,6 +791,86 @@ public class ECLPS implements ECLConstants {
return SearchPSExt(text, startRow, startCol, endRow, endCol, dir, ignoreCase, wrap); 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) { private boolean matchesAt(String screenText, String target, int pos, int size) {
int len = target.length(); int len = target.length();
for (int j = 0; j < len; j++) { for (int j = 0; j < len; j++) {
@@ -608,13 +979,37 @@ public class ECLPS implements ECLConstants {
public void SetPasteStopAtProtectedLine(boolean val) { setPasteStopAtProtectedLine(val); } 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) { public synchronized int pasteFromExcel(String text, int row, int col) {
if (text == null || text.isEmpty() || screen == null) return 0; if (text == null || text.isEmpty() || screen == null) return 0;
if (row >= 0 && col >= 0) { if (row >= 0 && col >= 0) {
setCursorPos(row, col); setCursorPos(row, col);
} }
if (inputProcessor != null) { if (inputProcessor != null) {
return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine(), isBlockPaste()); return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine(), isBlockPaste(), isPasteWordBreak());
} }
return 0; return 0;
} }
@@ -630,7 +1025,7 @@ public class ECLPS implements ECLConstants {
if (text == null || text.isEmpty() || screen == null) return 0; if (text == null || text.isEmpty() || screen == null) return 0;
if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine() || isBlockPaste())) { if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine() || isBlockPaste())) {
setCursorPos(row, col); setCursorPos(row, col);
return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine(), isBlockPaste()); return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine(), isBlockPaste(), isPasteWordBreak());
} }
int rows = screen.getRows(); int rows = screen.getRows();
int cols = screen.getCols(); int cols = screen.getCols();
@@ -920,6 +1315,10 @@ public class ECLPS implements ECLConstants {
// ========== ECLPS Event Listener Management ========== // ========== ECLPS Event Listener Management ==========
private final java.util.List<ECLPSListener> psListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); 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 isCursorVisible() { return cursorVisible; }
public boolean GetCursorVisible() { return cursorVisible; } public boolean GetCursorVisible() { return cursorVisible; }
@@ -1007,6 +1406,111 @@ public class ECLPS implements ECLConstants {
UnregisterPSEvent(listener); 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) { public void dispatchEvent(ECLPSEvent event) {
notifyPSEvent(event); notifyPSEvent(event);
} }
@@ -1045,6 +1549,26 @@ public class ECLPS implements ECLConstants {
} }
} catch (Exception ignored) {} } 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) { public void notifyPSError(ECLErr err) {
@@ -1053,6 +1577,19 @@ public class ECLPS implements ECLConstants {
l.PSNotifyError(this, err); l.PSNotifyError(this, err);
} catch (Exception ignored) {} } 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) { public void notifyPSStop(int reason) {
@@ -1061,6 +1598,16 @@ public class ECLPS implements ECLConstants {
l.PSNotifyStop(this, reason); l.PSNotifyStop(this, reason);
} catch (Exception ignored) {} } 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) { public void notifyPSUpdate(int startRow, int startCol, int endRow, int endCol, boolean full) {
@@ -1128,6 +1675,25 @@ public class ECLPS implements ECLConstants {
sendKeys(keys); 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. * Send keystrokes with configurable inter-character or inter-mnemonic delay.
*/ */
@@ -1154,6 +1720,17 @@ public class ECLPS implements ECLConstants {
sendCharacters(keys, row, col, delayMs); 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) { public void sendCharacters(String keys, int delayMs) {
if (keys == null || keys.isEmpty()) return; if (keys == null || keys.isEmpty()) return;
if (delayMs <= 0) { if (delayMs <= 0) {
@@ -1161,37 +1738,36 @@ public class ECLPS implements ECLConstants {
return; return;
} }
int i = 0; try {
int len = keys.length(); SendKeyTokenizer tokenizer = new SendKeyTokenizer(keys);
while (i < len) { while (tokenizer.hasMoreTokens()) {
if (keys.charAt(i) == '[') { String token = tokenizer.nextToken();
if (i + 1 < len && keys.charAt(i + 1) == '[') { if (token == null) break;
// Escaped bracket "[["
sendKeys("["); if (token.length() == 1) {
i += 2; 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 { } else {
int close = keys.indexOf(']', i); for (int i = 0; i < token.length(); i++) {
if (close > i) { if (inputProcessor != null) inputProcessor.typeCharacter(token.charAt(i));
String mnemonic = keys.substring(i, close + 1); if (delayMs > 0 && (i < token.length() - 1 || tokenizer.hasMoreTokens())) {
sendKeys(mnemonic); Thread.sleep(delayMs);
i = close + 1; }
} else {
sendKeys(keys.substring(i, i + 1));
i++;
} }
} }
} else {
sendKeys(keys.substring(i, i + 1));
i++;
} }
} catch (InterruptedException e) {
if (delayMs > 0 && i < len) { Thread.currentThread().interrupt();
try { } catch (ECLErr e) {
Thread.sleep(delayMs); if (inputProcessor != null) {
} catch (InterruptedException e) { sendKeys(keys);
Thread.currentThread().interrupt();
break;
}
} }
} }
} }
@@ -196,6 +196,14 @@ public class ECLPSEvent extends EventObject {
return getECLPSUpdate(); return getECLPSUpdate();
} }
public ECLPSUpdate getPSUpdate() {
return getECLPSUpdate();
}
public ECLPSUpdate GetPSUpdate() {
return getECLPSUpdate();
}
public ECLPS getPS() { public ECLPS getPS() {
return (getSource() instanceof ECLPS) ? (ECLPS) getSource() : null; return (getSource() instanceof ECLPS) ? (ECLPS) getSource() : null;
} }
@@ -1,279 +1,487 @@
package haus.nightmare.lib3270j.ecl; package haus.nightmare.lib3270j.ecl;
import haus.nightmare.lib3270j.Telnet3270Client; import java.io.Serializable;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import java.io.StringReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; 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, * 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 final List<ECLScreenDescriptor> descriptors = new CopyOnWriteArrayList<>();
private String name = "";
private interface ScreenCondition { private boolean active = true;
boolean matches(ECLPS ps, ECLOIA oia); private boolean match = false;
} public boolean useOIAInhibitStatus = false;
public ECLScreenDesc() {} public ECLScreenDesc() {}
/** public ECLScreenDesc(String xml) {
* Clear all conditions from this descriptor. if (xml != null && !xml.trim().isEmpty()) {
*/ setXML(xml);
public synchronized void Clear() { }
conditions.clear();
} }
public synchronized void clear() { // ========== Descriptor Management ==========
Clear();
}
/** public void AddDescriptor(ECLScreenDescriptor descriptor) {
* Add a condition that the given string must appear anywhere on the presentation space. if (descriptor != null) {
*/ descriptors.add(descriptor);
public synchronized void AddString(String text) { }
}
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; if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> { AddDescriptor(new ECLSDString(text));
if (ps == null) return false;
return ps.searchString(text) >= 0;
});
} }
public void addString(String text) { AddString(text); }
public synchronized void addString(String text) { public void AddString(String text, int row, int col) {
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) {
AddString(text, row, col, true); 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) { public void AddString(String text, int row, int col, boolean caseSense) {
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) {
if (text == null || text.isEmpty()) return; if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> { AddDescriptor(new ECLSDString(text, row, col, caseSense));
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);
});
} }
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) { public void AddString(String text, int pos, 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) {
if (text == null || text.isEmpty()) return; if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> { AddDescriptor(new ECLSDString(text, pos, caseSense));
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);
});
} }
public void addString(String text, int pos, boolean caseSense) { AddString(text, pos, caseSense); }
public synchronized void addString(String text, int pos, boolean caseSense) { public void AddStringInRect(String text, int sRow, int sCol, int eRow, int eCol, 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) {
if (text == null || text.isEmpty()) return; if (text == null || text.isEmpty()) return;
conditions.add((ps, oia) -> { AddDescriptor(new ECLSDString(text, sRow, sCol, eRow, eCol, caseSense, false, false));
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());
});
} }
public void addStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
public synchronized void addStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
AddStringInRect(text, sRow, sCol, eRow, eCol, caseSense); AddStringInRect(text, sRow, sCol, eRow, eCol, caseSense);
} }
/** public void AddCursorPos(int row, int col) {
* Add a condition that the cursor must be located at 1-based (row, col). AddDescriptor(new ECLSDCursor(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) { AddCursorPos(row, col); }
public synchronized void addCursorPos(int row, int col) { public void AddCursorPos(int pos) {
AddCursorPos(row, col); AddDescriptor(new ECLSDCursor(pos));
} }
public void addCursorPos(int pos) { AddCursorPos(pos); }
/** public void AddNumFields(int count) {
* Add a condition that the cursor must be located at 1-based linear buffer position. AddDescriptor(new ECLSDFields(count));
*/
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) { AddNumFields(count); }
public synchronized void addCursorPos(int pos) { public void AddNumInputFields(int count) {
AddCursorPos(pos); AddDescriptor(new ECLSDInputFields(count));
} }
public void addNumInputFields(int count) { AddNumInputFields(count); }
/** public void AddOIAStatus(int status) {
* Add a condition on total field count on the formatted screen. ECLSDOIA oia = new ECLSDOIA();
*/ oia.setExplicitInhibitStatus(status);
public synchronized void AddNumFields(int count) { AddDescriptor(oia);
conditions.add((ps, oia) -> {
if (ps == null) return false;
return ps.getFieldList().getFieldCount() == count;
});
} }
public void addOIAStatus(int status) { AddOIAStatus(status); }
public synchronized void addNumFields(int count) { public void AddOIAInhibitStatus(int status) {
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) {
AddOIAStatus(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) { public synchronized boolean Matches(ECLPS ps, ECLOIA oia) {
if (conditions.isEmpty()) { if (descriptors.isEmpty()) {
this.match = true;
return true; return true;
} }
for (ScreenCondition cond : conditions) { if (ps == null) {
if (!cond.matches(ps, oia)) { this.match = false;
return 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) { public boolean matches(ECLPS ps, ECLOIA oia) { return Matches(ps, oia); }
return Matches(ps, oia);
}
public boolean Matches(ECLSession session) { public boolean Matches(ECLSession session) {
if (session == null) return false; if (session == null) return false;
return Matches(session.GetPS(), session.GetOIA()); 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) { public boolean Matches(Telnet3270Client client) {
if (client == null) return false; if (client == null) return false;
return Matches(client.getPS(), client.getOIA()); 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) { public boolean Matches(ScreenBuffer buf) {
if (buf == null) return false; if (buf == null) return false;
return Matches(new ECLPS(buf, null, null), null); return Matches(new ECLPS(buf, null, null), null);
} }
public boolean matches(ScreenBuffer buf) { public boolean matches(ScreenBuffer buf) { return Matches(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() { @Override
return conditions.size(); 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 ECLSession session;
private final ECLPS ps; private final ECLPS ps;
private final ECLOIA oia;
private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>(); private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>();
private final Map<ECLScreenDesc, ECLPSListener> listenerMap = new ConcurrentHashMap<>(); private final Map<ECLScreenDesc, ECLPSListener> listenerMap = new ConcurrentHashMap<>();
private final ECLPSListener internalListener; private final ECLPSListener internalListener;
private final ECLOIANotify internalOiaListener;
public ECLScreenReco() { public ECLScreenReco() {
this((ECLPS) null); this((ECLPS) null);
} }
public ECLScreenReco(ECLSession session) { 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) { public ECLScreenReco(ECLPS ps) {
this(ps, (ECLOIA) null);
}
public ECLScreenReco(ECLPS ps, ECLOIA oia) {
this.ps = ps; this.ps = ps;
this.session = (ps != null) ? ps.GetParent() : null; this.session = (ps != null) ? ps.GetParent() : null;
this.oia = (oia != null) ? oia : ((session != null) ? session.GetOIA() : null);
this.internalListener = new ECLPSListener() { this.internalListener = new ECLPSListener() {
@Override @Override
@@ -34,9 +41,14 @@ public class ECLScreenReco {
} }
}; };
this.internalOiaListener = event -> checkAllScreens();
if (this.ps != null) { if (this.ps != null) {
this.ps.RegisterPSEvent(this.internalListener); this.ps.RegisterPSEvent(this.internalListener);
} }
if (this.oia != null) {
this.oia.RegisterOIAEvent(this.internalOiaListener);
}
} }
// ========== Static Recognition Helpers ========== // ========== Static Recognition Helpers ==========
@@ -95,6 +107,16 @@ public class ECLScreenReco {
UnregisterScreen(desc, notify); 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) { public void RegisterScreen(ECLScreenDesc desc, ECLPSListener listener) {
if (desc != null && listener != null) { if (desc != null && listener != null) {
listenerMap.put(desc, listener); listenerMap.put(desc, listener);
@@ -120,7 +142,10 @@ public class ECLScreenReco {
} }
public boolean IsMatch(ECLScreenDesc desc) { 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) { public boolean isMatch(ECLScreenDesc desc) {
return IsMatch(desc); return IsMatch(desc);
@@ -142,6 +167,9 @@ public class ECLScreenReco {
if (ps != null && internalListener != null) { if (ps != null && internalListener != null) {
ps.UnregisterPSEvent(internalListener); ps.UnregisterPSEvent(internalListener);
} }
if (oia != null && internalOiaListener != null) {
oia.UnregisterOIAEvent(internalOiaListener);
}
notifyMap.clear(); notifyMap.clear();
listenerMap.clear(); listenerMap.clear();
} }
@@ -41,6 +41,7 @@ public class ECLSession {
public static final String BLOCK_PASTE = "blockPaste"; public static final String BLOCK_PASTE = "blockPaste";
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap"; public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
public static final String PASTE_LINE_WRAP = "pasteLineWrap"; 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_DOCMODE = "EntryAssist_DOCmode";
public static final String ENTRYASSIST_DOCWORDWRAP = "EntryAssist_DOCwordWrap"; public static final String ENTRYASSIST_DOCWORDWRAP = "EntryAssist_DOCwordWrap";
public static final String ENTRYASSIST_STARTCOL = "EntryAssist_startCol"; public static final String ENTRYASSIST_STARTCOL = "EntryAssist_startCol";
@@ -456,11 +456,26 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
this.bytesTransferred = 0; this.bytesTransferred = 0;
this.totalBytes = (config.isSend() && localFile.exists()) ? localFile.length() : 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(); String command = config.buildCommand();
log.info("ECLXfer: initiating transfer: " + command); log.info("ECLXfer: initiating transfer: " + command);
int capacity = input.kybdPrime(); int capacity = input != null ? input.kybdPrime() : 0;
if (capacity < command.length()) { if (input != null && capacity < command.length()) {
String err = "Input field capacity insufficient for command (" + capacity + " chars)"; String err = "Input field capacity insufficient for command (" + capacity + " chars)";
fireEvent(ECLXferEvent.XFER_ABORTED, FTConstants.ECL_ERR_XFER_ABORT, err); fireEvent(ECLXferEvent.XFER_ABORTED, FTConstants.ECL_ERR_XFER_ABORT, err);
return FTConstants.ECL_ERR_XFER_ABORT; return FTConstants.ECL_ERR_XFER_ABORT;
@@ -469,10 +484,25 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
setState(FTState.AWAIT_ACK); setState(FTState.AWAIT_ACK);
fireEvent(ECLXferEvent.XFER_STARTED, 0, "Transfer initiated"); fireEvent(ECLXferEvent.XFER_STARTED, 0, "Transfer initiated");
input.emulateInput(command + "\n"); if (input != null) {
input.emulateInput(command + "\n");
}
return 0; 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) { private void parseOptionsIntoConfig(FTConfig config, String options) {
if (options == null || options.trim().isEmpty()) return; if (options == null || options.trim().isEmpty()) return;
config.parseOptions(options); 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);
}
}

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