Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
aa724fbcef
|
|||
|
8a30efe9c0
|
|||
|
5049d8df74
|
|||
|
badd026055
|
|||
|
ef70ea0f5a
|
@@ -42,8 +42,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
// Dialog instances
|
||||
private FindDialog findDialog;
|
||||
private ScriptDialog scriptDialog;
|
||||
private MacroDialog macroDialog;
|
||||
private FieldInspectorDialog fieldInspectorDialog;
|
||||
private PrinterSessionDialog printerSessionDialog;
|
||||
private ScreenHistoryDialog screenHistoryDialog;
|
||||
|
||||
// Modes menu items
|
||||
private JCheckBoxMenuItem docModeItem;
|
||||
@@ -94,6 +96,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
if (printerSessionDialog != null) {
|
||||
printerSessionDialog.dispose();
|
||||
}
|
||||
if (screenHistoryDialog != null) {
|
||||
screenHistoryDialog.dispose();
|
||||
}
|
||||
terminalPanel.dispose();
|
||||
}
|
||||
|
||||
@@ -104,6 +109,29 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure clicking anywhere inside the frame brings focus to window and terminalPanel
|
||||
Toolkit.getDefaultToolkit().addAWTEventListener(event -> {
|
||||
if (event instanceof MouseEvent && event.getID() == MouseEvent.MOUSE_PRESSED) {
|
||||
MouseEvent me = (MouseEvent) event;
|
||||
Object src = me.getSource();
|
||||
if (src instanceof Component) {
|
||||
Window win = SwingUtilities.getWindowAncestor((Component) src);
|
||||
if (win == J3270App.this && !isAnyChildDialogActive()) {
|
||||
if (!J3270App.this.isFocused() || !J3270App.this.isActive()) {
|
||||
J3270App.this.toFront();
|
||||
J3270App.this.requestFocus();
|
||||
}
|
||||
boolean isMenu = (src instanceof JMenuBar) || (src instanceof JMenuItem) || (src instanceof JPopupMenu)
|
||||
|| SwingUtilities.getAncestorOfClass(JMenuBar.class, (Component) src) != null
|
||||
|| SwingUtilities.getAncestorOfClass(JPopupMenu.class, (Component) src) != null;
|
||||
if (!isMenu && terminalPanel != null && !terminalPanel.hasFocus()) {
|
||||
terminalPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, AWTEvent.MOUSE_EVENT_MASK);
|
||||
}
|
||||
|
||||
private boolean isAnyChildDialogActive() {
|
||||
@@ -113,8 +141,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}
|
||||
if (findDialog != null && findDialog.isVisible()) return true;
|
||||
if (scriptDialog != null && scriptDialog.isVisible()) return true;
|
||||
if (macroDialog != null && macroDialog.isVisible()) return true;
|
||||
if (fieldInspectorDialog != null && fieldInspectorDialog.isVisible()) return true;
|
||||
if (printerSessionDialog != null && printerSessionDialog.isVisible()) return true;
|
||||
if (screenHistoryDialog != null && screenHistoryDialog.isVisible()) return true;
|
||||
for (Window owned : getOwnedWindows()) {
|
||||
if (owned != null && owned.isVisible()) {
|
||||
return true;
|
||||
@@ -181,6 +211,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
|
||||
// 2. Edit menu
|
||||
JMenu editMenu = createMenu("Edit");
|
||||
editMenu.add(createMenuItem("Cut", KeyEvent.VK_X, () -> terminalPanel.cutSelection()));
|
||||
editMenu.add(createMenuItem("Copy", KeyEvent.VK_C, () -> terminalPanel.copySelection()));
|
||||
editMenu.add(createMenuItem("Paste", KeyEvent.VK_V, () -> terminalPanel.pasteClipboard()));
|
||||
editMenu.add(createMenuItem("Paste with Line Wrap...", -1, this::showPasteLineWrapDialog));
|
||||
@@ -312,6 +343,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
viewMenu.add(gfxMenu);
|
||||
|
||||
viewMenu.addSeparator();
|
||||
viewMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
|
||||
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
|
||||
menuBar.add(viewMenu);
|
||||
|
||||
@@ -430,6 +462,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
}, true));
|
||||
actionsMenu.addSeparator();
|
||||
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
|
||||
actionsMenu.add(createMenuItem("Macro Runtime (<HASCRIPT>)...", KeyEvent.VK_M, this::showMacroDialog, true));
|
||||
actionsMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
|
||||
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
|
||||
menuBar.add(actionsMenu);
|
||||
|
||||
@@ -536,6 +570,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
scriptDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void showMacroDialog() {
|
||||
if (macroDialog == null) {
|
||||
macroDialog = new MacroDialog(this, client, terminalPanel);
|
||||
}
|
||||
macroDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void showFieldInspectorDialog() {
|
||||
if (client == null) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a host before inspecting fields.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
@@ -559,6 +600,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
printerSessionDialog.setVisible(true);
|
||||
}
|
||||
|
||||
private void showScreenHistoryDialog() {
|
||||
if (screenHistoryDialog == null) {
|
||||
screenHistoryDialog = new ScreenHistoryDialog(this, client);
|
||||
} else {
|
||||
if (client != null && client.getScreenHistoryBuffer() != null) {
|
||||
screenHistoryDialog.refreshHistoryView(client.getScreenHistoryBuffer().size() - 1);
|
||||
}
|
||||
}
|
||||
screenHistoryDialog.setVisible(true);
|
||||
screenHistoryDialog.toFront();
|
||||
}
|
||||
|
||||
private void showPasteLineWrapDialog() {
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
JOptionPane.showMessageDialog(this, "Connect to a 3270 session before pasting.", "Not Connected", JOptionPane.WARNING_MESSAGE);
|
||||
@@ -694,6 +747,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls());
|
||||
dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert());
|
||||
dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
|
||||
dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
dialog.setVisible(true);
|
||||
|
||||
if (dialog.isConfirmed()) {
|
||||
@@ -730,6 +784,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls());
|
||||
haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert());
|
||||
haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage());
|
||||
if (config.getGraphicsMode() != null) {
|
||||
haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode());
|
||||
}
|
||||
|
||||
disconnect();
|
||||
|
||||
@@ -773,6 +830,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
|
||||
fileTransfer.cancel();
|
||||
fileTransfer = null;
|
||||
}
|
||||
if (macroDialog != null) {
|
||||
macroDialog.stopMacro();
|
||||
}
|
||||
Telnet3270Client c = client;
|
||||
client = null;
|
||||
new Thread(() -> {
|
||||
|
||||
@@ -439,6 +439,33 @@ public class Settings {
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getTrimRectSizingHandles() {
|
||||
return prefs.getBoolean("trimRectSizingHandles", false);
|
||||
}
|
||||
|
||||
public static void setTrimRectSizingHandles(boolean val) {
|
||||
prefs.putBoolean("trimRectSizingHandles", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getTrimRectRemainAfterEdit() {
|
||||
return prefs.getBoolean("trimRectRemainAfterEdit", false);
|
||||
}
|
||||
|
||||
public static void setTrimRectRemainAfterEdit(boolean val) {
|
||||
prefs.putBoolean("trimRectRemainAfterEdit", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getPasteWordBreak() {
|
||||
return prefs.getBoolean("pasteWordBreak", true);
|
||||
}
|
||||
|
||||
public static void setPasteWordBreak(boolean val) {
|
||||
prefs.putBoolean("pasteWordBreak", val);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
// ========== Crosshair Ruler ==========
|
||||
|
||||
public static boolean getCrosshairRuler() {
|
||||
@@ -449,6 +476,71 @@ public class Settings {
|
||||
prefs.putBoolean("crosshairRuler", enabled);
|
||||
}
|
||||
|
||||
// ========== Phase 25: Hotspots & Mouse Wheel Navigation ==========
|
||||
|
||||
public static boolean getHotspotsEnabled() {
|
||||
return prefs.getBoolean("hotspotsEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getHotspotsUrlEnabled() {
|
||||
return prefs.getBoolean("hotspotsUrlEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsUrlEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsUrlEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getHotspotsPfEnabled() {
|
||||
return prefs.getBoolean("hotspotsPfEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsPfEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsPfEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getHotspotsMenuChoicesEnabled() {
|
||||
return prefs.getBoolean("hotspotsMenuChoicesEnabled", true);
|
||||
}
|
||||
|
||||
public static void setHotspotsMenuChoicesEnabled(boolean enabled) {
|
||||
prefs.putBoolean("hotspotsMenuChoicesEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static boolean getMouseWheelEnabled() {
|
||||
return prefs.getBoolean("mouseWheelEnabled", true);
|
||||
}
|
||||
|
||||
public static void setMouseWheelEnabled(boolean enabled) {
|
||||
prefs.putBoolean("mouseWheelEnabled", enabled);
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static String getMouseWheelUpKey() {
|
||||
return prefs.get("mouseWheelUpKey", "[pf7]");
|
||||
}
|
||||
|
||||
public static void setMouseWheelUpKey(String key) {
|
||||
prefs.put("mouseWheelUpKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf7]");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
public static String getMouseWheelDownKey() {
|
||||
return prefs.get("mouseWheelDownKey", "[pf8]");
|
||||
}
|
||||
|
||||
public static void setMouseWheelDownKey(String key) {
|
||||
prefs.put("mouseWheelDownKey", key != null && !key.trim().isEmpty() ? key.trim() : "[pf8]");
|
||||
flushPrefs();
|
||||
}
|
||||
|
||||
// ========== Status Bar Visibility ==========
|
||||
|
||||
public static boolean getStatusBarVisible() {
|
||||
@@ -685,6 +777,18 @@ public class Settings {
|
||||
case "pasteStopAtProtected":
|
||||
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectSizingHandles":
|
||||
case "sizingHandles":
|
||||
setTrimRectSizingHandles(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectRemainAfterEdit":
|
||||
case "remainAfterEdit":
|
||||
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteWordBreak":
|
||||
case "wordBreak":
|
||||
setPasteWordBreak(Boolean.parseBoolean(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown behavior/connection key: " + key);
|
||||
}
|
||||
@@ -706,6 +810,18 @@ public class Settings {
|
||||
case "pasteStopAtProtected":
|
||||
setPasteStopAtProtectedLine(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectSizingHandles":
|
||||
case "sizingHandles":
|
||||
setTrimRectSizingHandles(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "trimRectRemainAfterEdit":
|
||||
case "remainAfterEdit":
|
||||
setTrimRectRemainAfterEdit(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "pasteWordBreak":
|
||||
case "wordBreak":
|
||||
setPasteWordBreak(Boolean.parseBoolean(value));
|
||||
break;
|
||||
default:
|
||||
log.warning("Unknown clipboard key: " + key);
|
||||
}
|
||||
@@ -840,6 +956,9 @@ public class Settings {
|
||||
w.println("autoSysUnlock = " + getAutoSysUnlock());
|
||||
w.println("enablePasteFromExcel = " + getEnablePasteFromExcel());
|
||||
w.println("pasteStopAtProtectedLine = " + getPasteStopAtProtectedLine());
|
||||
w.println("trimRectSizingHandles = " + getTrimRectSizingHandles());
|
||||
w.println("trimRectRemainAfterEdit = " + getTrimRectRemainAfterEdit());
|
||||
w.println("pasteWordBreak = " + getPasteWordBreak());
|
||||
w.println();
|
||||
|
||||
// [entryassist]
|
||||
|
||||
@@ -234,7 +234,6 @@ public class HostStorage {
|
||||
if (map.containsKey("tn3270e")) h.setTn3270e(parseBool(map.get("tn3270e"), true));
|
||||
if (map.containsKey("graphicsMode")) {
|
||||
String gm = String.valueOf(map.get("graphicsMode"));
|
||||
if ("PROGRAMMED_SYMBOLS".equalsIgnoreCase(gm)) gm = "BOTH";
|
||||
h.setGraphicsMode(gm);
|
||||
}
|
||||
if (map.containsKey("codePage")) h.setCodePage(String.valueOf(map.get("codePage")));
|
||||
|
||||
@@ -6,6 +6,7 @@ import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.TerminalModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
@@ -226,6 +227,24 @@ public class ConnectDialog extends JDialog {
|
||||
graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode());
|
||||
graphicsCombo.setFont(ThemeManager.getUiFont());
|
||||
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);
|
||||
|
||||
gridy++;
|
||||
@@ -664,7 +683,7 @@ public class ConnectDialog extends JDialog {
|
||||
|
||||
String gfx = "BOTH";
|
||||
if (graphicsCombo.getSelectedItem() != null) {
|
||||
gfx = graphicsCombo.getSelectedItem().toString();
|
||||
gfx = ((haus.nightmare.lib3270j.graphics.GraphicsMode) graphicsCombo.getSelectedItem()).name();
|
||||
}
|
||||
|
||||
String cp = "037";
|
||||
@@ -731,7 +750,9 @@ public class ConnectDialog extends JDialog {
|
||||
result.setLuName(lu);
|
||||
}
|
||||
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();
|
||||
@@ -776,6 +797,14 @@ public class ConnectDialog extends JDialog {
|
||||
result.setProxy(pType, pHost, pPort, pUser.isEmpty() ? null : pUser, pPass.isEmpty() ? null : pPass);
|
||||
}
|
||||
|
||||
if (selectedProfileId != null) {
|
||||
SavedHost existing = HostStorage.findById(selectedProfileId);
|
||||
String pName = (existing != null && existing.getName() != null && !existing.getName().trim().isEmpty())
|
||||
? existing.getName() : host;
|
||||
SavedHost updated = buildCurrentSavedHost(pName);
|
||||
HostStorage.saveHost(updated);
|
||||
}
|
||||
|
||||
confirmed = true;
|
||||
dispose();
|
||||
}
|
||||
@@ -810,6 +839,12 @@ public class ConnectDialog extends JDialog {
|
||||
tn3270eCheckBox.setSelected(tn3270e);
|
||||
}
|
||||
|
||||
public void setInitialGraphicsMode(haus.nightmare.lib3270j.graphics.GraphicsMode mode) {
|
||||
if (mode != null && selectedProfileId == null && graphicsCombo != null) {
|
||||
graphicsCombo.setSelectedItem(mode);
|
||||
}
|
||||
}
|
||||
|
||||
public void setInitialCodePage(String cp) {
|
||||
if (cp != null && codePageCombo != null) {
|
||||
for (int i = 0; i < codePageCombo.getItemCount(); i++) {
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Real-time hotspot and URL auto-detection engine matching IBM Host On-Demand
|
||||
* ShowURLOptions and ScreenText parity.
|
||||
*
|
||||
* Scans screen text using regular expressions for:
|
||||
* 1. URLs (http://, https://, ftp://, file://, mailto:, www.)
|
||||
* 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
|
||||
* 3. Menu choices ("1.", "2.", "===> 3", "=> 4")
|
||||
*
|
||||
* Provides hit-testing for mouse hover/clicks and generates keystroke actions.
|
||||
*/
|
||||
public class HotspotEngine {
|
||||
|
||||
public enum HotspotType {
|
||||
URL,
|
||||
PF_KEY,
|
||||
ENTER,
|
||||
MENU_CHOICE
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an active interactive hotspot region on the terminal screen.
|
||||
*/
|
||||
public static class Hotspot {
|
||||
private final HotspotType type;
|
||||
private final int startRow;
|
||||
private final int startCol;
|
||||
private final int endRow;
|
||||
private final int endCol;
|
||||
private final String displayText;
|
||||
private final String action;
|
||||
private final String tooltip;
|
||||
|
||||
public Hotspot(HotspotType type, int startRow, int startCol, int endRow, int endCol,
|
||||
String displayText, String action, String tooltip) {
|
||||
this.type = type;
|
||||
this.startRow = startRow;
|
||||
this.startCol = startCol;
|
||||
this.endRow = endRow;
|
||||
this.endCol = endCol;
|
||||
this.displayText = displayText;
|
||||
this.action = action;
|
||||
this.tooltip = tooltip;
|
||||
}
|
||||
|
||||
public HotspotType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getStartRow() {
|
||||
return startRow;
|
||||
}
|
||||
|
||||
public int getStartCol() {
|
||||
return startCol;
|
||||
}
|
||||
|
||||
public int getEndRow() {
|
||||
return endRow;
|
||||
}
|
||||
|
||||
public int getEndCol() {
|
||||
return endCol;
|
||||
}
|
||||
|
||||
public String getDisplayText() {
|
||||
return displayText;
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public String getTooltip() {
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the given row/col coordinate falls within this hotspot.
|
||||
*/
|
||||
public boolean contains(int row, int col) {
|
||||
if (row < startRow || row > endRow) {
|
||||
return false;
|
||||
}
|
||||
if (startRow == endRow) {
|
||||
return col >= startCol && col <= endCol;
|
||||
}
|
||||
if (row == startRow) {
|
||||
return col >= startCol;
|
||||
}
|
||||
if (row == endRow) {
|
||||
return col <= endCol;
|
||||
}
|
||||
return true; // Intermediate rows
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Hotspot{" + type + " [" + startRow + "," + startCol + "]-[" + endRow + "," + endCol + "] '" + displayText + "'}";
|
||||
}
|
||||
}
|
||||
|
||||
// Configurable scan enablement
|
||||
private boolean urlEnabled = true;
|
||||
private boolean pfKeyEnabled = true;
|
||||
private boolean menuChoiceEnabled = true;
|
||||
|
||||
// Regular expression patterns
|
||||
// 1. URLs
|
||||
private static final Pattern URL_PATTERN = Pattern.compile(
|
||||
"(?:https?|ftp|file)://[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|www\\.[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+|mailto:[\\w\\-._~:/?#\\[\\]@!$&'*+,;=%]+",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
// 2. Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER], ENTER)
|
||||
private static final Pattern PF_KEY_PATTERN = Pattern.compile(
|
||||
"(?:(?<=\\W)|^)(PF(?:[1-9]|1[0-9]|2[0-4])|F(?:[1-9]|1[0-9]|2[0-4])|PA[1-3]|\\[ENTER\\]|ENTER)(?:(?=\\W)|$)",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
// 3. Menu choices: "1.", "2.", "===> 3", "=> 4", "--> 5"
|
||||
private static final Pattern MENU_NUMBER_PATTERN = Pattern.compile(
|
||||
"(?:(?<=\\s)|^)(\\d{1,2})\\.(?:(?=\\s)|$)"
|
||||
);
|
||||
|
||||
private static final Pattern MENU_PROMPT_PATTERN = Pattern.compile(
|
||||
"(?:===>|=>|-->)\\s*(\\d{1,2})(?:(?=\\s)|$)"
|
||||
);
|
||||
|
||||
// Caching state to avoid rescanning unchanged screens
|
||||
private String cachedScreenText = "";
|
||||
private int cachedRows = 0;
|
||||
private int cachedCols = 0;
|
||||
private List<Hotspot> cachedHotspots = Collections.emptyList();
|
||||
|
||||
public HotspotEngine() {
|
||||
}
|
||||
|
||||
public boolean isUrlEnabled() {
|
||||
return urlEnabled;
|
||||
}
|
||||
|
||||
public void setUrlEnabled(boolean urlEnabled) {
|
||||
this.urlEnabled = urlEnabled;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
public boolean isPfKeyEnabled() {
|
||||
return pfKeyEnabled;
|
||||
}
|
||||
|
||||
public void setPfKeyEnabled(boolean pfKeyEnabled) {
|
||||
this.pfKeyEnabled = pfKeyEnabled;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
public boolean isMenuChoiceEnabled() {
|
||||
return menuChoiceEnabled;
|
||||
}
|
||||
|
||||
public void setMenuChoiceEnabled(boolean menuChoiceEnabled) {
|
||||
this.menuChoiceEnabled = menuChoiceEnabled;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
public void invalidateCache() {
|
||||
cachedScreenText = "";
|
||||
cachedHotspots = Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the given ScreenBuffer for interactive hotspots.
|
||||
*/
|
||||
public synchronized List<Hotspot> scan(ScreenBuffer sb) {
|
||||
if (sb == null) return Collections.emptyList();
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
|
||||
// Build plain text representation
|
||||
StringBuilder fullText = new StringBuilder(rows * (cols + 1));
|
||||
for (int r = 0; r < rows; r++) {
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int baddr = r * cols + c;
|
||||
ExtendedAttribute ea = sb.getDisplayCell(baddr);
|
||||
if (ea == null || ea.isFieldAttribute() || ea.ucs4 <= 0x20 || ea.ucs4 == 0xFF) {
|
||||
fullText.append(' ');
|
||||
} else {
|
||||
fullText.append((char) ea.ucs4);
|
||||
}
|
||||
}
|
||||
if (r < rows - 1) {
|
||||
fullText.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
return scanText(fullText.toString(), rows, cols);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan formatted screen text (lines separated by '\n') for hotspots.
|
||||
*/
|
||||
public synchronized List<Hotspot> scanText(String screenText, int rows, int cols) {
|
||||
if (screenText == null || rows <= 0 || cols <= 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (screenText.equals(cachedScreenText) && rows == cachedRows && cols == cachedCols) {
|
||||
return cachedHotspots;
|
||||
}
|
||||
|
||||
List<Hotspot> hotspots = new ArrayList<>();
|
||||
String[] lines = screenText.split("\n", -1);
|
||||
int actualRows = Math.min(rows, lines.length);
|
||||
|
||||
// Scan line by line for precise row/column tracking
|
||||
for (int r = 0; r < actualRows; r++) {
|
||||
String line = lines[r];
|
||||
boolean[] occupied = new boolean[line.length() + 1];
|
||||
|
||||
// 1. Scan URLs
|
||||
if (urlEnabled) {
|
||||
Matcher mUrl = URL_PATTERN.matcher(line);
|
||||
while (mUrl.find()) {
|
||||
int startCol = mUrl.start();
|
||||
int endCol = mUrl.end();
|
||||
String match = mUrl.group();
|
||||
|
||||
// Trim trailing punctuation like . , ; : ) ]
|
||||
while (match.length() > 1) {
|
||||
char last = match.charAt(match.length() - 1);
|
||||
if (last == '.' || last == ',' || last == ';' || last == ':' || last == ')' || last == ']' || last == '>') {
|
||||
match = match.substring(0, match.length() - 1);
|
||||
endCol--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match.isEmpty()) continue;
|
||||
|
||||
String actionUrl = match;
|
||||
if (match.toLowerCase().startsWith("www.")) {
|
||||
actionUrl = "http://" + match;
|
||||
}
|
||||
|
||||
Hotspot h = new Hotspot(
|
||||
HotspotType.URL,
|
||||
r, startCol, r, endCol - 1,
|
||||
match, actionUrl, "Open URL: " + actionUrl
|
||||
);
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Scan PF / Function keys
|
||||
if (pfKeyEnabled) {
|
||||
Matcher mPf = PF_KEY_PATTERN.matcher(line);
|
||||
while (mPf.find()) {
|
||||
int startCol = mPf.start(1);
|
||||
int endCol = mPf.end(1);
|
||||
if (isRangeOccupied(occupied, startCol, endCol)) continue;
|
||||
|
||||
String keyText = mPf.group(1).toUpperCase();
|
||||
String action;
|
||||
String tooltip;
|
||||
|
||||
if (keyText.equals("ENTER") || keyText.equals("[ENTER]")) {
|
||||
action = "[enter]";
|
||||
tooltip = "Send [ENTER]";
|
||||
} else if (keyText.startsWith("PF")) {
|
||||
String numStr = keyText.substring(2);
|
||||
action = "[pf" + numStr + "]";
|
||||
tooltip = "Send " + action.toUpperCase();
|
||||
} else if (keyText.startsWith("F")) {
|
||||
String numStr = keyText.substring(1);
|
||||
action = "[pf" + numStr + "]";
|
||||
tooltip = "Send " + action.toUpperCase();
|
||||
} else if (keyText.startsWith("PA")) {
|
||||
String numStr = keyText.substring(2);
|
||||
action = "[pa" + numStr + "]";
|
||||
tooltip = "Send " + action.toUpperCase();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
Hotspot h = new Hotspot(
|
||||
keyText.contains("ENTER") ? HotspotType.ENTER : HotspotType.PF_KEY,
|
||||
r, startCol, r, endCol - 1,
|
||||
mPf.group(1), action, tooltip
|
||||
);
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Scan Menu choices ("1.", "2.", "===> 3", "=> 4")
|
||||
if (menuChoiceEnabled) {
|
||||
// Pattern 3a: Number followed by dot (e.g. " 1. ")
|
||||
Matcher mNum = MENU_NUMBER_PATTERN.matcher(line);
|
||||
while (mNum.find()) {
|
||||
int startCol = mNum.start(1);
|
||||
int endCol = mNum.end();
|
||||
if (isRangeOccupied(occupied, startCol, endCol)) continue;
|
||||
|
||||
String choiceNum = mNum.group(1);
|
||||
String action = choiceNum + "[enter]";
|
||||
String tooltip = "Select option " + choiceNum;
|
||||
|
||||
Hotspot h = new Hotspot(
|
||||
HotspotType.MENU_CHOICE,
|
||||
r, startCol, r, endCol - 1,
|
||||
choiceNum + ".", action, tooltip
|
||||
);
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
|
||||
// Pattern 3b: Prompt followed by number (e.g. "===> 3")
|
||||
Matcher mPrompt = MENU_PROMPT_PATTERN.matcher(line);
|
||||
while (mPrompt.find()) {
|
||||
int startCol = mPrompt.start();
|
||||
int endCol = mPrompt.end();
|
||||
if (isRangeOccupied(occupied, startCol, endCol)) continue;
|
||||
|
||||
String choiceNum = mPrompt.group(1);
|
||||
String action = choiceNum + "[enter]";
|
||||
String tooltip = "Execute choice " + choiceNum;
|
||||
|
||||
Hotspot h = new Hotspot(
|
||||
HotspotType.MENU_CHOICE,
|
||||
r, startCol, r, endCol - 1,
|
||||
mPrompt.group(), action, tooltip
|
||||
);
|
||||
hotspots.add(h);
|
||||
markOccupied(occupied, startCol, endCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedScreenText = screenText;
|
||||
this.cachedRows = rows;
|
||||
this.cachedCols = cols;
|
||||
this.cachedHotspots = Collections.unmodifiableList(hotspots);
|
||||
|
||||
return cachedHotspots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit-test for a hotspot at the specified terminal row and column.
|
||||
*/
|
||||
public Hotspot findHotspot(int row, int col) {
|
||||
for (Hotspot h : cachedHotspots) {
|
||||
if (h.contains(row, col)) {
|
||||
return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query currently active cached hotspots.
|
||||
*/
|
||||
public List<Hotspot> getHotspots() {
|
||||
return cachedHotspots;
|
||||
}
|
||||
|
||||
private static void markOccupied(boolean[] arr, int start, int end) {
|
||||
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
|
||||
arr[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRangeOccupied(boolean[] arr, int start, int end) {
|
||||
for (int i = Math.max(0, start); i < Math.min(arr.length, end); i++) {
|
||||
if (arr[i]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -206,7 +206,8 @@ public class KeyBindings {
|
||||
|
||||
/**
|
||||
* Parse a .kmp file from a Reader.
|
||||
* Supports both INI-style KEY<n>=action formats and bind statements.
|
||||
* Supports both IBM Host On-Demand (HoD) .kmp format (hashKey prefixes)
|
||||
* and IBM Personal Communications (PCOMM) INI-style and bind statements.
|
||||
*
|
||||
* @param reader the reader to parse from
|
||||
* @return Map of j3270 action names to key stroke bindings
|
||||
@@ -214,11 +215,38 @@ public class KeyBindings {
|
||||
*/
|
||||
public static Map<String, String> parseKmp(Reader reader) throws IOException {
|
||||
BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);
|
||||
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
|
||||
|
||||
List<String> rawLines = new ArrayList<>();
|
||||
boolean hasHodKeyRemap = false;
|
||||
boolean hasHodHashKey = false;
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
line = line.trim();
|
||||
rawLines.add(line);
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.equalsIgnoreCase("[KeyRemap]")) {
|
||||
hasHodKeyRemap = true;
|
||||
}
|
||||
int eq = trimmed.indexOf('=');
|
||||
if (eq > 0) {
|
||||
String left = trimmed.substring(0, eq).trim();
|
||||
if (left.matches("^[A-Za-z][0-9]+$")) {
|
||||
hasHodHashKey = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHodKeyRemap || hasHodHashKey) {
|
||||
Map<String, String> hodMap = haus.nightmare.lib3270j.input.HodKeyMap.parseHodKmp(
|
||||
new StringReader(String.join("\n", rawLines))
|
||||
);
|
||||
if (hodMap != null && !hodMap.isEmpty()) {
|
||||
return hodMap;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
|
||||
|
||||
for (String raw : rawLines) {
|
||||
line = raw.trim();
|
||||
|
||||
// Strip comments
|
||||
int commentIdx = line.indexOf(';');
|
||||
@@ -262,6 +290,22 @@ public class KeyBindings {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export key bindings to a .kmp file using IBM HoD format.
|
||||
*/
|
||||
public static void exportKmp(File file, Map<String, String> keyBindings) throws IOException {
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
||||
exportKmp(bw, keyBindings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export key bindings to a Writer using IBM HoD format.
|
||||
*/
|
||||
public static void exportKmp(Writer writer, Map<String, String> keyBindings) throws IOException {
|
||||
haus.nightmare.lib3270j.input.HodKeyMap.exportHodKmp(writer, keyBindings);
|
||||
}
|
||||
|
||||
private static void parseBindStatement(String stmt, Map<String, List<String>> actionToBindings) {
|
||||
String[] parts = stmt.split("\\s+", 2);
|
||||
if (parts.length < 2) return;
|
||||
@@ -288,6 +332,19 @@ public class KeyBindings {
|
||||
}
|
||||
|
||||
private static void parseKeyAssignment(String left, String right, Map<String, List<String>> actionToBindings) {
|
||||
if (left.matches("^[A-Za-z][0-9]+$")) {
|
||||
haus.nightmare.lib3270j.input.HodKeyMap.KeyEntry entry =
|
||||
haus.nightmare.lib3270j.input.HodKeyMap.parseHashKey(left, right);
|
||||
if (entry != null) {
|
||||
String stroke = entry.toKeyStrokeString();
|
||||
String act = haus.nightmare.lib3270j.input.HodKeyMap.resolveMnemonicToAction(right);
|
||||
if (act != null && stroke != null && !stroke.isEmpty()) {
|
||||
addBinding(actionToBindings, act, stroke);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case A: left is KEY<n> or Modifier-KEY<n>, right is mnemonic
|
||||
String action = resolveMnemonic(right);
|
||||
String keyStroke = null;
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.Macro;
|
||||
import haus.nightmare.lib3270j.macro.MacroScreen;
|
||||
import haus.nightmare.lib3270j.macro.MacroScreens;
|
||||
import haus.nightmare.lib3270j.macro.MacroXmlParser;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroAction;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Interactive Swing dialog for loading, editing, running, and debugging
|
||||
* IBM Host On-Demand XML Macros (<HASCRIPT> / .mac files).
|
||||
*/
|
||||
public class MacroDialog extends JDialog implements Macro.MacroListener, Macro.MacroPromptHandler {
|
||||
|
||||
private final Telnet3270Client client;
|
||||
private final TerminalPanel terminalPanel;
|
||||
|
||||
private JTextArea xmlArea;
|
||||
private JTextArea logArea;
|
||||
private JLabel statusLabel;
|
||||
private JButton playBtn;
|
||||
private JButton pauseBtn;
|
||||
private JButton stopBtn;
|
||||
|
||||
private Macro macro;
|
||||
|
||||
public MacroDialog(Frame parent, Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
super(parent, "IBM HoD XML Macro Runtime (<HASCRIPT>)", false);
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
AppIcon.applyTo(this);
|
||||
|
||||
this.macro = new Macro(client);
|
||||
this.macro.addMacroListener(this);
|
||||
this.macro.setPromptHandler(this);
|
||||
|
||||
buildUI();
|
||||
setSize(780, 580);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
|
||||
mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14));
|
||||
|
||||
Font labelFont = ThemeManager.getUiFont();
|
||||
|
||||
// Top info label
|
||||
JPanel topPanel = new JPanel(new BorderLayout(5, 5));
|
||||
topPanel.setOpaque(false);
|
||||
JLabel descLabel = new JLabel("<html>Load, edit, and execute IBM Host On-Demand XML Macro files (<code>.mac</code> / <code><HASCRIPT></code>).<br>"
|
||||
+ "Automates host flows via screen descriptors, actions, variables, and state transitions.</html>");
|
||||
descLabel.setFont(labelFont);
|
||||
topPanel.add(descLabel, BorderLayout.CENTER);
|
||||
mainPanel.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Center split: XML editor and Execution Log
|
||||
xmlArea = new JTextArea();
|
||||
xmlArea.setFont(ThemeManager.getMonospacedUiFont());
|
||||
xmlArea.setLineWrap(false);
|
||||
xmlArea.setText(getDefaultMacroXml());
|
||||
ThemeManager.styleTextArea(xmlArea);
|
||||
|
||||
JScrollPane xmlScroll = new JScrollPane(xmlArea);
|
||||
ThemeManager.styleScrollPane(xmlScroll);
|
||||
xmlScroll.setBorder(ThemeManager.createTitledBorder("Macro XML (<HASCRIPT>)"));
|
||||
|
||||
logArea = new JTextArea();
|
||||
logArea.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -1));
|
||||
logArea.setEditable(false);
|
||||
logArea.setLineWrap(true);
|
||||
ThemeManager.styleTextArea(logArea);
|
||||
|
||||
JScrollPane logScroll = new JScrollPane(logArea);
|
||||
ThemeManager.styleScrollPane(logScroll);
|
||||
logScroll.setBorder(ThemeManager.createTitledBorder("Execution Log"));
|
||||
|
||||
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, xmlScroll, logScroll);
|
||||
splitPane.setResizeWeight(0.65);
|
||||
splitPane.setOpaque(false);
|
||||
mainPanel.add(splitPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom control panel
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||
bottomPanel.setOpaque(false);
|
||||
|
||||
statusLabel = new JLabel("Status: Stopped");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
statusLabel.setFont(labelFont);
|
||||
bottomPanel.add(statusLabel, BorderLayout.WEST);
|
||||
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
||||
buttonPanel.setOpaque(false);
|
||||
|
||||
JButton loadBtn = new JButton("Load .mac...");
|
||||
ThemeManager.styleButton(loadBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
loadBtn.setFont(labelFont);
|
||||
loadBtn.addActionListener(e -> loadMacroFile());
|
||||
|
||||
JButton saveBtn = new JButton("Save .mac...");
|
||||
ThemeManager.styleButton(saveBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
saveBtn.setFont(labelFont);
|
||||
saveBtn.addActionListener(e -> saveMacroFile());
|
||||
|
||||
playBtn = new JButton("Play");
|
||||
ThemeManager.styleButton(playBtn, ThemeManager.ButtonVariant.PRIMARY);
|
||||
playBtn.setFont(labelFont);
|
||||
playBtn.addActionListener(e -> playMacro());
|
||||
|
||||
pauseBtn = new JButton("Pause");
|
||||
ThemeManager.styleButton(pauseBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
pauseBtn.setFont(labelFont);
|
||||
pauseBtn.setEnabled(false);
|
||||
pauseBtn.addActionListener(e -> togglePause());
|
||||
|
||||
stopBtn = new JButton("Stop");
|
||||
ThemeManager.styleButton(stopBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
stopBtn.setFont(labelFont);
|
||||
stopBtn.setEnabled(false);
|
||||
stopBtn.addActionListener(e -> stopMacro());
|
||||
|
||||
JButton clearLogBtn = new JButton("Clear Log");
|
||||
ThemeManager.styleButton(clearLogBtn, ThemeManager.ButtonVariant.DEFAULT);
|
||||
clearLogBtn.setFont(labelFont);
|
||||
clearLogBtn.addActionListener(e -> logArea.setText(""));
|
||||
|
||||
JButton closeBtn = new JButton("Close");
|
||||
ThemeManager.styleButton(closeBtn, ThemeManager.ButtonVariant.CANCEL);
|
||||
closeBtn.setFont(labelFont);
|
||||
closeBtn.addActionListener(e -> dispose());
|
||||
|
||||
buttonPanel.add(loadBtn);
|
||||
buttonPanel.add(saveBtn);
|
||||
buttonPanel.add(Box.createHorizontalStrut(8));
|
||||
buttonPanel.add(playBtn);
|
||||
buttonPanel.add(pauseBtn);
|
||||
buttonPanel.add(stopBtn);
|
||||
buttonPanel.add(clearLogBtn);
|
||||
buttonPanel.add(Box.createHorizontalStrut(8));
|
||||
buttonPanel.add(closeBtn);
|
||||
|
||||
bottomPanel.add(buttonPanel, BorderLayout.EAST);
|
||||
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(mainPanel);
|
||||
ThemeManager.applyThemeToWindow(this);
|
||||
}
|
||||
|
||||
private void loadMacroFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Open IBM HoD Macro");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
|
||||
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
xmlArea.setText(sb.toString());
|
||||
log("Loaded macro file: " + f.getName());
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to load macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveMacroFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Save IBM HoD Macro");
|
||||
chooser.setFileFilter(new FileNameExtensionFilter("IBM Macro (*.mac, *.xml)", "mac", "xml"));
|
||||
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
if (!f.getName().contains(".")) {
|
||||
f = new File(f.getParentFile(), f.getName() + ".mac");
|
||||
}
|
||||
try (Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8)) {
|
||||
w.write(xmlArea.getText());
|
||||
log("Saved macro file: " + f.getName());
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to save macro file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void playMacro() {
|
||||
String xml = xmlArea.getText();
|
||||
if (xml == null || xml.trim().isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "Macro XML is empty.", "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (client == null || !client.getConnectionState().isFullSession()) {
|
||||
int opt = JOptionPane.showConfirmDialog(this,
|
||||
"Session is not currently connected to a host.\nDo you want to run macro anyway (descriptors will wait for connection)?",
|
||||
"Not Connected", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||||
if (opt != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
MacroScreens screens = MacroXmlParser.parse(xml);
|
||||
macro.setScreens(screens);
|
||||
macro.setClient(client);
|
||||
|
||||
log("Starting macro: " + (screens.getName().isEmpty() ? "Untitled" : screens.getName()));
|
||||
macro.play();
|
||||
} catch (Exception ex) {
|
||||
log("Parse / Launch Error: " + ex.getMessage());
|
||||
JOptionPane.showMessageDialog(this, "Macro parsing error: " + ex.getMessage(), "Macro Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
public void togglePause() {
|
||||
if (macro.getState() == Macro.STATE_PLAYING) {
|
||||
macro.pause();
|
||||
pauseBtn.setText("Resume");
|
||||
log("Macro paused.");
|
||||
} else if (macro.getState() == Macro.STATE_PAUSED) {
|
||||
macro.resume();
|
||||
pauseBtn.setText("Pause");
|
||||
log("Macro resumed.");
|
||||
}
|
||||
}
|
||||
|
||||
public void stopMacro() {
|
||||
macro.stop();
|
||||
log("Macro stopped by user.");
|
||||
}
|
||||
|
||||
private void log(String msg) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
logArea.append(msg + "\n");
|
||||
logArea.setCaretPosition(logArea.getDocument().getLength());
|
||||
});
|
||||
}
|
||||
|
||||
// ========== MacroListener Callbacks ==========
|
||||
|
||||
@Override
|
||||
public void macroStateChanged(Macro macro, int oldState, int newState) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
switch (newState) {
|
||||
case Macro.STATE_PLAYING:
|
||||
statusLabel.setText("Status: Playing");
|
||||
statusLabel.setForeground(ThemeManager.getFgMain());
|
||||
playBtn.setEnabled(false);
|
||||
pauseBtn.setEnabled(true);
|
||||
pauseBtn.setText("Pause");
|
||||
stopBtn.setEnabled(true);
|
||||
break;
|
||||
case Macro.STATE_PAUSED:
|
||||
statusLabel.setText("Status: Paused");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
pauseBtn.setText("Resume");
|
||||
break;
|
||||
case Macro.STATE_ERROR:
|
||||
statusLabel.setText("Status: Error");
|
||||
statusLabel.setForeground(Color.RED);
|
||||
playBtn.setEnabled(true);
|
||||
pauseBtn.setEnabled(false);
|
||||
stopBtn.setEnabled(false);
|
||||
break;
|
||||
case Macro.STATE_STOPPED:
|
||||
default:
|
||||
statusLabel.setText("Status: Stopped");
|
||||
statusLabel.setForeground(ThemeManager.getFgMuted());
|
||||
playBtn.setEnabled(true);
|
||||
pauseBtn.setEnabled(false);
|
||||
pauseBtn.setText("Pause");
|
||||
stopBtn.setEnabled(false);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroScreenMatching(MacroScreen screen) {
|
||||
log("Matched screen: [" + screen.getName() + "]");
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroActionExecuting(MacroAction action) {
|
||||
log(" -> Executing: " + action.getClass().getSimpleName());
|
||||
if (terminalPanel != null) {
|
||||
terminalPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroMessage(String title, String message) {
|
||||
log("Message [" + title + "]: " + message);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(this, message, title.isEmpty() ? "Macro Message" : title, JOptionPane.INFORMATION_MESSAGE);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void macroError(String error) {
|
||||
log("ERROR: " + error);
|
||||
}
|
||||
|
||||
// ========== MacroPromptHandler Callback ==========
|
||||
|
||||
@Override
|
||||
public String prompt(MacroActionPrompt prompt) {
|
||||
AtomicReference<String> result = new AtomicReference<>(null);
|
||||
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
result.set(showPromptModal(prompt));
|
||||
} else {
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
result.set(showPromptModal(prompt));
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return result.get();
|
||||
}
|
||||
|
||||
private String showPromptModal(MacroActionPrompt prompt) {
|
||||
JPanel panel = new JPanel(new BorderLayout(5, 5));
|
||||
JLabel msgLabel = new JLabel(prompt.getDescription().isEmpty() ? "Enter parameter:" : prompt.getDescription());
|
||||
panel.add(msgLabel, BorderLayout.NORTH);
|
||||
|
||||
JTextField textField;
|
||||
if (prompt.isEncrypted()) {
|
||||
textField = new JPasswordField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
|
||||
} else {
|
||||
textField = new JTextField(prompt.getDefaultValue(), prompt.getLen() > 0 ? Math.min(prompt.getLen(), 25) : 15);
|
||||
}
|
||||
panel.add(textField, BorderLayout.CENTER);
|
||||
|
||||
int opt = JOptionPane.showConfirmDialog(this, panel,
|
||||
prompt.getTitle().isEmpty() ? "Macro Input Prompt" : prompt.getTitle(),
|
||||
JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
|
||||
|
||||
if (opt == JOptionPane.OK_OPTION) {
|
||||
if (textField instanceof JPasswordField) {
|
||||
return new String(((JPasswordField) textField).getPassword());
|
||||
}
|
||||
return textField.getText();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getDefaultMacroXml() {
|
||||
return "<HASCRIPT name=\"SampleLogon\" timeout=\"15000\" usevars=\"true\">\n"
|
||||
+ " <vars>\n"
|
||||
+ " <create name=\"$userId\" type=\"string\" value=\"USER01\" />\n"
|
||||
+ " <create name=\"$count\" type=\"integer\" value=\"0\" />\n"
|
||||
+ " </vars>\n\n"
|
||||
+ " <screen name=\"LogonScreen\" entryscreen=\"true\">\n"
|
||||
+ " <description>\n"
|
||||
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||
+ " </description>\n"
|
||||
+ " <actions>\n"
|
||||
+ " <input value=\"$userId + '[enter]'\" movecursor=\"true\" xlatehostkeys=\"true\" />\n"
|
||||
+ " </actions>\n"
|
||||
+ " <nextscreens timeout=\"10000\">\n"
|
||||
+ " <nextscreen name=\"ReadyScreen\" />\n"
|
||||
+ " </nextscreens>\n"
|
||||
+ " </screen>\n\n"
|
||||
+ " <screen name=\"ReadyScreen\" exitscreen=\"true\">\n"
|
||||
+ " <description>\n"
|
||||
+ " <oia status=\"NOTINHIBITED\" />\n"
|
||||
+ " </description>\n"
|
||||
+ " <actions>\n"
|
||||
+ " <message title=\"Success\" value=\"Logon macro completed successfully!\" />\n"
|
||||
+ " </actions>\n"
|
||||
+ " </screen>\n"
|
||||
+ "</HASCRIPT>";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.J3270App;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenHistoryListener;
|
||||
import haus.nightmare.lib3270j.screen.ScreenHistoryBuffer.ScreenSnapshot;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.event.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Screen History viewer dialog matching IBM Host On-Demand ScreenHistory UI parity.
|
||||
*
|
||||
* Provides:
|
||||
* - Interactive JSlider scrubber to review past 64 screen buffer frames
|
||||
* - Previous / Next navigation buttons
|
||||
* - Exact monospace terminal frame preview with selection capability
|
||||
* - Copy Screen, Copy Selection, and Copy as Table actions
|
||||
* - Live update synchronization as new host screens arrive
|
||||
*/
|
||||
public class ScreenHistoryDialog extends JDialog implements ScreenHistoryListener {
|
||||
|
||||
private final J3270App parentApp;
|
||||
private final Telnet3270Client client;
|
||||
private final ScreenHistoryBuffer historyBuffer;
|
||||
|
||||
private JSlider scrubSlider;
|
||||
private JButton btnPrev;
|
||||
private JButton btnNext;
|
||||
private JLabel infoLabel;
|
||||
private JTextArea previewArea;
|
||||
private JButton btnCopyScreen;
|
||||
private JButton btnCopySelection;
|
||||
private JButton btnCopyTable;
|
||||
private JButton btnClose;
|
||||
|
||||
private int currentIndex = -1; // 0-based index into historyBuffer
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public ScreenHistoryDialog(J3270App parent, Telnet3270Client client) {
|
||||
super(parent, "Screen History", false);
|
||||
this.parentApp = parent;
|
||||
this.client = client;
|
||||
this.historyBuffer = client != null ? client.getScreenHistoryBuffer() : null;
|
||||
|
||||
AppIcon.applyTo(this);
|
||||
initComponents();
|
||||
setupKeyNavigation();
|
||||
|
||||
if (historyBuffer != null) {
|
||||
historyBuffer.addListener(this);
|
||||
refreshHistoryView(historyBuffer.size() - 1);
|
||||
} else {
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
if (historyBuffer != null) {
|
||||
historyBuffer.removeListener(ScreenHistoryDialog.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setSize(780, 560);
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new BorderLayout(6, 6));
|
||||
JPanel contentPane = new JPanel(new BorderLayout(8, 8));
|
||||
contentPane.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
setContentPane(contentPane);
|
||||
|
||||
// Top Navigation Bar
|
||||
JPanel topPanel = new JPanel(new BorderLayout(8, 8));
|
||||
topPanel.setBorder(BorderFactory.createTitledBorder("History Scrub Navigation"));
|
||||
|
||||
JPanel sliderPanel = new JPanel(new BorderLayout(6, 6));
|
||||
btnPrev = new JButton("◄ Prev");
|
||||
ThemeManager.styleButton(btnPrev, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnPrev.addActionListener(e -> stepHistory(-1));
|
||||
|
||||
btnNext = new JButton("Next ►");
|
||||
ThemeManager.styleButton(btnNext, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnNext.addActionListener(e -> stepHistory(1));
|
||||
|
||||
scrubSlider = new JSlider(1, 1, 1);
|
||||
scrubSlider.setMajorTickSpacing(10);
|
||||
scrubSlider.setMinorTickSpacing(1);
|
||||
scrubSlider.setPaintTicks(true);
|
||||
scrubSlider.setSnapToTicks(true);
|
||||
scrubSlider.addChangeListener(e -> {
|
||||
if (!scrubSlider.getValueIsAdjusting()) {
|
||||
int selected = scrubSlider.getValue() - 1;
|
||||
displayScreen(selected);
|
||||
}
|
||||
});
|
||||
|
||||
sliderPanel.add(btnPrev, BorderLayout.WEST);
|
||||
sliderPanel.add(scrubSlider, BorderLayout.CENTER);
|
||||
sliderPanel.add(btnNext, BorderLayout.EAST);
|
||||
|
||||
infoLabel = new JLabel("Screen 0 of 0", SwingConstants.CENTER);
|
||||
infoLabel.setFont(infoLabel.getFont().deriveFont(Font.BOLD, 13f));
|
||||
topPanel.add(sliderPanel, BorderLayout.CENTER);
|
||||
topPanel.add(infoLabel, BorderLayout.SOUTH);
|
||||
|
||||
contentPane.add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// Center Terminal Preview Pane
|
||||
previewArea = new JTextArea();
|
||||
previewArea.setEditable(false);
|
||||
previewArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
|
||||
previewArea.setBackground(Color.BLACK);
|
||||
previewArea.setForeground(new Color(0, 255, 65)); // 3270 Green Phosphor
|
||||
previewArea.setCaretColor(Color.WHITE);
|
||||
previewArea.setLineWrap(false);
|
||||
previewArea.setMargin(new Insets(6, 6, 6, 6));
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(previewArea);
|
||||
scrollPane.setBorder(BorderFactory.createLineBorder(new Color(60, 60, 60)));
|
||||
contentPane.add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
// Bottom Action Bar
|
||||
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
|
||||
|
||||
btnCopyScreen = new JButton("Copy Screen");
|
||||
ThemeManager.styleButton(btnCopyScreen, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnCopyScreen.setToolTipText("Copy entire historical screen to clipboard");
|
||||
btnCopyScreen.addActionListener(e -> copyCurrentScreen());
|
||||
|
||||
btnCopySelection = new JButton("Copy Selection");
|
||||
ThemeManager.styleButton(btnCopySelection, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnCopySelection.setToolTipText("Copy selected text to clipboard");
|
||||
btnCopySelection.addActionListener(e -> copySelectedText());
|
||||
|
||||
btnCopyTable = new JButton("Copy as Table");
|
||||
ThemeManager.styleButton(btnCopyTable, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnCopyTable.setToolTipText("Copy screen text with trimmed trailing spaces per row");
|
||||
btnCopyTable.addActionListener(e -> copyAsTable());
|
||||
|
||||
btnClose = new JButton("Close");
|
||||
ThemeManager.styleButton(btnClose, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnClose.addActionListener(e -> setVisible(false));
|
||||
|
||||
bottomPanel.add(btnCopyScreen);
|
||||
bottomPanel.add(btnCopySelection);
|
||||
bottomPanel.add(btnCopyTable);
|
||||
bottomPanel.add(Box.createHorizontalStrut(12));
|
||||
bottomPanel.add(btnClose);
|
||||
|
||||
contentPane.add(bottomPanel, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
private void setupKeyNavigation() {
|
||||
// Keyboard shortcuts for rapid scrub navigation
|
||||
Action prevAction = new AbstractAction() {
|
||||
@Override public void actionPerformed(ActionEvent e) { stepHistory(-1); }
|
||||
};
|
||||
Action nextAction = new AbstractAction() {
|
||||
@Override public void actionPerformed(ActionEvent e) { stepHistory(1); }
|
||||
};
|
||||
Action homeAction = new AbstractAction() {
|
||||
@Override public void actionPerformed(ActionEvent e) { displayScreen(0); }
|
||||
};
|
||||
Action endAction = new AbstractAction() {
|
||||
@Override public void actionPerformed(ActionEvent e) {
|
||||
if (historyBuffer != null) displayScreen(historyBuffer.size() - 1);
|
||||
}
|
||||
};
|
||||
Action closeAction = new AbstractAction() {
|
||||
@Override public void actionPerformed(ActionEvent e) { setVisible(false); }
|
||||
};
|
||||
|
||||
JRootPane root = getRootPane();
|
||||
InputMap im = root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
|
||||
ActionMap am = root.getActionMap();
|
||||
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.ALT_DOWN_MASK), "prevScreen");
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0), "prevScreen");
|
||||
am.put("prevScreen", prevAction);
|
||||
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.ALT_DOWN_MASK), "nextScreen");
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0), "nextScreen");
|
||||
am.put("nextScreen", nextAction);
|
||||
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, InputEvent.ALT_DOWN_MASK), "homeScreen");
|
||||
am.put("homeScreen", homeAction);
|
||||
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_END, InputEvent.ALT_DOWN_MASK), "endScreen");
|
||||
am.put("endScreen", endAction);
|
||||
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeDialog");
|
||||
am.put("closeDialog", closeAction);
|
||||
}
|
||||
|
||||
private void stepHistory(int delta) {
|
||||
if (historyBuffer == null || historyBuffer.isEmpty()) return;
|
||||
int target = currentIndex + delta;
|
||||
target = Math.max(0, Math.min(target, historyBuffer.size() - 1));
|
||||
displayScreen(target);
|
||||
}
|
||||
|
||||
private void displayScreen(int index) {
|
||||
if (historyBuffer == null || historyBuffer.isEmpty()) {
|
||||
updateEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
int total = historyBuffer.size();
|
||||
index = Math.max(0, Math.min(index, total - 1));
|
||||
currentIndex = index;
|
||||
|
||||
ScreenSnapshot snap = historyBuffer.get(index);
|
||||
if (snap != null) {
|
||||
previewArea.setText(snap.getText());
|
||||
previewArea.setCaretPosition(0);
|
||||
|
||||
String timeStr = dateFormat.format(new Date(snap.getTimestamp()));
|
||||
infoLabel.setText("Screen " + (index + 1) + " of " + total + " (" + timeStr + ") — "
|
||||
+ snap.getRows() + "x" + snap.getCols() + ", Cursor: ["
|
||||
+ (snap.getCursorRow() + 1) + "," + (snap.getCursorCol() + 1) + "]");
|
||||
|
||||
scrubSlider.setValue(index + 1);
|
||||
}
|
||||
|
||||
btnPrev.setEnabled(index > 0);
|
||||
btnNext.setEnabled(index < total - 1);
|
||||
btnCopyScreen.setEnabled(true);
|
||||
btnCopySelection.setEnabled(true);
|
||||
btnCopyTable.setEnabled(true);
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
currentIndex = -1;
|
||||
previewArea.setText("No screen history captured yet.\n\nScreens are automatically recorded as the host session receives updates or unlocks the keyboard.");
|
||||
infoLabel.setText("Screen 0 of 0 — Empty History");
|
||||
scrubSlider.setEnabled(false);
|
||||
btnPrev.setEnabled(false);
|
||||
btnNext.setEnabled(false);
|
||||
btnCopyScreen.setEnabled(false);
|
||||
btnCopySelection.setEnabled(false);
|
||||
btnCopyTable.setEnabled(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh slider bounds and select target index.
|
||||
*/
|
||||
public synchronized void refreshHistoryView(int targetIndex) {
|
||||
if (historyBuffer == null || historyBuffer.isEmpty()) {
|
||||
updateEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
int total = historyBuffer.size();
|
||||
scrubSlider.setEnabled(true);
|
||||
scrubSlider.setMinimum(1);
|
||||
scrubSlider.setMaximum(total);
|
||||
|
||||
int select = targetIndex >= 0 ? Math.min(targetIndex, total - 1) : total - 1;
|
||||
displayScreen(select);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHistoryUpdated(ScreenHistoryBuffer buffer, ScreenSnapshot newSnapshot) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
boolean wasAtLatest = (currentIndex >= 0 && currentIndex == buffer.size() - 2);
|
||||
int newTotal = buffer.size();
|
||||
scrubSlider.setMaximum(newTotal);
|
||||
|
||||
if (wasAtLatest || currentIndex < 0) {
|
||||
// Keep tracked with latest frame
|
||||
displayScreen(newTotal - 1);
|
||||
} else {
|
||||
// Keep operator's active scrub position, update label
|
||||
displayScreen(currentIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void copyCurrentScreen() {
|
||||
if (historyBuffer == null || currentIndex < 0) return;
|
||||
ScreenSnapshot snap = historyBuffer.get(currentIndex);
|
||||
if (snap != null) {
|
||||
setClipboardText(snap.getText());
|
||||
}
|
||||
}
|
||||
|
||||
private void copySelectedText() {
|
||||
String selected = previewArea.getSelectedText();
|
||||
if (selected != null && !selected.isEmpty()) {
|
||||
setClipboardText(selected);
|
||||
} else {
|
||||
copyCurrentScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private void copyAsTable() {
|
||||
if (historyBuffer == null || currentIndex < 0) return;
|
||||
ScreenSnapshot snap = historyBuffer.get(currentIndex);
|
||||
if (snap != null) {
|
||||
setClipboardText(snap.getFormattedTable());
|
||||
}
|
||||
}
|
||||
|
||||
private void setClipboardText(String text) {
|
||||
try {
|
||||
Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
cb.setContents(new StringSelection(text), null);
|
||||
} catch (Exception e) {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,18 @@ public class SettingsDialog extends JDialog {
|
||||
private JCheckBox blockPasteCheck;
|
||||
private JCheckBox enablePasteFromExcelCheck;
|
||||
private JCheckBox pasteStopAtProtectedCheck;
|
||||
private JCheckBox pasteWordBreakCheck;
|
||||
private JCheckBox trimRectSizingHandlesCheck;
|
||||
private JCheckBox trimRectRemainAfterEditCheck;
|
||||
|
||||
// Phase 25: Hotspots & Mouse Wheel controls
|
||||
private JCheckBox hotspotsEnabledCheck;
|
||||
private JCheckBox hotspotsUrlCheck;
|
||||
private JCheckBox hotspotsPfCheck;
|
||||
private JCheckBox hotspotsMenuCheck;
|
||||
private JCheckBox mouseWheelEnabledCheck;
|
||||
private JTextField mouseWheelUpField;
|
||||
private JTextField mouseWheelDownField;
|
||||
|
||||
// Connection tab
|
||||
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
|
||||
@@ -338,7 +350,17 @@ public class SettingsDialog extends JDialog {
|
||||
JTabbedPane behaviorTabs = new JTabbedPane();
|
||||
ThemeManager.styleTabbedPane(behaviorTabs);
|
||||
|
||||
behaviorTabs.addTab("General", createGeneralBehaviorPanel());
|
||||
JPanel generalContent = createGeneralBehaviorPanel();
|
||||
JScrollPane generalScroll = new JScrollPane(generalContent);
|
||||
ThemeManager.styleScrollPane(generalScroll);
|
||||
generalScroll.setBorder(BorderFactory.createEmptyBorder());
|
||||
if (generalScroll.getViewport() != null) {
|
||||
generalScroll.getViewport().setBackground(generalContent.getBackground());
|
||||
}
|
||||
generalScroll.getVerticalScrollBar().setUnitIncrement(16);
|
||||
generalScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
|
||||
behaviorTabs.addTab("General", generalScroll);
|
||||
behaviorTabs.addTab("Keymap", createKeymapPanel());
|
||||
|
||||
panel.add(behaviorTabs, BorderLayout.CENTER);
|
||||
@@ -346,12 +368,13 @@ public class SettingsDialog extends JDialog {
|
||||
}
|
||||
|
||||
private JPanel createGeneralBehaviorPanel() {
|
||||
JPanel panel = new JPanel();
|
||||
ScrollablePanel panel = new ScrollablePanel();
|
||||
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
|
||||
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
// Group 1: Typing & Operational Modes
|
||||
JPanel typingGroup = new JPanel(new GridBagLayout());
|
||||
typingGroup.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
typingGroup.setBorder(BorderFactory.createTitledBorder("Typing & Operational Modes"));
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(4, 6, 4, 6);
|
||||
@@ -402,6 +425,7 @@ public class SettingsDialog extends JDialog {
|
||||
|
||||
// Group 2: Selection & Clipboard
|
||||
JPanel selGroup = new JPanel(new GridBagLayout());
|
||||
selGroup.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
selGroup.setBorder(BorderFactory.createTitledBorder("Selection & Clipboard"));
|
||||
GridBagConstraints gbcS = new GridBagConstraints();
|
||||
gbcS.insets = new Insets(4, 6, 4, 6);
|
||||
@@ -430,7 +454,106 @@ public class SettingsDialog extends JDialog {
|
||||
gbcS.gridy = 3;
|
||||
selGroup.add(pasteStopAtProtectedCheck, gbcS);
|
||||
|
||||
pasteWordBreakCheck = new JCheckBox("Paste without splitting words across fields", Settings.getPasteWordBreak());
|
||||
ThemeManager.styleCheckBox(pasteWordBreakCheck);
|
||||
gbcS.gridy = 4;
|
||||
selGroup.add(pasteWordBreakCheck, gbcS);
|
||||
|
||||
trimRectSizingHandlesCheck = new JCheckBox("Trim rectangle sizing handles", Settings.getTrimRectSizingHandles());
|
||||
ThemeManager.styleCheckBox(trimRectSizingHandlesCheck);
|
||||
gbcS.gridy = 5;
|
||||
selGroup.add(trimRectSizingHandlesCheck, gbcS);
|
||||
|
||||
trimRectRemainAfterEditCheck = new JCheckBox("Trim rectangle remains after cut/copy/paste", Settings.getTrimRectRemainAfterEdit());
|
||||
ThemeManager.styleCheckBox(trimRectRemainAfterEditCheck);
|
||||
gbcS.gridy = 6;
|
||||
selGroup.add(trimRectRemainAfterEditCheck, gbcS);
|
||||
|
||||
panel.add(selGroup);
|
||||
panel.add(Box.createVerticalStrut(10));
|
||||
|
||||
// Group 3: Hotspots & Mouse Wheel Navigation (Phase 25)
|
||||
JPanel hotGroup = new JPanel(new GridBagLayout());
|
||||
hotGroup.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
hotGroup.setBorder(BorderFactory.createTitledBorder("Hotspots & Mouse Wheel Navigation"));
|
||||
GridBagConstraints gbcH = new GridBagConstraints();
|
||||
gbcH.insets = new Insets(3, 6, 3, 6);
|
||||
gbcH.anchor = GridBagConstraints.WEST;
|
||||
gbcH.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbcH.gridx = 0;
|
||||
gbcH.weightx = 1.0;
|
||||
|
||||
hotspotsEnabledCheck = new JCheckBox("Enable Interactive Hotspots (links, function keys, choices)", Settings.getHotspotsEnabled());
|
||||
ThemeManager.styleCheckBox(hotspotsEnabledCheck);
|
||||
gbcH.gridy = 0;
|
||||
hotGroup.add(hotspotsEnabledCheck, gbcH);
|
||||
|
||||
hotspotsUrlCheck = new JCheckBox("Auto-detect URLs (http://, https://, ftp://, www)", Settings.getHotspotsUrlEnabled());
|
||||
ThemeManager.styleCheckBox(hotspotsUrlCheck);
|
||||
gbcH.gridy = 1;
|
||||
gbcH.insets = new Insets(2, 24, 2, 6);
|
||||
hotGroup.add(hotspotsUrlCheck, gbcH);
|
||||
|
||||
hotspotsPfCheck = new JCheckBox("Auto-detect Function keys (PF1..PF24, F1..F24, PA1..PA3, [ENTER])", Settings.getHotspotsPfEnabled());
|
||||
ThemeManager.styleCheckBox(hotspotsPfCheck);
|
||||
gbcH.gridy = 2;
|
||||
hotGroup.add(hotspotsPfCheck, gbcH);
|
||||
|
||||
hotspotsMenuCheck = new JCheckBox("Auto-detect Menu choices (\"1.\", \"2.\", \"===> 3\")", Settings.getHotspotsMenuChoicesEnabled());
|
||||
ThemeManager.styleCheckBox(hotspotsMenuCheck);
|
||||
gbcH.gridy = 3;
|
||||
hotGroup.add(hotspotsMenuCheck, gbcH);
|
||||
|
||||
hotspotsEnabledCheck.addActionListener(e -> {
|
||||
boolean en = hotspotsEnabledCheck.isSelected();
|
||||
hotspotsUrlCheck.setEnabled(en);
|
||||
hotspotsPfCheck.setEnabled(en);
|
||||
hotspotsMenuCheck.setEnabled(en);
|
||||
});
|
||||
boolean initHotEn = hotspotsEnabledCheck.isSelected();
|
||||
hotspotsUrlCheck.setEnabled(initHotEn);
|
||||
hotspotsPfCheck.setEnabled(initHotEn);
|
||||
hotspotsMenuCheck.setEnabled(initHotEn);
|
||||
|
||||
mouseWheelEnabledCheck = new JCheckBox("Enable Mouse Wheel Navigation (scroll up/down)", Settings.getMouseWheelEnabled());
|
||||
ThemeManager.styleCheckBox(mouseWheelEnabledCheck);
|
||||
gbcH.gridy = 4;
|
||||
gbcH.insets = new Insets(6, 6, 3, 6);
|
||||
hotGroup.add(mouseWheelEnabledCheck, gbcH);
|
||||
|
||||
JPanel wheelKeysPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
|
||||
wheelKeysPanel.setOpaque(false);
|
||||
JLabel lblUp = new JLabel("Wheel Up:");
|
||||
mouseWheelUpField = new JTextField(Settings.getMouseWheelUpKey(), 6);
|
||||
ThemeManager.styleTextField(mouseWheelUpField);
|
||||
JLabel lblDown = new JLabel("Wheel Down:");
|
||||
mouseWheelDownField = new JTextField(Settings.getMouseWheelDownKey(), 6);
|
||||
ThemeManager.styleTextField(mouseWheelDownField);
|
||||
|
||||
wheelKeysPanel.add(lblUp);
|
||||
wheelKeysPanel.add(mouseWheelUpField);
|
||||
wheelKeysPanel.add(Box.createHorizontalStrut(12));
|
||||
wheelKeysPanel.add(lblDown);
|
||||
wheelKeysPanel.add(mouseWheelDownField);
|
||||
|
||||
mouseWheelEnabledCheck.addActionListener(e -> {
|
||||
boolean mwEn = mouseWheelEnabledCheck.isSelected();
|
||||
lblUp.setEnabled(mwEn);
|
||||
mouseWheelUpField.setEnabled(mwEn);
|
||||
lblDown.setEnabled(mwEn);
|
||||
mouseWheelDownField.setEnabled(mwEn);
|
||||
});
|
||||
boolean initMwEn = mouseWheelEnabledCheck.isSelected();
|
||||
lblUp.setEnabled(initMwEn);
|
||||
mouseWheelUpField.setEnabled(initMwEn);
|
||||
lblDown.setEnabled(initMwEn);
|
||||
mouseWheelDownField.setEnabled(initMwEn);
|
||||
|
||||
gbcH.gridy = 5;
|
||||
gbcH.insets = new Insets(2, 24, 2, 6);
|
||||
hotGroup.add(wheelKeysPanel, gbcH);
|
||||
|
||||
panel.add(hotGroup);
|
||||
panel.add(Box.createVerticalGlue());
|
||||
|
||||
return panel;
|
||||
@@ -676,14 +799,23 @@ public class SettingsDialog extends JDialog {
|
||||
monoPanel.add(p);
|
||||
}
|
||||
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(8, 0));
|
||||
JPanel bottomPanel = new JPanel();
|
||||
bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.Y_AXIS));
|
||||
bottomPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
|
||||
|
||||
fourColorOverrideCheck = new JCheckBox("Base 4-Color Override mode (3279 green/white/red/turquoise)", Settings.getFourColorOverride());
|
||||
fourColorOverrideCheck = new JCheckBox("Base 4 Override Mode (3279)", Settings.getFourColorOverride());
|
||||
fourColorOverrideCheck.setToolTipText("Base 4-Color Override mode (3279 green/white/red/turquoise)");
|
||||
ThemeManager.styleCheckBox(fourColorOverrideCheck);
|
||||
bottomPanel.add(fourColorOverrideCheck, BorderLayout.WEST);
|
||||
fourColorOverrideCheck.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
bottomPanel.add(fourColorOverrideCheck);
|
||||
|
||||
JButton btnResetColors = new JButton("Reset to Defaults");
|
||||
bottomPanel.add(Box.createVerticalStrut(4));
|
||||
|
||||
JPanel btnRow = new JPanel(new BorderLayout());
|
||||
btnRow.setOpaque(false);
|
||||
btnRow.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
JButton btnResetColors = new JButton("Restore Defaults");
|
||||
ThemeManager.styleButton(btnResetColors, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnResetColors.addActionListener(e -> {
|
||||
for (int i=0; i<16; i++) {
|
||||
@@ -701,7 +833,8 @@ public class SettingsDialog extends JDialog {
|
||||
}
|
||||
JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save.");
|
||||
});
|
||||
bottomPanel.add(btnResetColors, BorderLayout.EAST);
|
||||
btnRow.add(btnResetColors, BorderLayout.EAST);
|
||||
bottomPanel.add(btnRow);
|
||||
|
||||
main.add(hostPanel, BorderLayout.NORTH);
|
||||
main.add(monoPanel, BorderLayout.CENTER);
|
||||
@@ -870,7 +1003,7 @@ public class SettingsDialog extends JDialog {
|
||||
btnPanel.add(btnReset);
|
||||
|
||||
// Import Keymap — import from .kmp file
|
||||
JButton btnImportKmp = new JButton("Import Keymap...");
|
||||
JButton btnImportKmp = new JButton("Import .kmp...");
|
||||
ThemeManager.styleButton(btnImportKmp, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnImportKmp.addActionListener(e -> {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
@@ -908,6 +1041,36 @@ public class SettingsDialog extends JDialog {
|
||||
});
|
||||
btnPanel.add(btnImportKmp);
|
||||
|
||||
// Export Keymap — export to .kmp file
|
||||
JButton btnExportKmp = new JButton("Export .kmp...");
|
||||
ThemeManager.styleButton(btnExportKmp, ThemeManager.ButtonVariant.DEFAULT);
|
||||
btnExportKmp.addActionListener(e -> {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setDialogTitle("Export Keymap Profile");
|
||||
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("Keyboard Map Profile (*.kmp)", "kmp"));
|
||||
chooser.setSelectedFile(new File("User.kmp"));
|
||||
int ret = chooser.showSaveDialog(this);
|
||||
if (ret == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile();
|
||||
if (!f.getName().toLowerCase().endsWith(".kmp")) {
|
||||
f = new File(f.getParentFile(), f.getName() + ".kmp");
|
||||
}
|
||||
try {
|
||||
KeyBindings.exportKmp(f, tempKeyBindings);
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Successfully exported key bindings to " + f.getName(),
|
||||
"Keymap Exported",
|
||||
JOptionPane.INFORMATION_MESSAGE);
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Failed to export keymap: " + ex.getMessage(),
|
||||
"Export Error",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
btnPanel.add(btnExportKmp);
|
||||
|
||||
main.add(new JScrollPane(table), BorderLayout.CENTER);
|
||||
main.add(btnPanel, BorderLayout.SOUTH);
|
||||
|
||||
@@ -987,6 +1150,36 @@ public class SettingsDialog extends JDialog {
|
||||
if (pasteStopAtProtectedCheck != null) {
|
||||
Settings.setPasteStopAtProtectedLine(pasteStopAtProtectedCheck.isSelected());
|
||||
}
|
||||
if (pasteWordBreakCheck != null) {
|
||||
Settings.setPasteWordBreak(pasteWordBreakCheck.isSelected());
|
||||
}
|
||||
if (trimRectSizingHandlesCheck != null) {
|
||||
Settings.setTrimRectSizingHandles(trimRectSizingHandlesCheck.isSelected());
|
||||
}
|
||||
if (trimRectRemainAfterEditCheck != null) {
|
||||
Settings.setTrimRectRemainAfterEdit(trimRectRemainAfterEditCheck.isSelected());
|
||||
}
|
||||
if (hotspotsEnabledCheck != null) {
|
||||
Settings.setHotspotsEnabled(hotspotsEnabledCheck.isSelected());
|
||||
}
|
||||
if (hotspotsUrlCheck != null) {
|
||||
Settings.setHotspotsUrlEnabled(hotspotsUrlCheck.isSelected());
|
||||
}
|
||||
if (hotspotsPfCheck != null) {
|
||||
Settings.setHotspotsPfEnabled(hotspotsPfCheck.isSelected());
|
||||
}
|
||||
if (hotspotsMenuCheck != null) {
|
||||
Settings.setHotspotsMenuChoicesEnabled(hotspotsMenuCheck.isSelected());
|
||||
}
|
||||
if (mouseWheelEnabledCheck != null) {
|
||||
Settings.setMouseWheelEnabled(mouseWheelEnabledCheck.isSelected());
|
||||
}
|
||||
if (mouseWheelUpField != null) {
|
||||
Settings.setMouseWheelUpKey(mouseWheelUpField.getText().trim());
|
||||
}
|
||||
if (mouseWheelDownField != null) {
|
||||
Settings.setMouseWheelDownKey(mouseWheelDownField.getText().trim());
|
||||
}
|
||||
|
||||
// Apply Connection
|
||||
if (startupBehaviorBox != null) {
|
||||
@@ -1133,6 +1326,43 @@ public class SettingsDialog extends JDialog {
|
||||
if (pasteStopAtProtectedCheck != null) {
|
||||
pasteStopAtProtectedCheck.setSelected(Settings.getPasteStopAtProtectedLine());
|
||||
}
|
||||
if (pasteWordBreakCheck != null) {
|
||||
pasteWordBreakCheck.setSelected(Settings.getPasteWordBreak());
|
||||
}
|
||||
if (trimRectSizingHandlesCheck != null) {
|
||||
trimRectSizingHandlesCheck.setSelected(Settings.getTrimRectSizingHandles());
|
||||
}
|
||||
if (trimRectRemainAfterEditCheck != null) {
|
||||
trimRectRemainAfterEditCheck.setSelected(Settings.getTrimRectRemainAfterEdit());
|
||||
}
|
||||
if (hotspotsEnabledCheck != null) {
|
||||
hotspotsEnabledCheck.setSelected(Settings.getHotspotsEnabled());
|
||||
boolean en = Settings.getHotspotsEnabled();
|
||||
if (hotspotsUrlCheck != null) {
|
||||
hotspotsUrlCheck.setSelected(Settings.getHotspotsUrlEnabled());
|
||||
hotspotsUrlCheck.setEnabled(en);
|
||||
}
|
||||
if (hotspotsPfCheck != null) {
|
||||
hotspotsPfCheck.setSelected(Settings.getHotspotsPfEnabled());
|
||||
hotspotsPfCheck.setEnabled(en);
|
||||
}
|
||||
if (hotspotsMenuCheck != null) {
|
||||
hotspotsMenuCheck.setSelected(Settings.getHotspotsMenuChoicesEnabled());
|
||||
hotspotsMenuCheck.setEnabled(en);
|
||||
}
|
||||
}
|
||||
if (mouseWheelEnabledCheck != null) {
|
||||
boolean mwEn = Settings.getMouseWheelEnabled();
|
||||
mouseWheelEnabledCheck.setSelected(mwEn);
|
||||
if (mouseWheelUpField != null) {
|
||||
mouseWheelUpField.setText(Settings.getMouseWheelUpKey());
|
||||
mouseWheelUpField.setEnabled(mwEn);
|
||||
}
|
||||
if (mouseWheelDownField != null) {
|
||||
mouseWheelDownField.setText(Settings.getMouseWheelDownKey());
|
||||
mouseWheelDownField.setEnabled(mwEn);
|
||||
}
|
||||
}
|
||||
|
||||
// Connection
|
||||
if (startupBehaviorBox != null) {
|
||||
@@ -1302,4 +1532,39 @@ public class SettingsDialog extends JDialog {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ScrollablePanel extends JPanel implements Scrollable {
|
||||
public ScrollablePanel() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ScrollablePanel(LayoutManager layout) {
|
||||
super(layout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredScrollableViewportSize() {
|
||||
return getPreferredSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
|
||||
return 48;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getScrollableTracksViewportWidth() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getScrollableTracksViewportHeight() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public class StatusBar extends JPanel {
|
||||
|
||||
private Telnet3270Client client;
|
||||
private TerminalPanel terminalPanel;
|
||||
private haus.nightmare.lib3270j.ecl.ECLOIANotify oiaNotifyListener;
|
||||
|
||||
public StatusBar() {
|
||||
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||
@@ -131,8 +132,15 @@ public class StatusBar extends JPanel {
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
|
||||
if (this.client != null && this.client.getOIA() != null && this.oiaNotifyListener != null) {
|
||||
this.client.getOIA().unregisterOIAEvent(this.oiaNotifyListener);
|
||||
}
|
||||
this.client = client;
|
||||
this.terminalPanel = terminalPanel;
|
||||
if (this.client != null && this.client.getOIA() != null) {
|
||||
this.oiaNotifyListener = event -> updateStatus();
|
||||
this.client.getOIA().registerOIAEvent(this.oiaNotifyListener);
|
||||
}
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
@@ -388,4 +396,8 @@ public class StatusBar extends JPanel {
|
||||
cursorPosition.setText(String.format("%03d/%03d [%04d]", row, col, curAddr));
|
||||
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
|
||||
}
|
||||
|
||||
public String getLockStatusText() {
|
||||
return lockStatus != null ? lockStatus.getText() : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
// Font and cell dimensions
|
||||
private Font terminalFont;
|
||||
private Font boldTerminalFont;
|
||||
private int cellWidth;
|
||||
private int cellHeight;
|
||||
int cellWidth;
|
||||
int cellHeight;
|
||||
private int fontAscent;
|
||||
private int fontDescent;
|
||||
private int padding = 4;
|
||||
@@ -74,8 +74,18 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
private int selectionStartRow = -1, selectionStartCol = -1;
|
||||
private int selectionEndRow = -1, selectionEndCol = -1;
|
||||
private boolean isDragging = false;
|
||||
private boolean isActivationClick = false;
|
||||
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 102); // 40% alpha blend
|
||||
|
||||
public enum DragHandle {
|
||||
NONE, NW, N, NE, E, SE, S, SW, W, MOVE
|
||||
}
|
||||
private boolean trimRectSizingHandles = false;
|
||||
private DragHandle activeDragHandle = DragHandle.NONE;
|
||||
private int dragAnchorRow = -1;
|
||||
private int dragAnchorCol = -1;
|
||||
private int initialBoxR1 = -1, initialBoxR2 = -1, initialBoxC1 = -1, initialBoxC2 = -1;
|
||||
|
||||
// ========== Search Highlight state ==========
|
||||
private int searchHighlightAddr = -1;
|
||||
private int searchHighlightLen = 0;
|
||||
@@ -88,6 +98,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
private boolean resizeGuard = false;
|
||||
private StatusBar statusBar = null;
|
||||
|
||||
// ========== Phase 25: Hotspots & Mouse Wheel ==========
|
||||
private final HotspotEngine hotspotEngine = new HotspotEngine();
|
||||
private HotspotEngine.Hotspot hoveredHotspot = null;
|
||||
private boolean hotspotsEnabled = haus.nightmare.j3270.config.Settings.getHotspotsEnabled();
|
||||
private boolean mouseWheelEnabled = haus.nightmare.j3270.config.Settings.getMouseWheelEnabled();
|
||||
private String mouseWheelUpAction = haus.nightmare.j3270.config.Settings.getMouseWheelUpKey();
|
||||
private String mouseWheelDownAction = haus.nightmare.j3270.config.Settings.getMouseWheelDownKey();
|
||||
|
||||
// ========== Mode and Listener State ==========
|
||||
private boolean insertMode = false;
|
||||
|
||||
@@ -117,7 +135,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
/**
|
||||
* Compute the horizontal render offset to center the grid within the panel.
|
||||
*/
|
||||
private int getRenderOffsetX() {
|
||||
int getRenderOffsetX() {
|
||||
int termCols = 80;
|
||||
if (client != null) termCols = client.getScreenBuffer().getDisplayCols();
|
||||
int gridW = termCols * cellWidth;
|
||||
@@ -128,7 +146,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
/**
|
||||
* Compute the vertical render offset to center the grid within the panel.
|
||||
*/
|
||||
private int getRenderOffsetY() {
|
||||
int getRenderOffsetY() {
|
||||
int termRows = 24;
|
||||
if (client != null) termRows = client.getScreenBuffer().getDisplayRows();
|
||||
int gridH = termRows * cellHeight;
|
||||
@@ -178,6 +196,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
setupCursorBlink();
|
||||
|
||||
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
|
||||
trimRectSizingHandles = haus.nightmare.j3270.config.Settings.getTrimRectSizingHandles();
|
||||
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
|
||||
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
|
||||
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
|
||||
@@ -186,7 +205,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
MouseAdapter mouseHandler = new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
requestFocusInWindow();
|
||||
Window win = SwingUtilities.getWindowAncestor(TerminalPanel.this);
|
||||
boolean wasWindowInactive = (win != null && (!win.isActive() || !win.isFocused()));
|
||||
boolean wasPanelUnfocused = !hasFocus();
|
||||
isActivationClick = wasWindowInactive || wasPanelUnfocused;
|
||||
|
||||
if (wasWindowInactive) {
|
||||
win.toFront();
|
||||
win.requestFocus();
|
||||
}
|
||||
requestFocus();
|
||||
|
||||
// Right-click context menu
|
||||
if (SwingUtilities.isRightMouseButton(e)) {
|
||||
@@ -206,18 +234,30 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
row = Math.max(0, Math.min(row, displayRows - 1));
|
||||
|
||||
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive();
|
||||
boolean isSelectableField = false;
|
||||
if (sb.isFormatted()) {
|
||||
int faPos = sb.findFieldAttribute(row * displayCols + col);
|
||||
if (faPos >= 0) {
|
||||
int fa = sb.getCell(faPos).fa & 0xFF;
|
||||
isSelectableField = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
||||
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa);
|
||||
}
|
||||
}
|
||||
if (isGraphic || lightPenMode || isSelectableField) {
|
||||
if (isGraphic || lightPenMode) {
|
||||
clearSelection();
|
||||
} else if (SwingUtilities.isLeftMouseButton(e) && trimRectSizingHandles && hasSelection()) {
|
||||
DragHandle handle = getDragHandleAt(e.getX(), e.getY());
|
||||
if (handle != DragHandle.NONE) {
|
||||
activeDragHandle = handle;
|
||||
isDragging = true;
|
||||
dragAnchorRow = row;
|
||||
dragAnchorCol = col;
|
||||
initialBoxR1 = Math.min(selectionStartRow, selectionEndRow);
|
||||
initialBoxR2 = Math.max(selectionStartRow, selectionEndRow);
|
||||
initialBoxC1 = Math.min(selectionStartCol, selectionEndCol);
|
||||
initialBoxC2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
repaint();
|
||||
return;
|
||||
} else {
|
||||
activeDragHandle = DragHandle.NONE;
|
||||
selectionStartRow = row;
|
||||
selectionStartCol = col;
|
||||
selectionEndRow = row;
|
||||
selectionEndCol = col;
|
||||
}
|
||||
} else {
|
||||
activeDragHandle = DragHandle.NONE;
|
||||
selectionStartRow = row;
|
||||
selectionStartCol = col;
|
||||
selectionEndRow = row;
|
||||
@@ -251,8 +291,43 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
col = Math.max(0, Math.min(col, sb.getDisplayCols() - 1));
|
||||
row = Math.max(0, Math.min(row, sb.getDisplayRows() - 1));
|
||||
|
||||
selectionEndRow = row;
|
||||
selectionEndCol = col;
|
||||
if (activeDragHandle != DragHandle.NONE) {
|
||||
if (activeDragHandle == DragHandle.MOVE) {
|
||||
int deltaR = row - dragAnchorRow;
|
||||
int deltaC = col - dragAnchorCol;
|
||||
int h = initialBoxR2 - initialBoxR1;
|
||||
int w = initialBoxC2 - initialBoxC1;
|
||||
int newR1 = Math.max(0, Math.min(initialBoxR1 + deltaR, sb.getDisplayRows() - 1 - h));
|
||||
int newC1 = Math.max(0, Math.min(initialBoxC1 + deltaC, sb.getDisplayCols() - 1 - w));
|
||||
selectionStartRow = newR1;
|
||||
selectionStartCol = newC1;
|
||||
selectionEndRow = newR1 + h;
|
||||
selectionEndCol = newC1 + w;
|
||||
} else {
|
||||
int newR1 = initialBoxR1;
|
||||
int newR2 = initialBoxR2;
|
||||
int newC1 = initialBoxC1;
|
||||
int newC2 = initialBoxC2;
|
||||
switch (activeDragHandle) {
|
||||
case NW: newR1 = row; newC1 = col; break;
|
||||
case N: newR1 = row; break;
|
||||
case NE: newR1 = row; newC2 = col; break;
|
||||
case E: newC2 = col; break;
|
||||
case SE: newR2 = row; newC2 = col; break;
|
||||
case S: newR2 = row; break;
|
||||
case SW: newR2 = row; newC1 = col; break;
|
||||
case W: newC1 = col; break;
|
||||
default: break;
|
||||
}
|
||||
selectionStartRow = Math.min(newR1, newR2);
|
||||
selectionEndRow = Math.max(newR1, newR2);
|
||||
selectionStartCol = Math.min(newC1, newC2);
|
||||
selectionEndCol = Math.max(newC1, newC2);
|
||||
}
|
||||
} else {
|
||||
selectionEndRow = row;
|
||||
selectionEndCol = col;
|
||||
}
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
@@ -274,12 +349,72 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
py = Math.max(0, Math.min(py, gHeight - 1));
|
||||
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
||||
}
|
||||
|
||||
// Phase 26.1: Drag handle cursor hover detection
|
||||
if (trimRectSizingHandles && hasSelection() && client != null && client.getConnectionState().isFullSession()) {
|
||||
DragHandle handle = getDragHandleAt(e.getX(), e.getY());
|
||||
if (handle != DragHandle.NONE) {
|
||||
setCursor(getCursorForDragHandle(handle));
|
||||
setToolTipText(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 25.2: Real-time hotspot hover detection
|
||||
if (hotspotsEnabled && client != null && client.getScreenBuffer() != null) {
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int rows = sb.getDisplayRows();
|
||||
int cols = sb.getDisplayCols();
|
||||
int col = (e.getX() - ox) / cellWidth;
|
||||
int row = (e.getY() - oy) / cellHeight;
|
||||
if (row >= 0 && row < rows && col >= 0 && col < cols) {
|
||||
hotspotEngine.scan(sb);
|
||||
HotspotEngine.Hotspot h = hotspotEngine.findHotspot(row, col);
|
||||
if (h != null) {
|
||||
if (hoveredHotspot != h) {
|
||||
hoveredHotspot = h;
|
||||
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
setToolTipText(h.getTooltip());
|
||||
repaint();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hoveredHotspot != null) {
|
||||
hoveredHotspot = null;
|
||||
setCursor(Cursor.getDefaultCursor());
|
||||
setToolTipText(null);
|
||||
repaint();
|
||||
} else if (activeDragHandle == DragHandle.NONE) {
|
||||
setCursor(Cursor.getDefaultCursor());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
if (hoveredHotspot != null) {
|
||||
hoveredHotspot = null;
|
||||
setCursor(Cursor.getDefaultCursor());
|
||||
setToolTipText(null);
|
||||
repaint();
|
||||
} else if (activeDragHandle == DragHandle.NONE) {
|
||||
setCursor(Cursor.getDefaultCursor());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
if (activeDragHandle != DragHandle.NONE) {
|
||||
activeDragHandle = DragHandle.NONE;
|
||||
isActivationClick = false;
|
||||
repaint();
|
||||
return;
|
||||
}
|
||||
if (client != null && client.getConnectionState().isFullSession()) {
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int ox = getRenderOffsetX();
|
||||
@@ -292,7 +427,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
row = Math.max(0, Math.min(row, displayRows - 1));
|
||||
int clickAddr = row * displayCols + col;
|
||||
|
||||
boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null;
|
||||
boolean isGraphic = client.getGocaDecoder() != null &&
|
||||
(client.getGocaDecoder().isGraphicsCursorActive() ||
|
||||
(client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()));
|
||||
|
||||
if (isGraphic) {
|
||||
clearSelection();
|
||||
@@ -307,8 +444,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
py = Math.max(0, Math.min(py, gHeight - 1));
|
||||
client.getGocaDecoder().setGraphicCursorFromPixel(px, py);
|
||||
|
||||
int gx = client.getGocaDecoder().getGraphicCursorX();
|
||||
int gy = client.getGocaDecoder().getGraphicCursorY();
|
||||
int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1;
|
||||
client.getInputProcessor().sendGraphicMouseAid(
|
||||
gx, gy,
|
||||
haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER,
|
||||
button,
|
||||
e.isShiftDown(),
|
||||
@@ -318,25 +458,75 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
return;
|
||||
}
|
||||
|
||||
// Automatic Light-Pen / Selectable Field detection on mouse click:
|
||||
boolean wasDrag = (selectionStartRow != -1 && selectionEndRow != -1 &&
|
||||
!(selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol));
|
||||
boolean activation = isActivationClick;
|
||||
isActivationClick = false;
|
||||
|
||||
if (wasDrag) {
|
||||
// Preserve drag/block selection so user can interact with menus or press Ctrl+C
|
||||
repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
// Single click (not a drag):
|
||||
clearSelection();
|
||||
sb.setCursorAddress(clickAddr);
|
||||
|
||||
// If this was an activation click (e.g. clicking to resume focus),
|
||||
// do not fire field actions or send AIDs
|
||||
if (activation) {
|
||||
refreshScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 25.2: Interactive Hotspot click execution (URLs, PF keys, choices)
|
||||
if (hotspotsEnabled && client.getConnectionState().isFullSession()) {
|
||||
HotspotEngine.Hotspot h = hotspotEngine.findHotspot(row, col);
|
||||
if (h != null) {
|
||||
if (h.getType() == HotspotEngine.HotspotType.URL) {
|
||||
openUrl(h.getAction());
|
||||
return;
|
||||
} else {
|
||||
client.getPS().SendKeys(h.getAction());
|
||||
refreshScreen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Automatic Light-Pen / Selectable Field detection on mouse click (HoD parity):
|
||||
if (sb.isFormatted()) {
|
||||
int faPos = sb.findFieldAttribute(clickAddr);
|
||||
if (faPos >= 0) {
|
||||
int fa = sb.getCell(faPos).fa & 0xFF;
|
||||
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) &&
|
||||
!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
|
||||
clearSelection();
|
||||
if (client.lightPenSelect(clickAddr)) {
|
||||
refreshScreen();
|
||||
return;
|
||||
int size = displayRows * displayCols;
|
||||
int desPos = (faPos + 1) % size;
|
||||
char desAscii = (char) sb.getCell(desPos).ucs4;
|
||||
int desEbcdic = sb.getCell(desPos).ec & 0xFF;
|
||||
|
||||
boolean isToggleField = (desEbcdic == 0x6F || desAscii == '?' || desAscii == '\uFF1F' ||
|
||||
desEbcdic == 0x6E || desAscii == '>' || desAscii == '\uFF1E');
|
||||
boolean isEnterField = (desEbcdic == 0x50 || desAscii == '&' || desAscii == '\uFF06');
|
||||
boolean isProtected = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa);
|
||||
|
||||
boolean isDoubleClick = (e.getClickCount() >= 2);
|
||||
// For detectable fields (FA bits 4-5 = 01 or 11):
|
||||
// Double-clicking or invoking mnemonic [cursel] toggles the designator character (? -> >).
|
||||
// If field has immediate select attribute, immediately transmit Cursor Select AID (0x7E).
|
||||
if (lightPenMode || isDoubleClick || isToggleField || isEnterField || !isProtected) {
|
||||
if (client.lightPenSelect(clickAddr)) {
|
||||
refreshScreen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lightPenMode) {
|
||||
clearSelection();
|
||||
sb.setCursorAddress(clickAddr);
|
||||
boolean result = client.lightPenSelect(clickAddr);
|
||||
if (result) {
|
||||
refreshScreen();
|
||||
@@ -346,12 +536,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
|
||||
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
|
||||
sb.setCursorAddress(clickAddr);
|
||||
clearSelection();
|
||||
refreshScreen();
|
||||
return;
|
||||
}
|
||||
refreshScreen();
|
||||
return;
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
@@ -360,6 +546,21 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
addMouseListener(mouseHandler);
|
||||
addMouseMotionListener(mouseHandler);
|
||||
|
||||
// Phase 25.2: Mouse wheel navigation (Wheel Up -> [pf7], Wheel Down -> [pf8])
|
||||
addMouseWheelListener(e -> {
|
||||
if (!mouseWheelEnabled || client == null || !client.getConnectionState().isFullSession()) {
|
||||
return;
|
||||
}
|
||||
int rotation = e.getWheelRotation();
|
||||
if (rotation < 0) {
|
||||
client.getPS().SendKeys(mouseWheelUpAction);
|
||||
refreshScreen();
|
||||
} else if (rotation > 0) {
|
||||
client.getPS().SendKeys(mouseWheelDownAction);
|
||||
refreshScreen();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle resize: keep user configured font persistent without auto-fit reset
|
||||
addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
@@ -370,8 +571,104 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Phase 25: Hotspot URL Opener ==========
|
||||
|
||||
private void openUrl(String url) {
|
||||
if (url == null || url.trim().isEmpty()) return;
|
||||
try {
|
||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||
Desktop.getDesktop().browse(new java.net.URI(url));
|
||||
return;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
// Fallback: command line launcher
|
||||
try {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
if (os.contains("win")) {
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32", "url.dll,FileProtocolHandler", url});
|
||||
} else if (os.contains("mac")) {
|
||||
Runtime.getRuntime().exec(new String[]{"open", url});
|
||||
} else {
|
||||
Runtime.getRuntime().exec(new String[]{"xdg-open", url});
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
}
|
||||
}
|
||||
|
||||
public HotspotEngine getHotspotEngine() {
|
||||
return hotspotEngine;
|
||||
}
|
||||
|
||||
// ========== Selection / Copy-Paste ==========
|
||||
|
||||
public boolean isBlockSelectMode() {
|
||||
return blockSelectMode;
|
||||
}
|
||||
|
||||
public void setBlockSelectMode(boolean val) {
|
||||
this.blockSelectMode = val;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public boolean isTrimRectSizingHandles() {
|
||||
return trimRectSizingHandles;
|
||||
}
|
||||
|
||||
public void setTrimRectSizingHandles(boolean val) {
|
||||
this.trimRectSizingHandles = val;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public DragHandle getDragHandleAt(int mx, int my) {
|
||||
if (!hasSelection() || client == null) return DragHandle.NONE;
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
int r1 = Math.min(selectionStartRow, selectionEndRow);
|
||||
int r2 = Math.max(selectionStartRow, selectionEndRow);
|
||||
int c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
|
||||
int x1 = ox + c1 * cellWidth;
|
||||
int y1 = oy + r1 * cellHeight;
|
||||
int x2 = ox + (c2 + 1) * cellWidth;
|
||||
int y2 = oy + (r2 + 1) * cellHeight;
|
||||
int xm = (x1 + x2) / 2;
|
||||
int ym = (y1 + y2) / 2;
|
||||
|
||||
int tol = 6;
|
||||
if (Math.abs(mx - x1) <= tol && Math.abs(my - y1) <= tol) return DragHandle.NW;
|
||||
if (Math.abs(mx - xm) <= tol && Math.abs(my - y1) <= tol) return DragHandle.N;
|
||||
if (Math.abs(mx - x2) <= tol && Math.abs(my - y1) <= tol) return DragHandle.NE;
|
||||
if (Math.abs(mx - x2) <= tol && Math.abs(my - ym) <= tol) return DragHandle.E;
|
||||
if (Math.abs(mx - x2) <= tol && Math.abs(my - y2) <= tol) return DragHandle.SE;
|
||||
if (Math.abs(mx - xm) <= tol && Math.abs(my - y2) <= tol) return DragHandle.S;
|
||||
if (Math.abs(mx - x1) <= tol && Math.abs(my - y2) <= tol) return DragHandle.SW;
|
||||
if (Math.abs(mx - x1) <= tol && Math.abs(my - ym) <= tol) return DragHandle.W;
|
||||
|
||||
if (mx >= x1 && mx <= x2 && my >= y1 && my <= y2) {
|
||||
return DragHandle.MOVE;
|
||||
}
|
||||
|
||||
return DragHandle.NONE;
|
||||
}
|
||||
|
||||
public Cursor getCursorForDragHandle(DragHandle handle) {
|
||||
switch (handle) {
|
||||
case NW: return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR);
|
||||
case N: return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR);
|
||||
case NE: return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR);
|
||||
case E: return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
|
||||
case SE: return Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR);
|
||||
case S: return Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR);
|
||||
case SW: return Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR);
|
||||
case W: return Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR);
|
||||
case MOVE: return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
|
||||
default: return Cursor.getDefaultCursor();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearSelection() {
|
||||
selectionStartRow = -1;
|
||||
selectionStartCol = -1;
|
||||
@@ -417,23 +714,30 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
int r2 = Math.max(selectionStartRow, selectionEndRow);
|
||||
int c1, c2;
|
||||
|
||||
if (blockSelectMode) {
|
||||
if (blockSelectMode || trimRectSizingHandles) {
|
||||
c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||
c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int r = r1; r <= r2; r++) {
|
||||
if (r > r1) result.append('\n');
|
||||
StringBuilder line = new StringBuilder();
|
||||
for (int c = c1; c <= c2; c++) {
|
||||
int baddr = r * cols + c;
|
||||
ExtendedAttribute ea = sb.getCell(baddr);
|
||||
char ch = ea.ucs4;
|
||||
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
||||
result.append(' ');
|
||||
line.append(' ');
|
||||
} else {
|
||||
result.append(ch);
|
||||
line.append(ch);
|
||||
}
|
||||
}
|
||||
// Trim trailing whitespace on copied lines per HoD CutPasteOptions
|
||||
int end = line.length();
|
||||
while (end > 0 && line.charAt(end - 1) == ' ') {
|
||||
end--;
|
||||
}
|
||||
result.append(line.substring(0, end));
|
||||
}
|
||||
return result.toString();
|
||||
} else {
|
||||
@@ -448,21 +752,34 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
StringBuilder line = new StringBuilder();
|
||||
int prevRow = startAddr / cols;
|
||||
for (int addr = startAddr; addr <= endAddr; addr++) {
|
||||
int curRow = addr / cols;
|
||||
if (curRow != prevRow) {
|
||||
result.append('\n');
|
||||
int end = line.length();
|
||||
while (end > 0 && line.charAt(end - 1) == ' ') {
|
||||
end--;
|
||||
}
|
||||
if (result.length() > 0) result.append('\n');
|
||||
result.append(line.substring(0, end));
|
||||
line.setLength(0);
|
||||
prevRow = curRow;
|
||||
}
|
||||
ExtendedAttribute ea = sb.getCell(addr);
|
||||
char ch = ea.ucs4;
|
||||
if (ea.isFieldAttribute() || ch <= 0x20 || ch == 0xFF) {
|
||||
result.append(' ');
|
||||
line.append(' ');
|
||||
} else {
|
||||
result.append(ch);
|
||||
line.append(ch);
|
||||
}
|
||||
}
|
||||
int end = line.length();
|
||||
while (end > 0 && line.charAt(end - 1) == ' ') {
|
||||
end--;
|
||||
}
|
||||
if (result.length() > 0) result.append('\n');
|
||||
result.append(line.substring(0, end));
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -473,6 +790,65 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
StringSelection ss = new StringSelection(text);
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
|
||||
}
|
||||
if (!haus.nightmare.j3270.config.Settings.getTrimRectRemainAfterEdit()) {
|
||||
clearSelection();
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
public void cutSelection() {
|
||||
if (!hasSelection() || client == null || !client.getConnectionState().isFullSession()) return;
|
||||
String text = getSelectedText();
|
||||
if (!text.isEmpty()) {
|
||||
StringSelection ss = new StringSelection(text);
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
|
||||
}
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
int cols = sb.getCols();
|
||||
int r1 = Math.min(selectionStartRow, selectionEndRow);
|
||||
int r2 = Math.max(selectionStartRow, selectionEndRow);
|
||||
int c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
|
||||
if (blockSelectMode || trimRectSizingHandles) {
|
||||
for (int r = r1; r <= r2; r++) {
|
||||
for (int c = c1; c <= c2; c++) {
|
||||
int baddr = r * cols + c;
|
||||
if (!sb.isProtected(baddr) && !sb.getCell(baddr).isFieldAttribute()) {
|
||||
ExtendedAttribute ea = sb.getCell(baddr);
|
||||
ea.ucs4 = ' ';
|
||||
ea.ec = (byte) 0x40;
|
||||
if (sb.isFormatted()) {
|
||||
int faAddr = sb.findFieldAttribute(baddr);
|
||||
if (faAddr >= 0) {
|
||||
sb.getCell(faAddr).fa = (byte) (sb.getCell(faAddr).fa | haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int startAddr = Math.min(selectionStartRow * cols + selectionStartCol, selectionEndRow * cols + selectionEndCol);
|
||||
int endAddr = Math.max(selectionStartRow * cols + selectionStartCol, selectionEndRow * cols + selectionEndCol);
|
||||
for (int baddr = startAddr; baddr <= endAddr; baddr++) {
|
||||
if (!sb.isProtected(baddr) && !sb.getCell(baddr).isFieldAttribute()) {
|
||||
ExtendedAttribute ea = sb.getCell(baddr);
|
||||
ea.ucs4 = ' ';
|
||||
ea.ec = (byte) 0x40;
|
||||
if (sb.isFormatted()) {
|
||||
int faAddr = sb.findFieldAttribute(baddr);
|
||||
if (faAddr >= 0) {
|
||||
sb.getCell(faAddr).fa = (byte) (sb.getCell(faAddr).fa | haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.updateDisplaySnapshot();
|
||||
if (!haus.nightmare.j3270.config.Settings.getTrimRectRemainAfterEdit()) {
|
||||
clearSelection();
|
||||
}
|
||||
refreshScreen();
|
||||
}
|
||||
|
||||
public void pasteClipboard() {
|
||||
@@ -499,10 +875,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel();
|
||||
boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine();
|
||||
boolean blockPaste = haus.nightmare.j3270.config.Settings.getBlockPaste();
|
||||
boolean wordBreak = haus.nightmare.j3270.config.Settings.getPasteWordBreak();
|
||||
|
||||
if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || blockPaste || text.contains("\t") || text.contains("\n") || text.contains("\r"))) {
|
||||
client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected, blockPaste);
|
||||
if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || blockPaste || wordBreak || text.contains("\t") || text.contains("\n") || text.contains("\r"))) {
|
||||
client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected, blockPaste, wordBreak);
|
||||
} else if (client.getPS() != null) {
|
||||
client.getPS().setPasteWordBreak(wordBreak);
|
||||
if (blockPaste) {
|
||||
client.getPS().pasteRectangular(text, curRow, curCol);
|
||||
} else {
|
||||
@@ -518,6 +896,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!haus.nightmare.j3270.config.Settings.getTrimRectRemainAfterEdit() && hasSelection()) {
|
||||
clearSelection();
|
||||
}
|
||||
refreshScreen();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
@@ -555,6 +936,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
ThemeManager.stylePopupMenu(popup);
|
||||
|
||||
JMenuItem cutItem = new JMenuItem("Cut");
|
||||
ThemeManager.styleMenuItem(cutItem);
|
||||
cutItem.setEnabled(hasSelection() && client != null && client.getConnectionState().isFullSession());
|
||||
cutItem.addActionListener(ev -> cutSelection());
|
||||
popup.add(cutItem);
|
||||
|
||||
JMenuItem copyItem = new JMenuItem("Copy");
|
||||
ThemeManager.styleMenuItem(copyItem);
|
||||
copyItem.setEnabled(hasSelection());
|
||||
@@ -590,6 +977,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
});
|
||||
popup.add(blockPasteItem);
|
||||
|
||||
JCheckBoxMenuItem handlesItem = new JCheckBoxMenuItem("Trim Sizing Handles", trimRectSizingHandles);
|
||||
ThemeManager.styleMenuItem(handlesItem);
|
||||
handlesItem.addActionListener(ev -> {
|
||||
trimRectSizingHandles = handlesItem.isSelected();
|
||||
haus.nightmare.j3270.config.Settings.setTrimRectSizingHandles(trimRectSizingHandles);
|
||||
repaint();
|
||||
});
|
||||
popup.add(handlesItem);
|
||||
|
||||
popup.show(this, e.getX(), e.getY());
|
||||
}
|
||||
|
||||
@@ -599,7 +995,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
int r1 = Math.min(selectionStartRow, selectionEndRow);
|
||||
int r2 = Math.max(selectionStartRow, selectionEndRow);
|
||||
|
||||
if (blockSelectMode) {
|
||||
if (blockSelectMode || trimRectSizingHandles) {
|
||||
int c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
return row >= r1 && row <= r2 && col >= c1 && col <= c2;
|
||||
@@ -701,9 +1097,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
bindKeyToMap(im, "APL", haus.nightmare.j3270.config.Settings.getKeyBinding("APL", "alt F3"));
|
||||
|
||||
// Copy/Paste/Lightpen bindings
|
||||
String defCut = haus.nightmare.j3270.config.Settings.isMac() ? "meta X, ctrl X" : "ctrl X, shift DELETE";
|
||||
String defCopy = haus.nightmare.j3270.config.Settings.getDefaultBinding("COPY");
|
||||
String defPaste = haus.nightmare.j3270.config.Settings.getDefaultBinding("PASTE");
|
||||
String defSelectAll = haus.nightmare.j3270.config.Settings.getDefaultBinding("SELECTALL");
|
||||
bindKeyToMap(im, "CUT", haus.nightmare.j3270.config.Settings.getKeyBinding("CUT", defCut));
|
||||
bindKeyToMap(im, "COPY", haus.nightmare.j3270.config.Settings.getKeyBinding("COPY", defCopy));
|
||||
bindKeyToMap(im, "PASTE", haus.nightmare.j3270.config.Settings.getKeyBinding("PASTE", defPaste));
|
||||
bindKeyToMap(im, "SELECTALL", haus.nightmare.j3270.config.Settings.getKeyBinding("SELECTALL", defSelectAll));
|
||||
@@ -740,6 +1138,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
am.put("j3270-SYSREQ", createAction(this::handleSysReq));
|
||||
am.put("j3270-CURSEL", createAction(this::handleCursorSelect));
|
||||
|
||||
am.put("j3270-CUT", createAction(this::cutSelection));
|
||||
am.put("j3270-COPY", createAction(this::copySelection));
|
||||
am.put("j3270-PASTE", createAction(this::pasteClipboard));
|
||||
am.put("j3270-SELECTALL", createAction(this::selectAll));
|
||||
@@ -1255,11 +1654,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
setupFont();
|
||||
applyModeSettings();
|
||||
blockSelectMode = haus.nightmare.j3270.config.Settings.getBlockSelectMode();
|
||||
trimRectSizingHandles = haus.nightmare.j3270.config.Settings.getTrimRectSizingHandles();
|
||||
crosshairRulerEnabled = haus.nightmare.j3270.config.Settings.getCrosshairRuler();
|
||||
inputMaskEnabled = haus.nightmare.j3270.config.Settings.getInputMask();
|
||||
inputMaskChar = haus.nightmare.j3270.config.Settings.getInputMaskChar();
|
||||
String cStyle = haus.nightmare.j3270.config.Settings.getCursorStyle();
|
||||
cursorStyle = "UNDERLINE".equalsIgnoreCase(cStyle) ? CursorStyle.UNDERLINE : CursorStyle.BLOCK;
|
||||
|
||||
// Phase 25: Reload Hotspots and Mouse Wheel preferences
|
||||
hotspotsEnabled = haus.nightmare.j3270.config.Settings.getHotspotsEnabled();
|
||||
hotspotEngine.setUrlEnabled(haus.nightmare.j3270.config.Settings.getHotspotsUrlEnabled());
|
||||
hotspotEngine.setPfKeyEnabled(haus.nightmare.j3270.config.Settings.getHotspotsPfEnabled());
|
||||
hotspotEngine.setMenuChoiceEnabled(haus.nightmare.j3270.config.Settings.getHotspotsMenuChoicesEnabled());
|
||||
hotspotEngine.invalidateCache();
|
||||
mouseWheelEnabled = haus.nightmare.j3270.config.Settings.getMouseWheelEnabled();
|
||||
mouseWheelUpAction = haus.nightmare.j3270.config.Settings.getMouseWheelUpKey();
|
||||
mouseWheelDownAction = haus.nightmare.j3270.config.Settings.getMouseWheelDownKey();
|
||||
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
@@ -1309,7 +1720,41 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
blinkTimer = new Timer(500, e -> {
|
||||
cursorVisible = !cursorVisible;
|
||||
textBlinkVisible = !textBlinkVisible;
|
||||
repaint();
|
||||
|
||||
// Check if there are any blinking cells or fields on the screen
|
||||
boolean hasBlinkingCells = false;
|
||||
ScreenBuffer sb = (client != null) ? client.getScreenBuffer() : null;
|
||||
if (sb != null) {
|
||||
int total = sb.getDisplayRows() * sb.getDisplayCols();
|
||||
byte curFaGr = 0;
|
||||
for (int i = 0; i < total; i++) {
|
||||
ExtendedAttribute c = sb.getDisplayCell(i);
|
||||
if (c.isFieldAttribute()) {
|
||||
curFaGr = c.gr;
|
||||
}
|
||||
if ((c.gr & ExtendedAttribute.GR_BLINK) != 0 || (curFaGr & ExtendedAttribute.GR_BLINK) != 0) {
|
||||
hasBlinkingCells = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBlinkingCells) {
|
||||
repaint();
|
||||
} else if (sb != null && client != null && client.getConnectionState() != null && client.getConnectionState().isFullSession()) {
|
||||
// Only repaint the cursor cell bounding rect
|
||||
int cols = sb.getDisplayCols();
|
||||
int ox = getRenderOffsetX();
|
||||
int oy = getRenderOffsetY();
|
||||
int curAddr = sb.getDisplayCursorAddress();
|
||||
int curRow = curAddr / cols;
|
||||
int curCol = curAddr % cols;
|
||||
int cx = ox + curCol * cellWidth;
|
||||
int cy = oy + curRow * cellHeight;
|
||||
repaint(cx, cy, cellWidth, cellHeight);
|
||||
} else {
|
||||
repaint();
|
||||
}
|
||||
});
|
||||
blinkTimer.start();
|
||||
}
|
||||
@@ -1575,6 +2020,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
|
||||
// Field Outlining (0xC2 / TY_FOUTLINE)
|
||||
drawOutlining(g2, ea, currentFieldEa, baddr, row, col, x, y, cellWidth, cellHeight, rows, cols, sb, fgColor);
|
||||
|
||||
if (isCellSelected(row, col)) {
|
||||
g2.setColor(SELECTION_COLOR);
|
||||
g2.fillRect(x, y, cellWidth, cellHeight);
|
||||
@@ -1594,7 +2042,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
|
||||
// Check text blink (500ms cycle)
|
||||
boolean blink = (gr & GR_BLINK) != 0;
|
||||
boolean blink = (gr & GR_BLINK) != 0 || (currentFieldEa != null && (currentFieldEa.gr & GR_BLINK) != 0);
|
||||
boolean suppressGlyph = blink && !textBlinkVisible;
|
||||
|
||||
// Draw character or Programmed Symbol
|
||||
@@ -1628,12 +2076,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
|
||||
if (underline) {
|
||||
if (underline && !suppressGlyph) {
|
||||
g2.setColor(fgColor);
|
||||
int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2);
|
||||
g2.drawLine(x, ulY, x + cellWidth - 1, ulY);
|
||||
}
|
||||
|
||||
// Field Outlining (0xC2 / TY_FOUTLINE)
|
||||
drawOutlining(g2, ea, currentFieldEa, baddr, row, col, x, y, cellWidth, cellHeight, rows, cols, sb, fgColor);
|
||||
|
||||
// Draw search highlight
|
||||
if (isCellSearchHighlighted(row, col)) {
|
||||
g2.setColor(SEARCH_HIGHLIGHT_COLOR);
|
||||
@@ -1648,6 +2099,63 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 26.1: Rectangular Block Selection Sizing Handles & Box Outline
|
||||
if (trimRectSizingHandles && hasSelection() && client != null && client.getConnectionState().isFullSession()) {
|
||||
int r1 = Math.min(selectionStartRow, selectionEndRow);
|
||||
int r2 = Math.max(selectionStartRow, selectionEndRow);
|
||||
int c1 = Math.min(selectionStartCol, selectionEndCol);
|
||||
int c2 = Math.max(selectionStartCol, selectionEndCol);
|
||||
|
||||
int x1 = ox + c1 * cellWidth;
|
||||
int y1 = oy + r1 * cellHeight;
|
||||
int x2 = ox + (c2 + 1) * cellWidth;
|
||||
int y2 = oy + (r2 + 1) * cellHeight;
|
||||
int w = x2 - x1;
|
||||
int h = y2 - y1;
|
||||
int xm = (x1 + x2) / 2;
|
||||
int ym = (y1 + y2) / 2;
|
||||
|
||||
Color oldColor = g2.getColor();
|
||||
Stroke oldStroke = g2.getStroke();
|
||||
|
||||
g2.setColor(Color.YELLOW);
|
||||
g2.drawRect(x1, y1, w, h);
|
||||
|
||||
drawSizingHandle(g2, x1, y1);
|
||||
drawSizingHandle(g2, xm, y1);
|
||||
drawSizingHandle(g2, x2, y1);
|
||||
drawSizingHandle(g2, x2, ym);
|
||||
drawSizingHandle(g2, x2, y2);
|
||||
drawSizingHandle(g2, xm, y2);
|
||||
drawSizingHandle(g2, x1, y2);
|
||||
drawSizingHandle(g2, x1, ym);
|
||||
|
||||
g2.setStroke(oldStroke);
|
||||
g2.setColor(oldColor);
|
||||
}
|
||||
|
||||
// Phase 25.2: Draw visual underline on hovered hotspot
|
||||
if (hotspotsEnabled && hoveredHotspot != null) {
|
||||
Color oldColor = g2.getColor();
|
||||
Stroke oldStroke = g2.getStroke();
|
||||
g2.setColor(new Color(0, 220, 255)); // Bright cyan hotspot underline
|
||||
g2.setStroke(new BasicStroke(2.0f));
|
||||
|
||||
int hr0 = hoveredHotspot.getStartRow();
|
||||
int hr1 = hoveredHotspot.getEndRow();
|
||||
for (int r = hr0; r <= hr1; r++) {
|
||||
int c0 = (r == hr0) ? hoveredHotspot.getStartCol() : 0;
|
||||
int c1 = (r == hr1) ? hoveredHotspot.getEndCol() : cols - 1;
|
||||
int x1 = ox + c0 * cellWidth;
|
||||
int x2 = ox + (c1 + 1) * cellWidth;
|
||||
int ulY = oy + r * cellHeight + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2;
|
||||
ulY = Math.min(oy + (r + 1) * cellHeight - 1, ulY);
|
||||
g2.drawLine(x1, ulY, x2 - 1, ulY);
|
||||
}
|
||||
g2.setStroke(oldStroke);
|
||||
g2.setColor(oldColor);
|
||||
}
|
||||
|
||||
// Draw Graphic Cursor if active
|
||||
boolean isGraphicCursor = (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive())
|
||||
|| (client.getGraphicsPlane() != null && client.getGraphicsPlane().isGraphicCursorAttached());
|
||||
@@ -1801,6 +2309,64 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable {
|
||||
return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render field outline lines around cells based on ea.ol or field attribute outline.
|
||||
* Conforms to IBM 3270 Extended Data Stream / Host On-Demand DS3270 TY_FOUTLINE (0xC2).
|
||||
*
|
||||
* Bit 0 (0x01): Underline -> draw line at y + cellHeight - 1
|
||||
* Bit 1 (0x02): Right vertical line -> draw line at x + cellWidth - 1
|
||||
* Bit 2 (0x04): Overline -> draw line at y
|
||||
* Bit 3 (0x08): Left vertical line -> draw line at x
|
||||
*/
|
||||
private void drawOutlining(Graphics2D g2, ExtendedAttribute ea, ExtendedAttribute currentFieldEa,
|
||||
int baddr, int row, int col, int x, int y,
|
||||
int cellWidth, int cellHeight, int rows, int cols,
|
||||
ScreenBuffer sb, Color fgColor) {
|
||||
int outline = ea.ol & 0xFF;
|
||||
if (outline == 0 && currentFieldEa != null && currentFieldEa.ol != 0) {
|
||||
int fol = currentFieldEa.ol & 0xFF;
|
||||
if ((fol & ExtendedAttribute.OL_UNDERLINE) != 0) outline |= ExtendedAttribute.OL_UNDERLINE;
|
||||
if ((fol & ExtendedAttribute.OL_OVERLINE) != 0) outline |= ExtendedAttribute.OL_OVERLINE;
|
||||
|
||||
int size = rows * cols;
|
||||
int prevAddr = (baddr - 1 + size) % size;
|
||||
if (sb.getDisplayCell(prevAddr).isFieldAttribute() || col == 0) {
|
||||
if ((fol & ExtendedAttribute.OL_LEFT) != 0) outline |= ExtendedAttribute.OL_LEFT;
|
||||
}
|
||||
int nextAddr = (baddr + 1) % size;
|
||||
if (sb.getDisplayCell(nextAddr).isFieldAttribute() || col == cols - 1) {
|
||||
if ((fol & ExtendedAttribute.OL_RIGHT) != 0) outline |= ExtendedAttribute.OL_RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
if (outline != 0) {
|
||||
g2.setColor(fgColor);
|
||||
// Bit 0 (0x01): Underline -> draw line at y + cellHeight - 1
|
||||
if ((outline & ExtendedAttribute.OL_UNDERLINE) != 0) {
|
||||
g2.drawLine(x, y + cellHeight - 1, x + cellWidth - 1, y + cellHeight - 1);
|
||||
}
|
||||
// Bit 1 (0x02): Right vertical line -> draw line at x + cellWidth - 1
|
||||
if ((outline & ExtendedAttribute.OL_RIGHT) != 0) {
|
||||
g2.drawLine(x + cellWidth - 1, y, x + cellWidth - 1, y + cellHeight - 1);
|
||||
}
|
||||
// Bit 2 (0x04): Overline -> draw line at y
|
||||
if ((outline & ExtendedAttribute.OL_OVERLINE) != 0) {
|
||||
g2.drawLine(x, y, x + cellWidth - 1, y);
|
||||
}
|
||||
// Bit 3 (0x08): Left vertical line -> draw line at x
|
||||
if ((outline & ExtendedAttribute.OL_LEFT) != 0) {
|
||||
g2.drawLine(x, y, x, y + cellHeight - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSizingHandle(Graphics2D g2, int cx, int cy) {
|
||||
g2.setColor(Color.YELLOW);
|
||||
g2.fillRect(cx - 2, cy - 2, 5, 5);
|
||||
g2.setColor(Color.BLACK);
|
||||
g2.drawRect(cx - 2, cy - 2, 5, 5);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (blinkTimer != null)
|
||||
blinkTimer.stop();
|
||||
|
||||
@@ -304,4 +304,40 @@ public class HostStorageAndProxyTest {
|
||||
dialog.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SavedHost preserves all GraphicsMode settings across JSON serialization without clobbering to BOTH")
|
||||
public void testSavedHostGraphicsModeSerializationRoundtrip() {
|
||||
for (haus.nightmare.lib3270j.graphics.GraphicsMode gm : haus.nightmare.lib3270j.graphics.GraphicsMode.values()) {
|
||||
SavedHost host = new SavedHost(
|
||||
UUID.randomUUID().toString(),
|
||||
"Gfx " + gm.name(),
|
||||
"test.host.com",
|
||||
23,
|
||||
4,
|
||||
24,
|
||||
80,
|
||||
"",
|
||||
false,
|
||||
"TSO",
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
gm.name(),
|
||||
"037",
|
||||
"NONE",
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
""
|
||||
);
|
||||
String json = HostStorage.serializeHostsJson(List.of(host));
|
||||
List<SavedHost> parsed = HostStorage.parseHostsJson(json);
|
||||
assertEquals(1, parsed.size());
|
||||
assertEquals(gm.name(), parsed.get(0).getGraphicsMode(),
|
||||
"Graphics mode " + gm.name() + " must be preserved and not coerced to BOTH");
|
||||
assertEquals(gm, parsed.get(0).toConnectionConfig().getGraphicsMode(),
|
||||
"ConnectionConfig must reflect GraphicsMode." + gm.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for HotspotEngine (Phase 25.2).
|
||||
* Verifies regex detection of URLs, PF keys, Enter, and menu choices,
|
||||
* hit-testing, action strings, and word boundary isolation.
|
||||
*/
|
||||
public class HotspotEngineTest {
|
||||
|
||||
private HotspotEngine engine;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
engine = new HotspotEngine();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Detects HTTP, HTTPS, FTP, and WWW URLs with action strings")
|
||||
public void testUrlDetection() {
|
||||
String screen = "Visit https://ibm.com/support or http://example.org:8080/path?q=1 for help.\n" +
|
||||
"Download from ftp://ftp.archive.org/pub/files or browse www.google.com.";
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 2, 80);
|
||||
|
||||
assertEquals(4, hotspots.size());
|
||||
|
||||
HotspotEngine.Hotspot h0 = hotspots.get(0);
|
||||
assertEquals(HotspotEngine.HotspotType.URL, h0.getType());
|
||||
assertEquals("https://ibm.com/support", h0.getDisplayText());
|
||||
assertEquals("https://ibm.com/support", h0.getAction());
|
||||
assertEquals(0, h0.getStartRow());
|
||||
assertEquals(6, h0.getStartCol());
|
||||
assertTrue(h0.contains(0, 10));
|
||||
assertFalse(h0.contains(0, 5));
|
||||
|
||||
HotspotEngine.Hotspot h1 = hotspots.get(1);
|
||||
assertEquals("http://example.org:8080/path?q=1", h1.getDisplayText());
|
||||
assertEquals("http://example.org:8080/path?q=1", h1.getAction());
|
||||
|
||||
HotspotEngine.Hotspot h2 = hotspots.get(2);
|
||||
assertEquals("ftp://ftp.archive.org/pub/files", h2.getDisplayText());
|
||||
assertEquals("ftp://ftp.archive.org/pub/files", h2.getAction());
|
||||
|
||||
HotspotEngine.Hotspot h3 = hotspots.get(3);
|
||||
assertEquals("www.google.com", h3.getDisplayText());
|
||||
assertEquals("http://www.google.com", h3.getAction(), "www. URLs should have http:// prepended for action");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Detects PF1..PF24, F1..F24, PA1..PA3, and [ENTER] function keys")
|
||||
public void testFunctionKeyDetection() {
|
||||
String screen = "PF1=Help PF3=Exit PF7=Up PF8=Down PF12=Cancel\n" +
|
||||
"F1=Help F12=Cancel PA1=Abort [ENTER]=Continue";
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 2, 80);
|
||||
|
||||
assertEquals(9, hotspots.size());
|
||||
|
||||
// PF1
|
||||
assertEquals(HotspotEngine.HotspotType.PF_KEY, hotspots.get(0).getType());
|
||||
assertEquals("PF1", hotspots.get(0).getDisplayText());
|
||||
assertEquals("[pf1]", hotspots.get(0).getAction());
|
||||
|
||||
// PF3
|
||||
assertEquals("PF3", hotspots.get(1).getDisplayText());
|
||||
assertEquals("[pf3]", hotspots.get(1).getAction());
|
||||
|
||||
// PF7
|
||||
assertEquals("PF7", hotspots.get(2).getDisplayText());
|
||||
assertEquals("[pf7]", hotspots.get(2).getAction());
|
||||
|
||||
// PF8
|
||||
assertEquals("PF8", hotspots.get(3).getDisplayText());
|
||||
assertEquals("[pf8]", hotspots.get(3).getAction());
|
||||
|
||||
// PF12
|
||||
assertEquals("PF12", hotspots.get(4).getDisplayText());
|
||||
assertEquals("[pf12]", hotspots.get(4).getAction());
|
||||
|
||||
// F1 -> [pf1]
|
||||
assertEquals("F1", hotspots.get(5).getDisplayText());
|
||||
assertEquals("[pf1]", hotspots.get(5).getAction());
|
||||
|
||||
// F12 -> [pf12]
|
||||
assertEquals("F12", hotspots.get(6).getDisplayText());
|
||||
assertEquals("[pf12]", hotspots.get(6).getAction());
|
||||
|
||||
// PA1 -> [pa1]
|
||||
assertEquals(HotspotEngine.HotspotType.PF_KEY, hotspots.get(7).getType());
|
||||
assertEquals("PA1", hotspots.get(7).getDisplayText());
|
||||
assertEquals("[pa1]", hotspots.get(7).getAction());
|
||||
|
||||
// [ENTER] -> [enter]
|
||||
assertEquals(HotspotEngine.HotspotType.ENTER, hotspots.get(8).getType());
|
||||
assertEquals("[ENTER]", hotspots.get(8).getDisplayText());
|
||||
assertEquals("[enter]", hotspots.get(8).getAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Isolates function keys with strict word boundaries to avoid false positives")
|
||||
public void testWordBoundaryIsolation() {
|
||||
String screen = "STAFF1 MEMBER SPF12 FILE TRANSFER PROMPT";
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 1, 80);
|
||||
|
||||
// Neither STAFF1 nor SPF12 nor TRANSFER should be detected as PF1, F1, or F12
|
||||
assertEquals(0, hotspots.size(), "Embedded letters in words should not trigger PF key hotspots");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Detects numbered menu choices and arrow prompts")
|
||||
public void testMenuChoiceDetection() {
|
||||
String screen = " 1. User Settings\n" +
|
||||
" 2. Session Profiles\n" +
|
||||
"===> 3";
|
||||
List<HotspotEngine.Hotspot> hotspots = engine.scanText(screen, 3, 80);
|
||||
|
||||
assertEquals(3, hotspots.size());
|
||||
|
||||
HotspotEngine.Hotspot m1 = hotspots.get(0);
|
||||
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, m1.getType());
|
||||
assertEquals("1.", m1.getDisplayText());
|
||||
assertEquals("1[enter]", m1.getAction());
|
||||
assertEquals(0, m1.getStartRow());
|
||||
|
||||
HotspotEngine.Hotspot m2 = hotspots.get(1);
|
||||
assertEquals("2.", m2.getDisplayText());
|
||||
assertEquals("2[enter]", m2.getAction());
|
||||
assertEquals(1, m2.getStartRow());
|
||||
|
||||
HotspotEngine.Hotspot m3 = hotspots.get(2);
|
||||
assertEquals("===> 3", m3.getDisplayText());
|
||||
assertEquals("3[enter]", m3.getAction());
|
||||
assertEquals(2, m3.getStartRow());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Hit-testing finds hotspot under specific coordinate")
|
||||
public void testFindHotspot() {
|
||||
String screen = "PF3=Exit https://github.com ===> 1";
|
||||
engine.scanText(screen, 1, 80);
|
||||
|
||||
// Under PF3 (cols 0..2)
|
||||
HotspotEngine.Hotspot hPf = engine.findHotspot(0, 1);
|
||||
assertNotNull(hPf);
|
||||
assertEquals("PF3", hPf.getDisplayText());
|
||||
|
||||
// Space between hotspots
|
||||
assertNull(engine.findHotspot(0, 5));
|
||||
|
||||
// Under URL (cols 11..28)
|
||||
HotspotEngine.Hotspot hUrl = engine.findHotspot(0, 15);
|
||||
assertNotNull(hUrl);
|
||||
assertEquals("https://github.com", hUrl.getDisplayText());
|
||||
|
||||
// Under prompt (cols 32..37)
|
||||
HotspotEngine.Hotspot hPrompt = engine.findHotspot(0, 34);
|
||||
assertNotNull(hPrompt);
|
||||
assertEquals("===> 1", hPrompt.getDisplayText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Toggling engine options enables or disables specific hotspot categories")
|
||||
public void testTogglingCategories() {
|
||||
String screen = "PF3=Exit https://example.com 1. Options";
|
||||
|
||||
engine.scanText(screen, 1, 80);
|
||||
assertEquals(3, engine.getHotspots().size());
|
||||
|
||||
// Disable URLs
|
||||
engine.setUrlEnabled(false);
|
||||
List<HotspotEngine.Hotspot> noUrls = engine.scanText(screen, 1, 80);
|
||||
assertEquals(2, noUrls.size());
|
||||
assertFalse(noUrls.stream().anyMatch(h -> h.getType() == HotspotEngine.HotspotType.URL));
|
||||
|
||||
// Disable PF keys
|
||||
engine.setPfKeyEnabled(false);
|
||||
List<HotspotEngine.Hotspot> menuOnly = engine.scanText(screen, 1, 80);
|
||||
assertEquals(1, menuOnly.size());
|
||||
assertEquals(HotspotEngine.HotspotType.MENU_CHOICE, menuOnly.get(0).getType());
|
||||
|
||||
// Disable menu choices
|
||||
engine.setMenuChoiceEnabled(false);
|
||||
List<HotspotEngine.Hotspot> empty = engine.scanText(screen, 1, 80);
|
||||
assertEquals(0, empty.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.j3270.config.Settings;
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.Cursor;
|
||||
import java.awt.HeadlessException;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class SelectionHandlesAndTrimTest {
|
||||
|
||||
private boolean origRemainAfterEdit;
|
||||
private boolean origSizingHandles;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
origRemainAfterEdit = Settings.getTrimRectRemainAfterEdit();
|
||||
origSizingHandles = Settings.getTrimRectSizingHandles();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
Settings.setTrimRectRemainAfterEdit(origRemainAfterEdit);
|
||||
Settings.setTrimRectSizingHandles(origSizingHandles);
|
||||
}
|
||||
|
||||
private TerminalPanel createTestPanel() {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
panel.cellWidth = 10;
|
||||
panel.cellHeight = 20;
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
panel.setClient(client);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Trailing whitespace is trimmed on copied lines in block selection mode")
|
||||
public void testBlockSelectionTrailingWhitespaceTrim() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
ScreenBuffer sb = panel.getClient().getScreenBuffer();
|
||||
int cols = sb.getCols();
|
||||
|
||||
// Populate row 0: "HELLO "
|
||||
String text0 = "HELLO";
|
||||
for (int i = 0; i < text0.length(); i++) {
|
||||
sb.getCell(i).ucs4 = text0.charAt(i);
|
||||
}
|
||||
|
||||
// Populate row 1: "WORLD 123 "
|
||||
String text1 = "WORLD 123";
|
||||
for (int i = 0; i < text1.length(); i++) {
|
||||
sb.getCell(cols + i).ucs4 = text1.charAt(i);
|
||||
}
|
||||
|
||||
// Select rectangle from (row 0, col 0) to (row 1, col 20)
|
||||
panel.setBlockSelectMode(true);
|
||||
panel.setSelectionRange(0, cols + 20);
|
||||
|
||||
String copied = panel.getSelectedText();
|
||||
// Should be "HELLO\nWORLD 123" without the trailing spaces up to col 20
|
||||
assertEquals("HELLO\nWORLD 123", copied);
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Trailing whitespace is trimmed on copied lines in stream selection mode")
|
||||
public void testStreamSelectionTrailingWhitespaceTrim() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
ScreenBuffer sb = panel.getClient().getScreenBuffer();
|
||||
int cols = sb.getCols();
|
||||
|
||||
// Populate row 0: "LINE1 "
|
||||
String text0 = "LINE1";
|
||||
for (int i = 0; i < text0.length(); i++) {
|
||||
sb.getCell(i).ucs4 = text0.charAt(i);
|
||||
}
|
||||
|
||||
// Populate row 1: "LINE2"
|
||||
String text1 = "LINE2";
|
||||
for (int i = 0; i < text1.length(); i++) {
|
||||
sb.getCell(cols + i).ucs4 = text1.charAt(i);
|
||||
}
|
||||
|
||||
panel.setBlockSelectMode(false);
|
||||
panel.setTrimRectSizingHandles(false);
|
||||
// Select from row 0, col 0 to row 1, col 10
|
||||
panel.setSelectionRange(0, cols + 10);
|
||||
|
||||
String copied = panel.getSelectedText();
|
||||
assertEquals("LINE1\nLINE2", copied);
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Drag handle detection identifies 8 resize handles, MOVE inside box, and NONE outside")
|
||||
public void testDragHandleDetection() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
panel.setBlockSelectMode(true);
|
||||
panel.setTrimRectSizingHandles(true);
|
||||
|
||||
// Without selection, should return NONE
|
||||
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(100, 100));
|
||||
|
||||
// Select rectangle: rows 2 to 5, cols 10 to 20
|
||||
ScreenBuffer sb = panel.getClient().getScreenBuffer();
|
||||
int cols = sb.getCols();
|
||||
panel.setSelectionRange(2 * cols + 10, 5 * cols + 20);
|
||||
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int oy = panel.getRenderOffsetY();
|
||||
int cw = panel.cellWidth;
|
||||
int ch = panel.cellHeight;
|
||||
|
||||
int x1 = ox + 10 * cw;
|
||||
int y1 = oy + 2 * ch;
|
||||
int x2 = ox + 21 * cw;
|
||||
int y2 = oy + 6 * ch;
|
||||
int xm = (x1 + x2) / 2;
|
||||
int ym = (y1 + y2) / 2;
|
||||
|
||||
// Test 8 handles
|
||||
assertEquals(TerminalPanel.DragHandle.NW, panel.getDragHandleAt(x1, y1));
|
||||
assertEquals(TerminalPanel.DragHandle.N, panel.getDragHandleAt(xm, y1));
|
||||
assertEquals(TerminalPanel.DragHandle.NE, panel.getDragHandleAt(x2, y1));
|
||||
assertEquals(TerminalPanel.DragHandle.E, panel.getDragHandleAt(x2, ym));
|
||||
assertEquals(TerminalPanel.DragHandle.SE, panel.getDragHandleAt(x2, y2));
|
||||
assertEquals(TerminalPanel.DragHandle.S, panel.getDragHandleAt(xm, y2));
|
||||
assertEquals(TerminalPanel.DragHandle.SW, panel.getDragHandleAt(x1, y2));
|
||||
assertEquals(TerminalPanel.DragHandle.W, panel.getDragHandleAt(x1, ym));
|
||||
|
||||
// Test inside box -> MOVE
|
||||
assertEquals(TerminalPanel.DragHandle.MOVE, panel.getDragHandleAt(xm, ym));
|
||||
|
||||
// Test far outside -> NONE
|
||||
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(0, 0));
|
||||
assertEquals(TerminalPanel.DragHandle.NONE, panel.getDragHandleAt(x2 + 50, y2 + 50));
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Cursor mapping for all drag handles")
|
||||
public void testCursorForDragHandles() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
assertEquals(Cursor.NW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NW).getType());
|
||||
assertEquals(Cursor.N_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.N).getType());
|
||||
assertEquals(Cursor.NE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NE).getType());
|
||||
assertEquals(Cursor.E_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.E).getType());
|
||||
assertEquals(Cursor.SE_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SE).getType());
|
||||
assertEquals(Cursor.S_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.S).getType());
|
||||
assertEquals(Cursor.SW_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.SW).getType());
|
||||
assertEquals(Cursor.W_RESIZE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.W).getType());
|
||||
assertEquals(Cursor.MOVE_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.MOVE).getType());
|
||||
assertEquals(Cursor.DEFAULT_CURSOR, panel.getCursorForDragHandle(TerminalPanel.DragHandle.NONE).getType());
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("trimRectRemainAfterEdit controls whether selection is retained after copy")
|
||||
public void testTrimRectRemainAfterEdit() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
ScreenBuffer sb = panel.getClient().getScreenBuffer();
|
||||
|
||||
sb.getCell(0).ucs4 = 'A';
|
||||
panel.setSelectionRange(0, 5);
|
||||
assertTrue(panel.hasSelection());
|
||||
|
||||
// By default (false), copySelection() should clear the selection
|
||||
Settings.setTrimRectRemainAfterEdit(false);
|
||||
panel.copySelection();
|
||||
assertFalse(panel.hasSelection(), "Selection should be cleared after copy when remainAfterEdit is false");
|
||||
|
||||
// When set to true, copySelection() should preserve the selection
|
||||
Settings.setTrimRectRemainAfterEdit(true);
|
||||
panel.setSelectionRange(0, 5);
|
||||
assertTrue(panel.hasSelection());
|
||||
panel.copySelection();
|
||||
assertTrue(panel.hasSelection(), "Selection should remain active after copy when remainAfterEdit is true");
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("cutSelection clears unprotected cells, sets FA_MODIFY, and respects remainAfterEdit")
|
||||
public void testCutSelectionBehavior() {
|
||||
try {
|
||||
TerminalPanel panel = createTestPanel();
|
||||
ScreenBuffer sb = panel.getClient().getScreenBuffer();
|
||||
|
||||
// Field at 0: Unprotected field
|
||||
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
|
||||
String data = "TESTDATA";
|
||||
for (int i = 0; i < data.length(); i++) {
|
||||
sb.getCell(1 + i).ucs4 = data.charAt(i);
|
||||
}
|
||||
|
||||
// Select the field contents
|
||||
panel.setSelectionRange(1, 8);
|
||||
assertTrue(panel.hasSelection());
|
||||
|
||||
Settings.setTrimRectRemainAfterEdit(false);
|
||||
panel.cutSelection();
|
||||
|
||||
// Characters should be replaced with spaces
|
||||
for (int i = 1; i <= 8; i++) {
|
||||
assertEquals(' ', sb.getCell(i).ucs4, "Cell " + i + " should be erased to space");
|
||||
}
|
||||
// FA_MODIFY should remain or be set
|
||||
assertTrue((sb.getCell(0).ec & FA_MODIFY) != 0, "FA_MODIFY should be set on field attribute");
|
||||
assertFalse(panel.hasSelection(), "Selection should be cleared when remainAfterEdit is false");
|
||||
} catch (HeadlessException ignored) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("KeyBindings exportKmp generates HoD format and round-trips through parseKmp")
|
||||
public void testKeyBindingsExportAndParseRoundTrip() throws Exception {
|
||||
Map<String, String> keymap = new LinkedHashMap<>();
|
||||
keymap.put("ENTER", "ENTER");
|
||||
keymap.put("shift TAB", "TAB");
|
||||
keymap.put("CLEAR", "PAUSE");
|
||||
keymap.put("PF1", "F1");
|
||||
keymap.put("PF13", "shift F1");
|
||||
keymap.put("ESCAPE", "ESCAPE");
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
KeyBindings.exportKmp(writer, keymap);
|
||||
String exported = writer.toString();
|
||||
|
||||
assertTrue(exported.contains("[KeyRemap]"), "Exported KMP must contain [KeyRemap] section header");
|
||||
assertTrue(exported.contains("[enter]"), "Exported KMP must contain HoD mnemonics");
|
||||
assertTrue(exported.contains("[backtab]"), "Exported KMP must contain [backtab]");
|
||||
|
||||
// Roundtrip parse
|
||||
Map<String, String> parsed = KeyBindings.parseKmp(new StringReader(exported));
|
||||
assertNotNull(parsed);
|
||||
assertEquals("ENTER", parsed.get("ENTER"));
|
||||
assertEquals("TAB", parsed.get("shift TAB"));
|
||||
assertEquals("PAUSE", parsed.get("CLEAR"));
|
||||
assertEquals("F1", parsed.get("PF1"));
|
||||
assertEquals("shift F1", parsed.get("PF13"));
|
||||
assertEquals("ESCAPE", parsed.get("ESCAPE"));
|
||||
}
|
||||
}
|
||||
@@ -59,4 +59,49 @@ public class StatusBarTest {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStatusBarXSystemAndXProt() {
|
||||
try {
|
||||
ConnectionConfig config = new ConnectionConfig("mvs.example.com", 23, TerminalModel.IBM_3279_2, false);
|
||||
Telnet3270Client client = new Telnet3270Client(config);
|
||||
client.getTelnetFSM().setConnectionState(haus.nightmare.lib3270j.ConnectionState.CONNECTED_3270);
|
||||
|
||||
StatusBar statusBar = new StatusBar();
|
||||
statusBar.setClient(client, null);
|
||||
statusBar.updateStatus();
|
||||
|
||||
// Initially not locked
|
||||
assertEquals("", statusBar.getLockStatusText());
|
||||
|
||||
// 1. Setup protected field and type into it -> triggers X PROT
|
||||
client.getScreenBuffer().setFieldAttribute(0, (byte) haus.nightmare.lib3270j.protocol.DS3270Constants.FA_PROTECT);
|
||||
client.getScreenBuffer().setCursorAddress(1);
|
||||
client.typeCharacter('A');
|
||||
statusBar.updateStatus();
|
||||
assertEquals("X PROT", statusBar.getLockStatusText());
|
||||
|
||||
// 2. Reset -> clears X PROT
|
||||
client.reset();
|
||||
statusBar.updateStatus();
|
||||
assertEquals("", statusBar.getLockStatusText());
|
||||
|
||||
// 3. Send Enter -> triggers X SYSTEM
|
||||
client.sendEnter();
|
||||
statusBar.updateStatus();
|
||||
assertEquals("X SYSTEM", statusBar.getLockStatusText());
|
||||
|
||||
// 4. Host unlocks keyboard
|
||||
client.getInputProcessor().setKeyboardLocked(false);
|
||||
statusBar.updateStatus();
|
||||
assertEquals("", statusBar.getLockStatusText());
|
||||
|
||||
// 5. Subsequent Enter -> triggers X SYSTEM again
|
||||
client.sendEnter();
|
||||
statusBar.updateStatus();
|
||||
assertEquals("X SYSTEM", statusBar.getLockStatusText());
|
||||
} catch (HeadlessException e) {
|
||||
// Ignored in headless environments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package haus.nightmare.j3270.ui;
|
||||
|
||||
import haus.nightmare.lib3270j.ConnectionConfig;
|
||||
import haus.nightmare.lib3270j.ConnectionState;
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.HeadlessException;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class TerminalPanelSelectionTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Selection drag over protected/intensified fields is preserved on mouse release (Bugs 3 & 4)")
|
||||
public void testSelectionOverProtectedFieldsAndReleasePreservation() {
|
||||
try {
|
||||
TerminalPanel panel = new TerminalPanel();
|
||||
panel.setSize(800, 600);
|
||||
|
||||
Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23));
|
||||
client.getTelnetFSM().setConnectionState(ConnectionState.CONNECTED_3270);
|
||||
|
||||
ScreenBuffer sb = client.getScreenBuffer();
|
||||
sb.erase(false);
|
||||
|
||||
// Put a protected, intensified selectable field at pos 0 (ISPF header simulation)
|
||||
sb.setCellFA(0, (byte) (FA_PRINTABLE | FA_PROTECT | FA_INT_HIGH_SEL));
|
||||
String headerText = " ISPF PRIMARY OPTION MENU";
|
||||
for (int i = 0; i < headerText.length(); i++) {
|
||||
sb.getCell(1 + i).ucs4 = headerText.charAt(i);
|
||||
sb.getCell(1 + i).ec = (byte) (0x40 + i);
|
||||
}
|
||||
|
||||
panel.setClient(client);
|
||||
|
||||
// Cell dimensions in panel
|
||||
int cellW = panel.cellWidth > 0 ? panel.cellWidth : 10;
|
||||
int cellH = panel.cellHeight > 0 ? panel.cellHeight : 18;
|
||||
int ox = panel.getRenderOffsetX();
|
||||
int oy = panel.getRenderOffsetY();
|
||||
|
||||
MouseListener[] mouseListeners = panel.getMouseListeners();
|
||||
MouseMotionListener[] motionListeners = panel.getMouseMotionListeners();
|
||||
assertTrue(mouseListeners.length > 0);
|
||||
assertTrue(motionListeners.length > 0);
|
||||
|
||||
MouseListener ml = mouseListeners[0];
|
||||
MouseMotionListener mml = motionListeners[0];
|
||||
|
||||
// 1. Mouse Pressed at cell (row 0, col 2)
|
||||
int startX = ox + 2 * cellW + cellW / 2;
|
||||
int startY = oy + cellH / 2;
|
||||
MouseEvent pressEvent = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, startX, startY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mousePressed(pressEvent);
|
||||
|
||||
// 2. Drag to cell (row 0, col 15)
|
||||
int dragX = ox + 15 * cellW + cellW / 2;
|
||||
int dragY = oy + cellH / 2;
|
||||
MouseEvent dragEvent = new MouseEvent(panel, MouseEvent.MOUSE_DRAGGED, System.currentTimeMillis(),
|
||||
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
|
||||
mml.mouseDragged(dragEvent);
|
||||
|
||||
assertTrue(panel.hasSelection(), "Drag over protected field must create active selection (Bug 3)");
|
||||
|
||||
// 3. Mouse Released at cell (row 0, col 15)
|
||||
MouseEvent releaseEvent = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, dragX, dragY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mouseReleased(releaseEvent);
|
||||
|
||||
// Selection MUST NOT be dropped upon mouse release! (Bug 4)
|
||||
assertTrue(panel.hasSelection(), "Selection must remain active after mouse release (Bug 4)");
|
||||
String selected = panel.getSelectedText();
|
||||
assertNotNull(selected);
|
||||
assertTrue(selected.length() > 0, "Selected text must not be empty");
|
||||
|
||||
// 4. A subsequent single click without drag should clear the selection and move the cursor
|
||||
int clickX = ox + 5 * cellW + cellW / 2;
|
||||
int clickY = oy + cellH / 2;
|
||||
MouseEvent clickPress = new MouseEvent(panel, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mousePressed(clickPress);
|
||||
MouseEvent clickRelease = new MouseEvent(panel, MouseEvent.MOUSE_RELEASED, System.currentTimeMillis(),
|
||||
0, clickX, clickY, 1, false, MouseEvent.BUTTON1);
|
||||
ml.mouseReleased(clickRelease);
|
||||
|
||||
assertFalse(panel.hasSelection(), "Single click must clear previous selection");
|
||||
assertEquals(5, sb.getCursorAddress(), "Single click must position cursor at clicked cell");
|
||||
|
||||
} catch (HeadlessException e) {
|
||||
// Handled gracefully in headless environments
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLEvent;
|
||||
|
||||
/**
|
||||
* Communications event matching IBM Host On-Demand ECLCommEvent.
|
||||
*/
|
||||
public class ECLCommEvent extends ECLEvent {
|
||||
|
||||
private final boolean status;
|
||||
private final int eventType;
|
||||
|
||||
public ECLCommEvent(Object object, boolean status) {
|
||||
super(object);
|
||||
this.status = status;
|
||||
this.eventType = status ? 1 : 2;
|
||||
}
|
||||
|
||||
public ECLCommEvent(Object object, int eventType, boolean status) {
|
||||
super(object);
|
||||
this.eventType = eventType;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public boolean connected() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public int getEventType() {
|
||||
return this.eventType;
|
||||
}
|
||||
|
||||
public ECLConnection GetConnection() {
|
||||
Object src = GetSource();
|
||||
if (src instanceof ECLConnection) return (ECLConnection) src;
|
||||
if (src instanceof haus.nightmare.lib3270j.ecl.ECLConnection) {
|
||||
return new ECLConnection((haus.nightmare.lib3270j.ecl.ECLConnection) src);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Communications notification interface matching IBM Host On-Demand ECLCommNotify.
|
||||
*/
|
||||
public interface ECLCommNotify {
|
||||
void NotifyEvent(ECLConnection conn, boolean connected);
|
||||
default void NotifyStop(ECLConnection conn, int reason) {}
|
||||
default void NotifyError(ECLConnection conn, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Communications and connection management facade matching IBM Host On-Demand ECLConnection.
|
||||
*/
|
||||
public class ECLConnection extends haus.nightmare.lib3270j.ecl.ECLConnection implements ECLConstants {
|
||||
|
||||
private final List<ECLCommNotify> commNotifies = new CopyOnWriteArrayList<>();
|
||||
|
||||
public ECLConnection() {
|
||||
super();
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(Properties props) {
|
||||
super(props);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(String host, int port) {
|
||||
super(host, port);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLSession session) {
|
||||
super(session);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
public ECLConnection(haus.nightmare.lib3270j.ecl.ECLConnection delegate) {
|
||||
super(delegate != null ? delegate.getSession() : null);
|
||||
setupListener();
|
||||
}
|
||||
|
||||
private void setupListener() {
|
||||
RegisterCommEvent(new haus.nightmare.lib3270j.ecl.ECLCommListener() {
|
||||
@Override
|
||||
public void CommNotifyEvent(haus.nightmare.lib3270j.ecl.ECLCommEvent event) {
|
||||
boolean conn = event != null && event.isConnected();
|
||||
for (ECLCommNotify n : commNotifies) {
|
||||
try {
|
||||
n.NotifyEvent(ECLConnection.this, conn);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void RegisterCommEvent(ECLCommNotify notify) {
|
||||
if (notify != null && !commNotifies.contains(notify)) {
|
||||
commNotifies.add(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerCommEvent(ECLCommNotify notify) { RegisterCommEvent(notify); }
|
||||
|
||||
public void UnregisterCommEvent(ECLCommNotify notify) {
|
||||
if (notify != null) {
|
||||
commNotifies.remove(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterCommEvent(ECLCommNotify notify) { UnregisterCommEvent(notify); }
|
||||
|
||||
@Override
|
||||
public void StartCommunication() throws IOException {
|
||||
super.StartCommunication();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void StopCommunication() {
|
||||
super.StopCommunication();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* IBM Host Access Class Library (HACL) common constants.
|
||||
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLConstants.
|
||||
*/
|
||||
public interface ECLConstants extends haus.nightmare.lib3270j.ecl.ECLConstants {
|
||||
|
||||
// Presentation space logical planes (HoD bitmask values)
|
||||
int TEXT_PLANE = 1;
|
||||
int COLOR_PLANE = 2;
|
||||
int DBCS_PLANE = 4;
|
||||
int GRID_PLANE = 8;
|
||||
int FIELD_PLANE = 16;
|
||||
int EXFIELD_PLANE = 32;
|
||||
int NLS_PLANE = 64;
|
||||
int HOST_PLANE = 128;
|
||||
int RAWTEXT_PLANE = 256;
|
||||
int COLOR_ATTRIBUTES_PLANE = 512;
|
||||
int ALL_PLANES = 319;
|
||||
|
||||
// Search directions
|
||||
int SEARCH_FORWARD = 1;
|
||||
int SEARCH_BACKWARD = 2;
|
||||
|
||||
// Field filters
|
||||
int ALL_FIELDS = 0;
|
||||
int MODIFIED_FIELD = 1;
|
||||
int ALPHANUM_FIELD = 2;
|
||||
int HILIGHT_FIELD = 16;
|
||||
int PROTECTED_FIELD = 32;
|
||||
int DISPLAYABLE_FIELD = 64;
|
||||
int PENDETECTABLE_FIELD = 128;
|
||||
int UNMODIFIED_FIELD = 256;
|
||||
int NUMERIC_FIELD = 512;
|
||||
int LOLIGHT_FIELD = 4096;
|
||||
int UNPROTECTED_FIELD = 8192;
|
||||
int NON_DISPLAYABLE_FIELD = 16384;
|
||||
int NON_PENDETECTABLE_FIELD = 32768;
|
||||
|
||||
// Standard Mnemonics Constants
|
||||
String ENTER_STR = "[enter]";
|
||||
String CLEAR_STR = "[clear]";
|
||||
String RESET_STR = "[reset]";
|
||||
String TAB_STR = "[tab]";
|
||||
String FWDTAB_STR = "[tab]";
|
||||
String BACKTAB_STR = "[backtab]";
|
||||
String BACKSP_STR = "[backspace]";
|
||||
String DELCHAR_STR = "[delete]";
|
||||
String INSERT_STR = "[insert]";
|
||||
String HOME_STR = "[home]";
|
||||
String NEWLINE_STR = "[newline]";
|
||||
String ENDLINE_STR = "[eof]";
|
||||
String ERASEEOF_STR = "[eraseeof]";
|
||||
String ERASEFLD_STR = "[erasefld]";
|
||||
String ERASEINPUT_STR = "[erinp]";
|
||||
String ATTN_STR = "[attn]";
|
||||
String SYSREQ_STR = "[sysreq]";
|
||||
String DUP_STR = "[dup]";
|
||||
String FLDMRK_STR = "[fieldmark]";
|
||||
String PA1_STR = "[pa1]";
|
||||
String PA2_STR = "[pa2]";
|
||||
String PA3_STR = "[pa3]";
|
||||
String CURUP_STR = "[up]";
|
||||
String CURDOWN_STR = "[down]";
|
||||
String CURLEFT_STR = "[left]";
|
||||
String CURRIGHT_STR = "[right]";
|
||||
String PAGEUP_STR = "[pageup]";
|
||||
String PAGEDWN_STR = "[pagedn]";
|
||||
String COPY_STR = "[copy]";
|
||||
String PASTE_STR = "[paste]";
|
||||
String CUT_STR = "[cut]";
|
||||
String SELECTALL_STR = "[selectall]";
|
||||
|
||||
// Function keys F1 - F24
|
||||
String F1_STR = "[pf1]";
|
||||
String F2_STR = "[pf2]";
|
||||
String F3_STR = "[pf3]";
|
||||
String F4_STR = "[pf4]";
|
||||
String F5_STR = "[pf5]";
|
||||
String F6_STR = "[pf6]";
|
||||
String F7_STR = "[pf7]";
|
||||
String F8_STR = "[pf8]";
|
||||
String F9_STR = "[pf9]";
|
||||
String F10_STR = "[pf10]";
|
||||
String F11_STR = "[pf11]";
|
||||
String F12_STR = "[pf12]";
|
||||
String F13_STR = "[pf13]";
|
||||
String F14_STR = "[pf14]";
|
||||
String F15_STR = "[pf15]";
|
||||
String F16_STR = "[pf16]";
|
||||
String F17_STR = "[pf17]";
|
||||
String F18_STR = "[pf18]";
|
||||
String F19_STR = "[pf19]";
|
||||
String F20_STR = "[pf20]";
|
||||
String F21_STR = "[pf21]";
|
||||
String F22_STR = "[pf22]";
|
||||
String F23_STR = "[pf23]";
|
||||
String F24_STR = "[pf24]";
|
||||
|
||||
// Shift F1 - F24
|
||||
String SHIFT_F1_STR = "[Spf1]";
|
||||
String SHIFT_F2_STR = "[Spf2]";
|
||||
String SHIFT_F3_STR = "[Spf3]";
|
||||
String SHIFT_F4_STR = "[Spf4]";
|
||||
String SHIFT_F5_STR = "[Spf5]";
|
||||
String SHIFT_F6_STR = "[Spf6]";
|
||||
String SHIFT_F7_STR = "[Spf7]";
|
||||
String SHIFT_F8_STR = "[Spf8]";
|
||||
String SHIFT_F9_STR = "[Spf9]";
|
||||
String SHIFT_F10_STR = "[Spf10]";
|
||||
String SHIFT_F11_STR = "[Spf11]";
|
||||
String SHIFT_F12_STR = "[Spf12]";
|
||||
String SHIFT_F13_STR = "[Spf13]";
|
||||
String SHIFT_F14_STR = "[Spf14]";
|
||||
String SHIFT_F15_STR = "[Spf15]";
|
||||
String SHIFT_F16_STR = "[Spf16]";
|
||||
String SHIFT_F17_STR = "[Spf17]";
|
||||
String SHIFT_F18_STR = "[Spf18]";
|
||||
String SHIFT_F19_STR = "[Spf19]";
|
||||
String SHIFT_F20_STR = "[Spf20]";
|
||||
|
||||
// Field attributes
|
||||
short ATTR_PROTECTED = 32;
|
||||
short ATTR_ALPHANUM = 16;
|
||||
short ATTR_DISPLAY1 = 8;
|
||||
short ATTR_DISPLAY2 = 4;
|
||||
short ATTR_MDT = 1;
|
||||
|
||||
// Extended attributes
|
||||
short EXT_ATTR_3270_UNDERLINE = 192;
|
||||
short EXT_ATTR_3270_REVERSE = 128;
|
||||
short EXT_ATTR_3270_BLINK = 64;
|
||||
short EXT_ATTR_3270_EBCDIC = 0;
|
||||
|
||||
// Timeout
|
||||
int NO_TIMEOUT = -1;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* IBM Host Access Class Library (HACL) ECLErr checked exception facade.
|
||||
* Conforms 1:1 to IBM Host On-Demand com.ibm.eNetwork.ECL.ECLErr.
|
||||
*/
|
||||
public class ECLErr extends haus.nightmare.lib3270j.ecl.ECLErr {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLErr() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLErr(String text) {
|
||||
super(text);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum) {
|
||||
super(location, msgNum);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert) {
|
||||
super(location, msgNum, insert);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2) {
|
||||
super(location, msgNum, insert1, insert2);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) {
|
||||
super(location, msgNum, insert1, insert2, log);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) {
|
||||
super(location, msgNum, insert1, insert2, insert3);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, int reasonCode) {
|
||||
super(location, msgNum, reasonCode);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
|
||||
super(location, msgNum, insert1, reasonCode);
|
||||
}
|
||||
|
||||
public ECLErr(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ECLErr(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Discrete 3270 field facade matching IBM Host On-Demand ECLField.
|
||||
*/
|
||||
public class ECLField extends haus.nightmare.lib3270j.ecl.ECLField implements ECLConstants {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLField delegate;
|
||||
|
||||
public ECLField(haus.nightmare.lib3270j.ecl.ECLField delegate) {
|
||||
super(delegate != null ? delegate.getPS() : null,
|
||||
delegate != null ? delegate.getStart() : 0,
|
||||
delegate != null ? delegate.getDataStart() : 0,
|
||||
delegate != null ? delegate.getEnd() : 0,
|
||||
delegate != null ? delegate.getLength() : 0,
|
||||
(byte) (delegate != null ? delegate.getAttribute() : 0));
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public int GetStart() {
|
||||
return delegate != null ? delegate.GetStart() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getStart() { return GetStart(); }
|
||||
|
||||
public int GetStartRow() {
|
||||
return delegate != null ? delegate.GetStartRow() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getStartRow() { return GetStartRow(); }
|
||||
|
||||
public int GetStartCol() {
|
||||
return delegate != null ? delegate.GetStartCol() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getStartCol() { return GetStartCol(); }
|
||||
|
||||
public int GetEnd() {
|
||||
return delegate != null ? delegate.GetEnd() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getEnd() { return GetEnd(); }
|
||||
|
||||
public int GetEndRow() {
|
||||
return delegate != null ? delegate.GetEndRow() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getEndRow() { return GetEndRow(); }
|
||||
|
||||
public int GetEndCol() {
|
||||
return delegate != null ? delegate.GetEndCol() + 1 : 1;
|
||||
}
|
||||
|
||||
public int getEndCol() { return GetEndCol(); }
|
||||
|
||||
public int GetLength() {
|
||||
return delegate != null ? delegate.GetLength() : 0;
|
||||
}
|
||||
|
||||
public int getLength() { return GetLength(); }
|
||||
|
||||
public String GetString() {
|
||||
return delegate != null ? delegate.getString() : "";
|
||||
}
|
||||
|
||||
public String getString() { return GetString(); }
|
||||
|
||||
public int GetString(char[] buffer, int len) throws ECLErr {
|
||||
if (buffer == null) throw new ECLErr("ECLField:GetString", "ECL0011", "1");
|
||||
String text = GetString();
|
||||
int count = Math.min(len, Math.min(buffer.length, text.length()));
|
||||
for (int i = 0; i < count; i++) {
|
||||
buffer[i] = text.charAt(i);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public void SetString(String text) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
delegate.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetText(String text) throws ECLErr {
|
||||
SetString(text);
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
if (delegate != null) {
|
||||
delegate.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean IsModified() {
|
||||
return delegate != null && delegate.isModified();
|
||||
}
|
||||
|
||||
public boolean isModified() { return IsModified(); }
|
||||
|
||||
public boolean IsProtected() {
|
||||
return delegate != null && delegate.isProtected();
|
||||
}
|
||||
|
||||
public boolean isProtected() { return IsProtected(); }
|
||||
|
||||
public boolean IsNumeric() {
|
||||
return delegate != null && delegate.isNumeric();
|
||||
}
|
||||
|
||||
public boolean isNumeric() { return IsNumeric(); }
|
||||
|
||||
public boolean IsHighIntensity() {
|
||||
return delegate != null && delegate.isHighIntensity();
|
||||
}
|
||||
|
||||
public boolean isHighIntensity() { return IsHighIntensity(); }
|
||||
|
||||
public boolean IsPenDetectable() {
|
||||
return delegate != null && delegate.isPenDetectable();
|
||||
}
|
||||
|
||||
public boolean isPenDetectable() { return IsPenDetectable(); }
|
||||
|
||||
public boolean IsDisplay() {
|
||||
return delegate != null && delegate.isDisplay();
|
||||
}
|
||||
|
||||
public boolean isDisplay() { return IsDisplay(); }
|
||||
|
||||
public short GetAttribute() {
|
||||
return delegate != null ? (short) (delegate.getAttribute() & 0xFF) : 0;
|
||||
}
|
||||
|
||||
public short getAttribute() { return GetAttribute(); }
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLField getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Collection of 3270 fields matching IBM Host On-Demand ECLFieldList.
|
||||
*/
|
||||
public class ECLFieldList extends Vector<ECLField> implements ECLConstants {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLFieldList delegate;
|
||||
private final ECLPS ps;
|
||||
|
||||
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate, ECLPS ps) {
|
||||
this.delegate = delegate;
|
||||
this.ps = ps;
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public ECLFieldList(haus.nightmare.lib3270j.ecl.ECLFieldList delegate) {
|
||||
this(delegate, null);
|
||||
}
|
||||
|
||||
private void refreshFields() {
|
||||
clear();
|
||||
if (delegate != null) {
|
||||
delegate.refresh();
|
||||
for (haus.nightmare.lib3270j.ecl.ECLField f : delegate.getFields()) {
|
||||
add(new ECLField(f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh() {
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public void Refresh(int planes) throws ECLErr {
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
refreshFields();
|
||||
}
|
||||
|
||||
public int GetFieldCount() {
|
||||
return size();
|
||||
}
|
||||
|
||||
public int getFieldCount() { return GetFieldCount(); }
|
||||
|
||||
public ECLField GetFirstField() {
|
||||
return GetFirstField(0);
|
||||
}
|
||||
|
||||
public ECLField GetFirstField(int filter) {
|
||||
for (ECLField f : this) {
|
||||
if (matchesFilter(f, filter)) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLField GetNextField(ECLField current) {
|
||||
return GetNextField(current, 0);
|
||||
}
|
||||
|
||||
public ECLField GetNextField(ECLField current, int filter) {
|
||||
if (current == null) return GetFirstField(filter);
|
||||
int idx = indexOf(current);
|
||||
if (idx >= 0) {
|
||||
for (int i = idx + 1; i < size(); i++) {
|
||||
ECLField f = get(i);
|
||||
if (matchesFilter(f, filter)) return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLField FindField(int pos) throws ECLErr {
|
||||
if (ps != null && (pos < 1 || pos > ps.GetSize())) {
|
||||
throw new ECLErr("ECLFieldList:FindField", "ECL0010", "1", String.valueOf(pos));
|
||||
}
|
||||
for (ECLField f : this) {
|
||||
int start = f.GetStart();
|
||||
int end = f.GetEnd();
|
||||
if (start <= end) {
|
||||
if (pos >= start && pos <= end) return f;
|
||||
} else {
|
||||
// Wrapped field
|
||||
if (pos >= start || pos <= end) return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLField FindField(int row, int col) throws ECLErr {
|
||||
if (ps == null) return null;
|
||||
int pos = ps.ConvertRowColToPos(row, col);
|
||||
return FindField(pos);
|
||||
}
|
||||
|
||||
private boolean matchesFilter(ECLField f, int filter) {
|
||||
if (filter == 0 || filter == ALL_FIELDS) return true;
|
||||
if ((filter & MODIFIED_FIELD) != 0 && !f.IsModified()) return false;
|
||||
if ((filter & PROTECTED_FIELD) != 0 && !f.IsProtected()) return false;
|
||||
if ((filter & UNPROTECTED_FIELD) != 0 && f.IsProtected()) return false;
|
||||
if ((filter & NUMERIC_FIELD) != 0 && !f.IsNumeric()) return false;
|
||||
if ((filter & HILIGHT_FIELD) != 0 && !f.IsHighIntensity()) return false;
|
||||
if ((filter & DISPLAYABLE_FIELD) != 0 && !f.IsDisplay()) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Host Print Session facade matching IBM Host On-Demand ECLHostPrintSession.
|
||||
*/
|
||||
public class ECLHostPrintSession extends ECLSession {
|
||||
|
||||
public static final String SESSION_PRINT_SESSION_LABEL_DEFAULT = "";
|
||||
public static final String SESSION_PRINT_SESSION_TYPE_DEFAULT = "5";
|
||||
public static final String SESSION_PRINT_SESSION_HOST_DEFAULT = "";
|
||||
public static final String SESSION_PRINT_SESSION_HOST_PORT_DEFAULT = "23";
|
||||
public static final String SESSION_PRINT_SESSION_LU_NAME_DEFAULT = "";
|
||||
public static final String SESSION_PRINT_SESSION_AUTOCONNECT_DEFAULT = "true";
|
||||
public static final String SESSION_PRINT_SESSION_AUTORECONNECT_DEFAULT = "true";
|
||||
public static final String SESSION_LU_TYPE = "printerLUType";
|
||||
|
||||
public ECLHostPrintSession(Properties properties) throws ECLErr {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public ECLHostPrintSession() throws ECLErr {
|
||||
super();
|
||||
}
|
||||
|
||||
public void cancelPrintJob() {
|
||||
ECLPS ps = GetPS();
|
||||
if (ps != null) {
|
||||
try {
|
||||
ps.SendKeys("[cancelpjob]");
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelPrintJob() {
|
||||
cancelPrintJob();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Operator Information Area facade matching IBM Host On-Demand ECLOIA.
|
||||
*/
|
||||
public class ECLOIA implements ECLConstants {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLOIA delegate;
|
||||
private final List<ECLOIANotify> oiaNotifies = new CopyOnWriteArrayList<>();
|
||||
|
||||
public ECLOIA(haus.nightmare.lib3270j.ecl.ECLOIA delegate) {
|
||||
this.delegate = delegate;
|
||||
if (delegate != null) {
|
||||
delegate.RegisterOIAEvent(new haus.nightmare.lib3270j.ecl.ECLOIAListener() {
|
||||
@Override
|
||||
public void OIANotifyEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent event) {
|
||||
ECLOIAEvent facadeEvt = new ECLOIAEvent(event);
|
||||
for (ECLOIANotify n : oiaNotifies) {
|
||||
try {
|
||||
n.OIANotifyEvent(facadeEvt);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterOIAEvent(ECLOIANotify notify) {
|
||||
if (notify != null && !oiaNotifies.contains(notify)) {
|
||||
oiaNotifies.add(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerOIAEvent(ECLOIANotify notify) { RegisterOIAEvent(notify); }
|
||||
|
||||
public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
|
||||
if (delegate != null) {
|
||||
delegate.RegisterOIAEvent(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterOIAEvent(ECLOIANotify notify) {
|
||||
if (notify != null) {
|
||||
oiaNotifies.remove(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterOIAEvent(ECLOIANotify notify) { UnregisterOIAEvent(notify); }
|
||||
|
||||
public void UnregisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAListener listener) {
|
||||
if (delegate != null) {
|
||||
delegate.UnregisterOIAEvent(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean IsAlphanumeric() {
|
||||
return delegate != null && delegate.IsAlphanumeric();
|
||||
}
|
||||
|
||||
public boolean isAlphanumeric() { return IsAlphanumeric(); }
|
||||
|
||||
public boolean IsNumeric() {
|
||||
return delegate != null && delegate.IsNumeric();
|
||||
}
|
||||
|
||||
public boolean isNumeric() { return IsNumeric(); }
|
||||
|
||||
public boolean IsDBCS() {
|
||||
return delegate != null && delegate.IsDBCS();
|
||||
}
|
||||
|
||||
public boolean isDBCS() { return IsDBCS(); }
|
||||
|
||||
public boolean IsInsertMode() {
|
||||
return delegate != null && delegate.IsInsertMode();
|
||||
}
|
||||
|
||||
public boolean isInsertMode() { return IsInsertMode(); }
|
||||
|
||||
public boolean IsCommError() {
|
||||
return delegate != null && delegate.IsCommError();
|
||||
}
|
||||
|
||||
public boolean isCommError() { return IsCommError(); }
|
||||
|
||||
public boolean IsKeyLocked() {
|
||||
return delegate != null && delegate.isKeyLocked();
|
||||
}
|
||||
|
||||
public boolean isKeyLocked() { return IsKeyLocked(); }
|
||||
|
||||
public boolean IsMessageWaiting() {
|
||||
return delegate != null && delegate.IsMessageWaiting();
|
||||
}
|
||||
|
||||
public boolean isMessageWaiting() { return IsMessageWaiting(); }
|
||||
|
||||
public int GetInputInhibited() {
|
||||
return delegate != null ? delegate.getInputInhibited() : 0;
|
||||
}
|
||||
|
||||
public int getInputInhibited() { return GetInputInhibited(); }
|
||||
|
||||
public int GetAlphanumericType() {
|
||||
return delegate != null ? delegate.getAlphanumericType() : 0;
|
||||
}
|
||||
|
||||
public int getAlphanumericType() { return GetAlphanumericType(); }
|
||||
|
||||
public boolean WaitForInput(long timeout) {
|
||||
return delegate != null && delegate.WaitForInput(timeout);
|
||||
}
|
||||
|
||||
public boolean WaitForSystemAvailable(long timeout) {
|
||||
return delegate != null && delegate.WaitForSystemAvailable(timeout);
|
||||
}
|
||||
|
||||
public boolean WaitForAppAvailable(long timeout) {
|
||||
return delegate != null && delegate.WaitForAppAvailable(timeout);
|
||||
}
|
||||
|
||||
public boolean WaitForTransition(long timeout) {
|
||||
return delegate != null && delegate.WaitForTransition(timeout);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLOIA getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLEvent;
|
||||
|
||||
/**
|
||||
* Event object dispatched on Operator Information Area (ECLOIA) status changes matching IBM HoD.
|
||||
*/
|
||||
public class ECLOIAEvent extends ECLEvent {
|
||||
|
||||
public String data;
|
||||
public long newState;
|
||||
public long changedMask;
|
||||
|
||||
private int inputInhibited = 0;
|
||||
private int alphanumericType = 0;
|
||||
private boolean insertMode = false;
|
||||
|
||||
public ECLOIAEvent(Object source, long newState, long changedMask, String data) {
|
||||
super(source);
|
||||
this.newState = newState;
|
||||
this.changedMask = changedMask;
|
||||
this.data = data != null ? data : "";
|
||||
}
|
||||
|
||||
public ECLOIAEvent(Object source, int eventType, int inputInhibited, int alphanumericType,
|
||||
boolean insertMode, String statusString) {
|
||||
super(source);
|
||||
this.inputInhibited = inputInhibited;
|
||||
this.alphanumericType = alphanumericType;
|
||||
this.insertMode = insertMode;
|
||||
this.data = statusString != null ? statusString : "";
|
||||
this.newState = inputInhibited;
|
||||
this.changedMask = eventType;
|
||||
}
|
||||
|
||||
public ECLOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIAEvent delegate) {
|
||||
super(delegate != null ? delegate.getSource() : null);
|
||||
if (delegate != null) {
|
||||
this.inputInhibited = delegate.getInputInhibited();
|
||||
this.alphanumericType = delegate.getAlphanumericType();
|
||||
this.insertMode = delegate.isInsertMode();
|
||||
this.data = delegate.getStatusString();
|
||||
this.newState = delegate.getInputInhibited();
|
||||
this.changedMask = delegate.getEventType();
|
||||
}
|
||||
}
|
||||
|
||||
public ECLOIA GetOIA() {
|
||||
Object src = GetSource();
|
||||
if (src instanceof ECLOIA) {
|
||||
return (ECLOIA) src;
|
||||
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLOIA) {
|
||||
return new ECLOIA((haus.nightmare.lib3270j.ecl.ECLOIA) src);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECLOIA getOIA() { return GetOIA(); }
|
||||
|
||||
public int GetInputInhibited() { return inputInhibited; }
|
||||
public int getInputInhibited() { return inputInhibited; }
|
||||
|
||||
public int GetAlphanumericType() { return alphanumericType; }
|
||||
public int getAlphanumericType() { return alphanumericType; }
|
||||
|
||||
public boolean IsInsertMode() { return insertMode; }
|
||||
public boolean isInsertMode() { return insertMode; }
|
||||
|
||||
public boolean IsInputInhibited() { return inputInhibited != 0; }
|
||||
public boolean isInputInhibited() { return IsInputInhibited(); }
|
||||
|
||||
public String GetStatusString() { return data; }
|
||||
public String getStatusString() { return data; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ECLOIAEvent: changedMask=0x" + Long.toHexString(this.changedMask) +
|
||||
", newState=0x" + Long.toHexString(this.newState) + ", data=" + this.data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Operator Information Area notification interface matching IBM Host On-Demand ECLOIANotify.
|
||||
*/
|
||||
public interface ECLOIANotify {
|
||||
void NotifyEvent(ECLOIA oia, int inhibitReason, int state, String text);
|
||||
default void NotifyStop(ECLOIA oia, int reason) {}
|
||||
default void NotifyError(ECLOIA oia, ECLErr err) {}
|
||||
default void OIANotifyStop(ECLOIA oia, int reason) { NotifyStop(oia, reason); }
|
||||
default void OIANotifyError(ECLOIA oia, ECLErr err) { NotifyError(oia, err); }
|
||||
default void OIANotifyEvent(ECLOIAEvent event) {
|
||||
if (event != null) {
|
||||
NotifyEvent(event.GetOIA(), event.GetInputInhibited(), 0, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLPSListener;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Presentation Space facade matching IBM Host On-Demand ECLPS.
|
||||
* Provides 1-based indexing, method signature parity, and checked ECLErr exception semantics.
|
||||
*/
|
||||
public class ECLPS implements ECLConstants, Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLPS delegate;
|
||||
|
||||
public ECLPS(haus.nightmare.lib3270j.ecl.ECLPS delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public ECLPS(haus.nightmare.lib3270j.ecl.ECLSession session) {
|
||||
this(session != null ? session.GetPS() : null);
|
||||
}
|
||||
|
||||
public ECLPS(ECLSession session) {
|
||||
this(session != null ? session.getDelegate() : null);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLPS getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
// ========== Dimensions & Position Conversion (1-based) ==========
|
||||
|
||||
public int GetSize() {
|
||||
return delegate != null ? delegate.GetSize() : 0;
|
||||
}
|
||||
|
||||
public int getSize() { return GetSize(); }
|
||||
|
||||
public int GetRows() {
|
||||
return delegate != null ? delegate.GetRows() : 0;
|
||||
}
|
||||
|
||||
public int getRows() { return GetRows(); }
|
||||
|
||||
public int GetCols() {
|
||||
return delegate != null ? delegate.GetCols() : 0;
|
||||
}
|
||||
|
||||
public int getCols() { return GetCols(); }
|
||||
|
||||
public int GetSizeRows() { return GetRows(); }
|
||||
public int GetSizeCols() { return GetCols(); }
|
||||
|
||||
public int ConvertRowColToPos(int row, int col) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertRowColToPos(row, col);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int convertRowColToPos(int row, int col) throws ECLErr {
|
||||
return ConvertRowColToPos(row, col);
|
||||
}
|
||||
|
||||
public int ConvertPosToRow(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertPosToRow(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int convertPosToRow(int pos) throws ECLErr {
|
||||
return ConvertPosToRow(pos);
|
||||
}
|
||||
|
||||
public int ConvertPosToCol(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertPosToCol(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public int convertPosToCol(int pos) throws ECLErr {
|
||||
return ConvertPosToCol(pos);
|
||||
}
|
||||
|
||||
public void ConvertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.ConvertPosToRowCol(pos, rowCol);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void convertPosToRowCol(int pos, int[] rowCol) throws ECLErr {
|
||||
ConvertPosToRowCol(pos, rowCol);
|
||||
}
|
||||
|
||||
public int[] ConvertPosToRowCol(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.ConvertPosToRowCol(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return new int[]{1, 1};
|
||||
}
|
||||
|
||||
public int[] convertPosToRowCol(int pos) throws ECLErr {
|
||||
return ConvertPosToRowCol(pos);
|
||||
}
|
||||
|
||||
// ========== Cursor Operations (1-based) ==========
|
||||
|
||||
public int GetCursorPos() {
|
||||
return delegate != null ? delegate.GetCursorPos() : 1;
|
||||
}
|
||||
|
||||
public int getCursorPos() { return GetCursorPos(); }
|
||||
|
||||
public int GetCursorRow() {
|
||||
return delegate != null ? delegate.GetCursorRow() : 1;
|
||||
}
|
||||
|
||||
public int getCursorRow() { return GetCursorRow(); }
|
||||
|
||||
public int GetCursorCol() {
|
||||
return delegate != null ? delegate.GetCursorCol() : 1;
|
||||
}
|
||||
|
||||
public int getCursorCol() { return GetCursorCol(); }
|
||||
|
||||
public void SetCursorPos(int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.SetCursorPos(pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCursorPos(int pos) throws ECLErr {
|
||||
SetCursorPos(pos);
|
||||
}
|
||||
|
||||
public void SetCursorPos(int row, int col) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.SetCursorPos(row, col);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCursorPos(int row, int col) throws ECLErr {
|
||||
SetCursorPos(row, col);
|
||||
}
|
||||
|
||||
// ========== GetString & SetString Operations (1-based) ==========
|
||||
|
||||
public int GetString(char[] buffer, int len, int pos) throws ECLErr {
|
||||
return GetString(buffer, len, pos, len);
|
||||
}
|
||||
|
||||
public int GetString(char[] buffer, int len, int pos, int length) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.GetString(buffer, len, pos, length);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int GetString(char[] buffer, int len, int row, int col, int length) throws ECLErr {
|
||||
return GetString(buffer, len, ConvertRowColToPos(row, col), length);
|
||||
}
|
||||
|
||||
public String GetString(int pos, int length) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.GetString(pos, length);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getString(int pos, int length) throws ECLErr {
|
||||
return GetString(pos, length);
|
||||
}
|
||||
|
||||
public String GetString(int row, int col, int length) throws ECLErr {
|
||||
return GetString(ConvertRowColToPos(row, col), length);
|
||||
}
|
||||
|
||||
public String getString(int row, int col, int length) throws ECLErr {
|
||||
return GetString(row, col, length);
|
||||
}
|
||||
|
||||
public void SetString(String text, int pos) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
delegate.SetString(text, pos);
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setString(String text, int pos) throws ECLErr {
|
||||
SetString(text, pos);
|
||||
}
|
||||
|
||||
public void SetString(String text, int row, int col) throws ECLErr {
|
||||
SetString(text, ConvertRowColToPos(row, col));
|
||||
}
|
||||
|
||||
public void setString(String text, int row, int col) throws ECLErr {
|
||||
SetString(text, row, col);
|
||||
}
|
||||
|
||||
public void SetString(String text) throws ECLErr {
|
||||
SetString(text, GetCursorPos());
|
||||
}
|
||||
|
||||
public void setString(String text) throws ECLErr {
|
||||
SetString(text);
|
||||
}
|
||||
|
||||
public void SetText(String text, int pos) throws ECLErr {
|
||||
SetString(text, pos);
|
||||
}
|
||||
|
||||
public void SetText(String text, int row, int col) throws ECLErr {
|
||||
SetString(text, row, col);
|
||||
}
|
||||
|
||||
public void setText(String text, int pos) {
|
||||
if (delegate != null) delegate.setText(text, pos);
|
||||
}
|
||||
|
||||
public void setText(String text, int row, int col) {
|
||||
if (delegate != null) delegate.setText(text, row, col);
|
||||
}
|
||||
|
||||
// ========== Search Operations (1-based) ==========
|
||||
|
||||
public int SearchText(String text, int dir, int startPos) {
|
||||
return delegate != null ? delegate.SearchText(text, dir, startPos) : 0;
|
||||
}
|
||||
|
||||
public int SearchText(String text, int dir) {
|
||||
return delegate != null ? delegate.SearchText(text, dir) : 0;
|
||||
}
|
||||
|
||||
public int SearchText(String text) {
|
||||
return delegate != null ? delegate.SearchText(text) : 0;
|
||||
}
|
||||
|
||||
public int SearchText(String text, int row, int col, int dir) throws ECLErr {
|
||||
return SearchText(text, dir, ConvertRowColToPos(row, col));
|
||||
}
|
||||
|
||||
public int SearchString(String text, int dir, int startPos) {
|
||||
return SearchText(text, dir, startPos);
|
||||
}
|
||||
|
||||
public int SearchString(String text, int dir) {
|
||||
return SearchText(text, dir);
|
||||
}
|
||||
|
||||
public int SearchString(String text) {
|
||||
return SearchText(text);
|
||||
}
|
||||
|
||||
public int SearchString(String text, int row, int col, int dir) throws ECLErr {
|
||||
return SearchText(text, row, col, dir);
|
||||
}
|
||||
|
||||
public int SearchPS(String text) {
|
||||
return delegate != null ? delegate.SearchPS(text) : 0;
|
||||
}
|
||||
|
||||
public int SearchPS(String text, int startPos) {
|
||||
return delegate != null ? delegate.SearchPS(text, startPos) : 0;
|
||||
}
|
||||
|
||||
public int SearchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
|
||||
return delegate != null ? delegate.SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap) : 0;
|
||||
}
|
||||
|
||||
// ========== Keystrokes & Mnemonics ==========
|
||||
|
||||
public void SendKeys(String keys) {
|
||||
if (delegate != null) delegate.SendKeys(keys);
|
||||
}
|
||||
|
||||
public void sendKeys(String keys) { SendKeys(keys); }
|
||||
|
||||
public void SendKeys(String keys, int pos) {
|
||||
if (delegate != null) delegate.SendKeys(keys, pos);
|
||||
}
|
||||
|
||||
public void sendKeys(String keys, int pos) { SendKeys(keys, pos); }
|
||||
|
||||
public void SendKeys(String keys, int row, int col) {
|
||||
if (delegate != null) delegate.SendKeys(keys, row, col);
|
||||
}
|
||||
|
||||
public void sendKeys(String keys, int row, int col) { SendKeys(keys, row, col); }
|
||||
|
||||
public void SendCharacters(String keys) {
|
||||
if (delegate != null) delegate.SendCharacters(keys);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys) { SendCharacters(keys); }
|
||||
|
||||
public void SendCharacters(String keys, int delayMs) {
|
||||
if (delegate != null) delegate.SendCharacters(keys, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int delayMs) { SendCharacters(keys, delayMs); }
|
||||
|
||||
public void SendCharacters(String keys, int pos, int delayMs) {
|
||||
if (delegate != null) delegate.SendCharacters(keys, pos, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int pos, int delayMs) { SendCharacters(keys, pos, delayMs); }
|
||||
|
||||
public void SendCharacters(String keys, int row, int col, int delayMs) {
|
||||
if (delegate != null) delegate.SendCharacters(keys, row, col, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int row, int col, int delayMs) { SendCharacters(keys, row, col, delayMs); }
|
||||
|
||||
public String[] GetSendKeyMnemonics() {
|
||||
return delegate != null ? delegate.GetSendKeyMnemonics() : new String[0];
|
||||
}
|
||||
|
||||
public String[] getSendKeyMnemonics() { return GetSendKeyMnemonics(); }
|
||||
|
||||
// ========== Field Operations ==========
|
||||
|
||||
public ECLFieldList GetFieldList() {
|
||||
haus.nightmare.lib3270j.ecl.ECLFieldList fl = delegate != null ? delegate.GetFieldList() : null;
|
||||
return new ECLFieldList(fl, this);
|
||||
}
|
||||
|
||||
public ECLFieldList getFieldList() { return GetFieldList(); }
|
||||
|
||||
public ECLField GetField(int pos) throws ECLErr {
|
||||
return GetFieldList().FindField(pos);
|
||||
}
|
||||
|
||||
public ECLField getField(int pos) throws ECLErr { return GetField(pos); }
|
||||
|
||||
public ECLField GetField(int row, int col) throws ECLErr {
|
||||
return GetFieldList().FindField(row, col);
|
||||
}
|
||||
|
||||
public ECLField getField(int row, int col) throws ECLErr { return GetField(row, col); }
|
||||
|
||||
public ECLField GetFirstField() {
|
||||
return GetFieldList().GetFirstField();
|
||||
}
|
||||
|
||||
public ECLField getFirstField() { return GetFirstField(); }
|
||||
|
||||
public ECLField GetNextField(ECLField prev) {
|
||||
return GetFieldList().GetNextField(prev);
|
||||
}
|
||||
|
||||
public ECLField getNextField(ECLField prev) { return GetNextField(prev); }
|
||||
|
||||
// ========== Listener Overloads for HoD Interfaces ==========
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify) { RegisterPSEvent(notify); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify, int eventType) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify, eventType);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify, int eventType) { RegisterPSEvent(notify, eventType); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify, desc);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc) { RegisterPSEvent(notify, desc); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(notify, desc, eventType);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSNotify notify, ECLScreenDesc desc, int eventType) { RegisterPSEvent(notify, desc, eventType); }
|
||||
|
||||
public void UnregisterPSEvent(ECLPSNotify notify) {
|
||||
if (delegate != null) delegate.UnregisterPSEvent(notify);
|
||||
}
|
||||
|
||||
public void unregisterPSEvent(ECLPSNotify notify) { UnregisterPSEvent(notify); }
|
||||
|
||||
public void RegisterPSEvent(ECLPSListener listener) {
|
||||
if (delegate != null) delegate.RegisterPSEvent(listener);
|
||||
}
|
||||
|
||||
public void registerPSEvent(ECLPSListener listener) { RegisterPSEvent(listener); }
|
||||
|
||||
public void UnregisterPSEvent(ECLPSListener listener) {
|
||||
if (delegate != null) delegate.UnregisterPSEvent(listener);
|
||||
}
|
||||
|
||||
public void unregisterPSEvent(ECLPSListener listener) { UnregisterPSEvent(listener); }
|
||||
|
||||
// ========== Synchronization & Waits ==========
|
||||
|
||||
public boolean WaitForScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForScreen(desc, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForScreen(ECLScreenDesc desc, long timeoutMs) { return WaitForScreen(desc, timeoutMs); }
|
||||
|
||||
public boolean WaitForScreen(ECLScreenDesc desc) {
|
||||
return delegate != null && delegate.WaitForScreen(desc);
|
||||
}
|
||||
|
||||
public boolean waitForScreen(ECLScreenDesc desc) { return WaitForScreen(desc); }
|
||||
|
||||
public boolean WaitWhileScreen(ECLScreenDesc desc, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitWhileScreen(desc, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitWhileScreen(ECLScreenDesc desc, long timeoutMs) { return WaitWhileScreen(desc, timeoutMs); }
|
||||
|
||||
public boolean WaitWhileScreen(ECLScreenDesc desc) {
|
||||
return delegate != null && delegate.WaitWhileScreen(desc);
|
||||
}
|
||||
|
||||
public boolean waitWhileScreen(ECLScreenDesc desc) { return WaitWhileScreen(desc); }
|
||||
|
||||
public boolean WaitForString(String text, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForString(text, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text, long timeoutMs) { return WaitForString(text, timeoutMs); }
|
||||
|
||||
public boolean WaitForString(String text) {
|
||||
return delegate != null && delegate.WaitForString(text);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text) { return WaitForString(text); }
|
||||
|
||||
public boolean WaitForString(String text, int row, int col, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForString(text, row, col, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForString(String text, int row, int col, long timeoutMs) { return WaitForString(text, row, col, timeoutMs); }
|
||||
|
||||
public boolean WaitForCursor(int row, int col, long timeoutMs) {
|
||||
return delegate != null && delegate.WaitForCursor(row, col, timeoutMs);
|
||||
}
|
||||
|
||||
public boolean waitForCursor(int row, int col, long timeoutMs) { return WaitForCursor(row, col, timeoutMs); }
|
||||
|
||||
// ========== Screen Update Inquiries ==========
|
||||
|
||||
public ECLPSUpdate GetScreen(int startRow, int startCol, int length) throws ECLErr {
|
||||
return GetScreen(startRow, startCol, length, false);
|
||||
}
|
||||
|
||||
public ECLPSUpdate GetScreen(int startRow, int startCol, int length, boolean fillNulls) throws ECLErr {
|
||||
int pos = ConvertRowColToPos(startRow, startCol);
|
||||
char[] buf = new char[length];
|
||||
GetString(buf, length, pos, length);
|
||||
if (fillNulls) {
|
||||
for (int i = 0; i < buf.length; i++) {
|
||||
if (buf[i] == '\0') buf[i] = ' ';
|
||||
}
|
||||
}
|
||||
int endPos = pos + length - 1;
|
||||
int endRow = ConvertPosToRow(endPos);
|
||||
int endCol = ConvertPosToCol(endPos);
|
||||
return new ECLPSUpdate(this, startRow, startCol, endRow, endCol, new String(buf));
|
||||
}
|
||||
|
||||
public int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
|
||||
return delegate.GetScreen(buffer, len, row, col, length, plane);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Presentation space notification interface matching IBM Host On-Demand ECLPSNotify.
|
||||
*/
|
||||
public interface ECLPSNotify {
|
||||
void NotifyEvent(ECLPS ps);
|
||||
default void NotifyStop(ECLPS ps, int reason) {}
|
||||
default void NotifyError(ECLPS ps, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Conforms to IBM Host On-Demand ECLPSUpdate.
|
||||
*/
|
||||
public class ECLPSUpdate implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate;
|
||||
private final int startRow;
|
||||
private final int startCol;
|
||||
private final int endRow;
|
||||
private final int endCol;
|
||||
private final int start;
|
||||
private final int end;
|
||||
private final boolean fullUpdate;
|
||||
private final String text;
|
||||
private final transient ECLPS ps;
|
||||
|
||||
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol,
|
||||
int start, int end, boolean fullUpdate, String text) {
|
||||
this.ps = ps;
|
||||
this.startRow = startRow;
|
||||
this.startCol = startCol;
|
||||
this.endRow = endRow;
|
||||
this.endCol = endCol;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.fullUpdate = fullUpdate;
|
||||
this.text = text != null ? text : "";
|
||||
this.delegate = null;
|
||||
}
|
||||
|
||||
public ECLPSUpdate(ECLPS ps, int startRow, int startCol, int endRow, int endCol, String text) {
|
||||
this(ps, startRow, startCol, endRow, endCol, 0, 0, false, text);
|
||||
}
|
||||
|
||||
public ECLPSUpdate(int startRow, int startCol, int endRow, int endCol, String text) {
|
||||
this(null, startRow, startCol, endRow, endCol, 0, 0, false, text);
|
||||
}
|
||||
|
||||
public ECLPSUpdate(haus.nightmare.lib3270j.ecl.ECLPSUpdate delegate) {
|
||||
this.delegate = delegate;
|
||||
this.ps = (delegate != null && delegate.getPS() != null) ? new ECLPS(delegate.getPS()) : null;
|
||||
this.startRow = delegate != null ? delegate.getStartRow() : 0;
|
||||
this.startCol = delegate != null ? delegate.getStartCol() : 0;
|
||||
this.endRow = delegate != null ? delegate.getEndRow() : 0;
|
||||
this.endCol = delegate != null ? delegate.getEndCol() : 0;
|
||||
this.start = delegate != null ? delegate.getStart() : 0;
|
||||
this.end = delegate != null ? delegate.getEnd() : 0;
|
||||
this.fullUpdate = delegate != null && delegate.isFullUpdate();
|
||||
this.text = delegate != null ? delegate.getText() : "";
|
||||
}
|
||||
|
||||
public ECLPS getPS() { return ps; }
|
||||
public ECLPS GetPS() { return ps; }
|
||||
|
||||
public int getStartRow() { return startRow; }
|
||||
public int GetStartRow() { return startRow + 1; }
|
||||
|
||||
public int getStartCol() { return startCol; }
|
||||
public int GetStartCol() { return startCol + 1; }
|
||||
|
||||
public int getEndRow() { return endRow; }
|
||||
public int GetEndRow() { return endRow + 1; }
|
||||
|
||||
public int getEndCol() { return endCol; }
|
||||
public int GetEndCol() { return endCol + 1; }
|
||||
|
||||
public int getStart() { return start; }
|
||||
public int GetStart() { return start + 1; }
|
||||
|
||||
public int getEnd() { return end; }
|
||||
public int GetEnd() { return end + 1; }
|
||||
|
||||
public boolean isFullUpdate() { return fullUpdate; }
|
||||
public boolean IsFullUpdate() { return fullUpdate; }
|
||||
|
||||
public String getText() { return text; }
|
||||
public String GetText() { return text; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ECLPSUpdate[start=(%d,%d), end=(%d,%d), full=%b]",
|
||||
startRow, startCol, endRow, endCol, fullUpdate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching engine facade matching IBM Host On-Demand ECLScreenDesc.
|
||||
*/
|
||||
public class ECLScreenDesc extends haus.nightmare.lib3270j.ecl.ECLScreenDesc implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLScreenDesc() {
|
||||
super();
|
||||
}
|
||||
|
||||
public boolean Matches(ECLPS ps, ECLOIA oia) {
|
||||
haus.nightmare.lib3270j.ecl.ECLPS p = (ps != null) ? ps.getDelegate() : null;
|
||||
haus.nightmare.lib3270j.ecl.ECLOIA o = (oia != null) ? oia.getDelegate() : null;
|
||||
return super.Matches(p, o);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
/**
|
||||
* Listener interface for ECLScreenReco matching notifications matching IBM HoD.
|
||||
*/
|
||||
public interface ECLScreenNotify {
|
||||
void screenMatching(ECLScreenRecoEvent event);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Screen recognition facade matching IBM Host On-Demand ECLScreenReco.
|
||||
*/
|
||||
public class ECLScreenReco {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLScreenReco delegate;
|
||||
private final ECLPS ps;
|
||||
private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>();
|
||||
|
||||
public ECLScreenReco() {
|
||||
this((ECLPS) null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLSession session) {
|
||||
this(session != null ? session.GetPS() : null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLPS ps) {
|
||||
this(ps, (ECLOIA) null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLPS ps, ECLOIA oia) {
|
||||
this.ps = ps;
|
||||
this.delegate = new haus.nightmare.lib3270j.ecl.ECLScreenReco(ps != null ? ps.getDelegate() : null,
|
||||
oia != null ? oia.getDelegate() : null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(haus.nightmare.lib3270j.ecl.ECLScreenReco delegate) {
|
||||
this.delegate = delegate;
|
||||
this.ps = null;
|
||||
}
|
||||
|
||||
public void RegisterScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
|
||||
if (desc != null && notify != null) {
|
||||
notifyMap.put(desc, notify);
|
||||
if (delegate != null) {
|
||||
delegate.RegisterScreen(desc, event -> notify.screenMatching(new ECLScreenRecoEvent(this, desc, ps)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerScreen(ECLScreenDesc desc, ECLScreenNotify notify) {
|
||||
RegisterScreen(desc, notify);
|
||||
}
|
||||
|
||||
public void UnregisterScreen(ECLScreenDesc desc) {
|
||||
if (desc != null) {
|
||||
notifyMap.remove(desc);
|
||||
if (delegate != null) {
|
||||
delegate.UnregisterScreen(desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterScreen(ECLScreenDesc desc) {
|
||||
UnregisterScreen(desc);
|
||||
}
|
||||
|
||||
public boolean IsMatch(ECLScreenDesc desc) {
|
||||
if (delegate != null) {
|
||||
return delegate.IsMatch(desc);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean IsMatch(ECLPS ps, ECLScreenDesc desc) {
|
||||
if (ps == null) return false;
|
||||
if (desc == null) return true;
|
||||
return desc.Matches(ps, (ECLOIA) null);
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLScreenReco getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLEvent;
|
||||
|
||||
/**
|
||||
* Event delivered when an ECLScreenDesc matches the Presentation Space.
|
||||
*/
|
||||
public class ECLScreenRecoEvent extends ECLEvent {
|
||||
|
||||
private final ECLScreenReco source;
|
||||
private final ECLScreenDesc screenDesc;
|
||||
private final ECLPS ps;
|
||||
|
||||
public ECLScreenRecoEvent(ECLScreenReco source, ECLScreenDesc screenDesc, ECLPS ps) {
|
||||
super(source);
|
||||
this.source = source;
|
||||
this.screenDesc = screenDesc;
|
||||
this.ps = ps;
|
||||
}
|
||||
|
||||
public ECLScreenRecoEvent(Object source, ECLPS ps) {
|
||||
super(source);
|
||||
this.source = (source instanceof ECLScreenReco) ? (ECLScreenReco) source : null;
|
||||
this.screenDesc = null;
|
||||
this.ps = ps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object GetSource() { return source != null ? source : super.GetSource(); }
|
||||
|
||||
public ECLScreenReco GetScreenReco() { return source; }
|
||||
public ECLScreenReco getScreenReco() { return source; }
|
||||
|
||||
public ECLScreenDesc GetScreenDesc() { return screenDesc; }
|
||||
public ECLScreenDesc getScreenDesc() { return screenDesc; }
|
||||
|
||||
public ECLPS GetPS() { return ps; }
|
||||
public ECLPS getPS() { return ps; }
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Top-level session facade matching IBM Host On-Demand ECLSession.
|
||||
* Extends ECLConnection and provides access to Presentation Space (ECLPS),
|
||||
* Operator Information Area (ECLOIA), File Transfer (ECLXfer), and Screen Recognition (ECLScreenReco).
|
||||
*/
|
||||
public class ECLSession extends ECLConnection implements ECLConstants {
|
||||
|
||||
public static final String SESSION_NAME = "SESSION_NAME";
|
||||
public static final String SESSION_LABEL = "SESSION_LABEL";
|
||||
public static final String SESSION_TYPE = "SESSION_TYPE";
|
||||
public static final String SESSION_HOST = "SESSION_HOST";
|
||||
public static final String SESSION_HOST_PORT = "SESSION_HOST_PORT";
|
||||
public static final String SESSION_PORT = "SESSION_PORT";
|
||||
public static final String SESSION_PS_SIZE = "SESSION_PS_SIZE";
|
||||
public static final String SESSION_CODE_PAGE = "SESSION_CODE_PAGE";
|
||||
public static final String SESSION_SSL = "SESSION_SSL";
|
||||
public static final String SESSION_SECURITY_PROTOCOL = "SESSION_SECURITY_PROTOCOL";
|
||||
public static final String SESSION_TLS_PROTOCOL_VERSION = "tlsProtocolVersion";
|
||||
public static final String SESSION_LU_NAME = "SESSION_LU_NAME";
|
||||
public static final String SESSION_TN3270E = "SESSION_TN3270E";
|
||||
public static final String SESSION_AUTO_CONNECT = "SESSION_AUTO_CONNECT";
|
||||
public static final String SESSION_AUTORECONNECT = "SESSION_AUTORECONNECT";
|
||||
public static final String SESSION_RECONNECT_RETRIES = "SESSION_RECONNECT_RETRIES";
|
||||
public static final String SESSION_KEEPALIVE = "SESSION_KEEPALIVE";
|
||||
public static final String KEY_KEEPALIVE_TYPE = "keepAliveType";
|
||||
public static final String KEY_KEEPALIVE_TIMEOUT = "keepAliveTimeout";
|
||||
public static final String SESSION_AUTO_SYS_UNLOCK = "autoSysUnlock";
|
||||
public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel";
|
||||
public static final String PASTE_TAB_OPTIONS = "pasteTabOptions";
|
||||
public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine";
|
||||
public static final String BLOCK_PASTE = "blockPaste";
|
||||
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
|
||||
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
|
||||
public static final String PASTE_WORD_BREAK = "pasteWordBreak";
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLSession delegate;
|
||||
private final ECLPS ps;
|
||||
private final ECLOIA oia;
|
||||
private final ECLXfer xfer;
|
||||
private final ECLScreenReco screenReco;
|
||||
|
||||
public ECLSession(Properties properties) throws ECLErr {
|
||||
super(properties);
|
||||
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(properties);
|
||||
this.ps = new ECLPS(delegate.getPS());
|
||||
this.oia = new ECLOIA(delegate.getOIA());
|
||||
this.xfer = new ECLXfer(delegate.getXfer());
|
||||
this.screenReco = new ECLScreenReco(this.ps, this.oia);
|
||||
}
|
||||
|
||||
public ECLSession() throws ECLErr {
|
||||
this(new Properties());
|
||||
}
|
||||
|
||||
public ECLSession(String host, int port) throws ECLErr {
|
||||
super(host, port);
|
||||
Properties props = new Properties();
|
||||
if (host != null) props.setProperty(SESSION_HOST, host);
|
||||
props.setProperty(SESSION_PORT, String.valueOf(port));
|
||||
this.delegate = new haus.nightmare.lib3270j.ecl.ECLSession(props);
|
||||
this.ps = new ECLPS(delegate.getPS());
|
||||
this.oia = new ECLOIA(delegate.getOIA());
|
||||
this.xfer = new ECLXfer(delegate.getXfer());
|
||||
this.screenReco = new ECLScreenReco(this.ps, this.oia);
|
||||
}
|
||||
|
||||
public ECLSession(haus.nightmare.lib3270j.ecl.ECLSession delegate) {
|
||||
super(delegate);
|
||||
this.delegate = delegate;
|
||||
this.ps = new ECLPS(delegate != null ? delegate.getPS() : null);
|
||||
this.oia = new ECLOIA(delegate != null ? delegate.getOIA() : null);
|
||||
this.xfer = new ECLXfer(delegate != null ? delegate.getXfer() : null);
|
||||
this.screenReco = new ECLScreenReco(this.ps, this.oia);
|
||||
}
|
||||
|
||||
public ECLPS GetPS() {
|
||||
return ps;
|
||||
}
|
||||
|
||||
public ECLPS getPS() {
|
||||
return ps;
|
||||
}
|
||||
|
||||
public ECLOIA GetOIA() {
|
||||
return oia;
|
||||
}
|
||||
|
||||
public ECLOIA getOIA() {
|
||||
return oia;
|
||||
}
|
||||
|
||||
public ECLXfer GetXfer() {
|
||||
return xfer;
|
||||
}
|
||||
|
||||
public ECLXfer getXfer() {
|
||||
return xfer;
|
||||
}
|
||||
|
||||
public ECLScreenReco GetScreenReco() {
|
||||
return screenReco;
|
||||
}
|
||||
|
||||
public ECLScreenReco getScreenReco() {
|
||||
return screenReco;
|
||||
}
|
||||
|
||||
public ECLConnection GetConnection() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public ECLConnection getConnection() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLSession getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void StartCommunication() throws IOException {
|
||||
if (delegate != null) {
|
||||
delegate.StartCommunication();
|
||||
} else {
|
||||
super.StartCommunication();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void StopCommunication() {
|
||||
if (delegate != null) {
|
||||
delegate.StopCommunication();
|
||||
} else {
|
||||
super.StopCommunication();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean StartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
return StartCommunicationWithBlocking(timeout, (ECLScreenDesc) null);
|
||||
}
|
||||
|
||||
public boolean startCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
return StartCommunicationWithBlocking(timeout);
|
||||
}
|
||||
|
||||
public boolean StartCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.StartCommunicationWithBlocking(timeout, desc);
|
||||
} catch (IOException e) {
|
||||
throw new ECLErr("ECLSession.StartCommunicationWithBlocking", "ECL0001", e.getMessage());
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean startCommunicationWithBlocking(long timeout, ECLScreenDesc desc) throws InterruptedException, ECLErr {
|
||||
return StartCommunicationWithBlocking(timeout, desc);
|
||||
}
|
||||
|
||||
public boolean RestartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
if (delegate != null) {
|
||||
try {
|
||||
return delegate.RestartCommunicationWithBlocking(timeout);
|
||||
} catch (IOException e) {
|
||||
throw new ECLErr("ECLSession.RestartCommunicationWithBlocking", "ECL0001", e.getMessage());
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
throw new ECLErr(e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean restartCommunicationWithBlocking(long timeout) throws InterruptedException, ECLErr {
|
||||
return RestartCommunicationWithBlocking(timeout);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (delegate != null) {
|
||||
delegate.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.ibm.eNetwork.ECL;
|
||||
|
||||
import com.ibm.eNetwork.ECL.event.ECLXferListener;
|
||||
|
||||
/**
|
||||
* File transfer facade matching IBM Host On-Demand ECLXfer.
|
||||
*/
|
||||
public class ECLXfer {
|
||||
|
||||
private final haus.nightmare.lib3270j.ecl.ECLXfer delegate;
|
||||
|
||||
public ECLXfer(ECLSession session, Boolean bidi) {
|
||||
this.delegate = (session != null && session.getDelegate() != null)
|
||||
? session.getDelegate().GetXfer() : null;
|
||||
}
|
||||
|
||||
public ECLXfer(haus.nightmare.lib3270j.ecl.ECLXfer delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public void SendFile(String localFile, String hostFile, String options) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
int rc = delegate.SendFile(localFile, hostFile, options);
|
||||
if (rc != 0) {
|
||||
throw new ECLErr("ECLXfer:SendFile", "ECL0150", String.valueOf(rc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ReceiveFile(String localFile, String hostFile, String options) throws ECLErr {
|
||||
if (delegate != null) {
|
||||
int rc = delegate.ReceiveFile(localFile, hostFile, options);
|
||||
if (rc != 0) {
|
||||
throw new ECLErr("ECLXfer:ReceiveFile", "ECL0150", String.valueOf(rc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel() {
|
||||
if (delegate != null) {
|
||||
delegate.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterXferEvent(ECLXferListener listener) {
|
||||
// Can bridge listener if needed
|
||||
}
|
||||
|
||||
public void UnregisterXferEvent(ECLXferListener listener) {
|
||||
// Can bridge listener if needed
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLXfer getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
/**
|
||||
* Base event class matching IBM Host On-Demand ECLEvent.
|
||||
*/
|
||||
public class ECLEvent {
|
||||
private final Object originator;
|
||||
|
||||
public ECLEvent(Object object) {
|
||||
this.originator = object;
|
||||
}
|
||||
|
||||
public Object GetSource() {
|
||||
return this.originator;
|
||||
}
|
||||
|
||||
public Object getSource() {
|
||||
return this.originator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + ": " + (this.originator != null ? this.originator.toString() : "null");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLPS;
|
||||
import com.ibm.eNetwork.ECL.ECLPSUpdate;
|
||||
|
||||
/**
|
||||
* Presentation Space event class matching IBM Host On-Demand ECLPSEvent.
|
||||
*/
|
||||
public class ECLPSEvent extends ECLEvent {
|
||||
|
||||
public static final int USER_EVENTS = 1;
|
||||
public static final int HOST_EVENTS = 2;
|
||||
public static final int ALL_EVENTS = 3;
|
||||
|
||||
private int type = 0;
|
||||
private int start = 1;
|
||||
private int end = 1;
|
||||
private int startRow = 1;
|
||||
private int startCol = 1;
|
||||
private int endRow = 1;
|
||||
private int endCol = 1;
|
||||
private int oldCursorPos = 1;
|
||||
private int newCursorPos = 1;
|
||||
private boolean fullUpdate = true;
|
||||
private boolean cursorVisible = true;
|
||||
private int ringCounter = 0;
|
||||
private ECLPSUpdate psUpdate = null;
|
||||
private haus.nightmare.lib3270j.ecl.ECLPSEvent delegate = null;
|
||||
|
||||
public ECLPSEvent(Object source) {
|
||||
super(source);
|
||||
if (source instanceof ECLPS) {
|
||||
ECLPS ps = (ECLPS) source;
|
||||
this.end = ps.GetSize();
|
||||
this.endRow = ps.GetRows();
|
||||
this.endCol = ps.GetCols();
|
||||
this.newCursorPos = ps.GetCursorPos();
|
||||
} else if (source instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
|
||||
haus.nightmare.lib3270j.ecl.ECLPS ps = (haus.nightmare.lib3270j.ecl.ECLPS) source;
|
||||
this.end = ps.GetSize();
|
||||
this.endRow = ps.GetRows();
|
||||
this.endCol = ps.GetCols();
|
||||
this.newCursorPos = ps.GetCursorPos();
|
||||
}
|
||||
}
|
||||
|
||||
public ECLPSEvent(Object source, int type) {
|
||||
this(source);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ECLPSEvent(haus.nightmare.lib3270j.ecl.ECLPSEvent delegate) {
|
||||
super(delegate != null ? delegate.getSource() : null);
|
||||
this.delegate = delegate;
|
||||
if (delegate != null) {
|
||||
this.type = delegate.getType();
|
||||
this.start = delegate.getStart();
|
||||
this.end = delegate.getEnd();
|
||||
this.startRow = delegate.getStartRow();
|
||||
this.startCol = delegate.getStartCol();
|
||||
this.endRow = delegate.getEndRow();
|
||||
this.endCol = delegate.getEndCol();
|
||||
this.oldCursorPos = delegate.getOldCursorAddress() + 1;
|
||||
this.newCursorPos = delegate.getNewCursorAddress() + 1;
|
||||
this.fullUpdate = delegate.isFullUpdate();
|
||||
this.cursorVisible = delegate.isCursorVisible();
|
||||
this.ringCounter = delegate.getRingCounter();
|
||||
if (delegate.getPSUpdate() != null) {
|
||||
this.psUpdate = new ECLPSUpdate(delegate.getPSUpdate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int GetType() { return type; }
|
||||
public int getType() { return type; }
|
||||
|
||||
public ECLPS GetPS() {
|
||||
Object src = GetSource();
|
||||
if (src instanceof ECLPS) {
|
||||
return (ECLPS) src;
|
||||
} else if (src instanceof haus.nightmare.lib3270j.ecl.ECLPS) {
|
||||
return new ECLPS((haus.nightmare.lib3270j.ecl.ECLPS) src);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SetStart(int start) { this.start = start; }
|
||||
public int GetStart() { return start; }
|
||||
public int getStart() { return start; }
|
||||
|
||||
public int GetStartRow() { return startRow; }
|
||||
public int getStartRow() { return startRow; }
|
||||
|
||||
public int GetStartCol() { return startCol; }
|
||||
public int getStartCol() { return startCol; }
|
||||
|
||||
public void SetEnd(int end) { this.end = end; }
|
||||
public int GetEnd() { return end; }
|
||||
public int getEnd() { return end; }
|
||||
|
||||
public int GetEndRow() { return endRow; }
|
||||
public int getEndRow() { return endRow; }
|
||||
|
||||
public int GetEndCol() { return endCol; }
|
||||
public int getEndCol() { return endCol; }
|
||||
|
||||
public int GetOldCursorPos() { return oldCursorPos; }
|
||||
public int getOldCursorPos() { return oldCursorPos; }
|
||||
|
||||
public int GetNewCursorPos() { return newCursorPos; }
|
||||
public int getNewCursorPos() { return newCursorPos; }
|
||||
|
||||
public boolean IsFullUpdate() { return fullUpdate; }
|
||||
public boolean isFullUpdate() { return fullUpdate; }
|
||||
|
||||
public boolean IsCursorVisible() { return cursorVisible; }
|
||||
public boolean isCursorVisible() { return cursorVisible; }
|
||||
|
||||
public int GetRingCounter() { return ringCounter; }
|
||||
public int getRingCounter() { return ringCounter; }
|
||||
|
||||
public ECLPSUpdate GetPSUpdate() { return psUpdate; }
|
||||
public ECLPSUpdate getPSUpdate() { return psUpdate; }
|
||||
|
||||
public haus.nightmare.lib3270j.ecl.ECLPSEvent getDelegate() { return delegate; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLErr;
|
||||
import com.ibm.eNetwork.ECL.ECLPS;
|
||||
|
||||
/**
|
||||
* Presentation Space listener interface matching IBM Host On-Demand ECLPSListener.
|
||||
*/
|
||||
public interface ECLPSListener {
|
||||
void PSNotifyEvent(ECLPSEvent event);
|
||||
default void PSNotifyStop(ECLPS ps, int reason) {}
|
||||
default void PSNotifyError(ECLPS ps, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLXfer;
|
||||
|
||||
/**
|
||||
* File transfer event matching IBM Host On-Demand ECLXferEvent.
|
||||
*/
|
||||
public class ECLXferEvent extends ECLEvent {
|
||||
|
||||
private final String sourceFile;
|
||||
private final String targetFile;
|
||||
private final boolean receive;
|
||||
private final long unitsDone;
|
||||
private final long unitsToDo;
|
||||
|
||||
public ECLXferEvent(Object source, long unitsDone, String sourceFile, String targetFile, boolean receive) {
|
||||
this(source, unitsDone, -1L, sourceFile, targetFile, receive);
|
||||
}
|
||||
|
||||
public ECLXferEvent(Object source, long unitsDone, long unitsToDo, String sourceFile, String targetFile, boolean receive) {
|
||||
super(source);
|
||||
this.unitsDone = unitsDone;
|
||||
this.unitsToDo = unitsToDo;
|
||||
this.sourceFile = sourceFile;
|
||||
this.targetFile = targetFile;
|
||||
this.receive = receive;
|
||||
}
|
||||
|
||||
public ECLXfer GetECLXfer() {
|
||||
Object src = GetSource();
|
||||
return (src instanceof ECLXfer) ? (ECLXfer) src : null;
|
||||
}
|
||||
|
||||
public String GetSourceFile() { return sourceFile; }
|
||||
public String getSourceFile() { return sourceFile; }
|
||||
|
||||
public String GetTargetFile() { return targetFile; }
|
||||
public String getTargetFile() { return targetFile; }
|
||||
|
||||
public long GetBytesSent() { return unitsDone; }
|
||||
public long getBytesSent() { return unitsDone; }
|
||||
|
||||
public long GetBytesToSend() { return unitsToDo; }
|
||||
public long getBytesToSend() { return unitsToDo; }
|
||||
|
||||
public boolean IsReceive() { return receive; }
|
||||
public boolean isReceive() { return receive; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ibm.eNetwork.ECL.event;
|
||||
|
||||
import com.ibm.eNetwork.ECL.ECLErr;
|
||||
import com.ibm.eNetwork.ECL.ECLXfer;
|
||||
|
||||
/**
|
||||
* File transfer listener interface matching IBM Host On-Demand ECLXferListener.
|
||||
*/
|
||||
public interface ECLXferListener {
|
||||
void XferNotifyEvent(ECLXferEvent event);
|
||||
default void XferNotifyStop(ECLXfer xfer, int reason) {}
|
||||
default void XferNotifyError(ECLXfer xfer, ECLErr err) {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ibm.eNetwork.ECL.macrovariable;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroExpressionParser.
|
||||
*/
|
||||
public class MacroExpressionParser extends haus.nightmare.lib3270j.macro.variable.MacroExpressionParser implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MacroExpressionParser() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ibm.eNetwork.ECL.macrovariable;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroVariables.
|
||||
*/
|
||||
public class MacroVariables extends haus.nightmare.lib3270j.macro.variable.MacroVariables implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MacroVariables() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ibm.eNetwork.ECL.screenreco;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand ECLSDAttrib drop-in facade.
|
||||
*/
|
||||
public class ECLSDAttrib extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDAttrib {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLSDAttrib() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDAttrib(char attrib, int row, int col, int plane) {
|
||||
super(attrib, row, col, plane);
|
||||
}
|
||||
|
||||
public ECLSDAttrib(char attrib, int row, int col, int plane, boolean optional) {
|
||||
super(attrib, row, col, plane, optional);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ibm.eNetwork.ECL.screenreco;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand ECLSDBlock drop-in facade.
|
||||
*/
|
||||
public class ECLSDBlock extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlock {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLSDBlock() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDBlock(int srow, int scol, boolean caseSense, boolean optional) {
|
||||
super(srow, scol, caseSense, optional);
|
||||
}
|
||||
|
||||
public ECLSDBlock(int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
|
||||
super(srow, scol, erow, ecol, caseSense, optional);
|
||||
}
|
||||
|
||||
public ECLSDBlock(String[] textLines, int srow, int scol, boolean caseSense) {
|
||||
super(textLines, srow, scol, caseSense);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ibm.eNetwork.ECL.screenreco;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand ECLSDBlockString drop-in facade.
|
||||
*/
|
||||
public class ECLSDBlockString extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlockString {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLSDBlockString() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDBlockString(String text) {
|
||||
super(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ibm.eNetwork.ECL.screenreco;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand ECLSDChecksum drop-in facade.
|
||||
*/
|
||||
public class ECLSDChecksum extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDChecksum {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLSDChecksum() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDChecksum(int value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
public ECLSDChecksum(int value, boolean optional) {
|
||||
super(value, optional);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ibm.eNetwork.ECL.screenreco;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand ECLSDCursor drop-in facade.
|
||||
*/
|
||||
public class ECLSDCursor extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDCursor {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLSDCursor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDCursor(int row, int col) {
|
||||
super(row, col);
|
||||
}
|
||||
|
||||
public ECLSDCursor(int row, int col, boolean optional) {
|
||||
super(row, col, optional);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ibm.eNetwork.ECL.screenreco;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand ECLSDFields drop-in facade.
|
||||
*/
|
||||
public class ECLSDFields extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDFields {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLSDFields() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDFields(int num) {
|
||||
super(num);
|
||||
}
|
||||
|
||||
public ECLSDFields(int num, boolean optional) {
|
||||
super(num, optional);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ibm.eNetwork.ECL.screenreco;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand ECLSDInputFields drop-in facade.
|
||||
*/
|
||||
public class ECLSDInputFields extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDInputFields {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLSDInputFields() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDInputFields(int num) {
|
||||
super(num);
|
||||
}
|
||||
|
||||
public ECLSDInputFields(int num, boolean optional) {
|
||||
super(num, optional);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ibm.eNetwork.ECL.screenreco;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand ECLSDOIA drop-in facade.
|
||||
*/
|
||||
public class ECLSDOIA extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDOIA {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLSDOIA() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDOIA(int type) {
|
||||
super(type);
|
||||
}
|
||||
|
||||
public ECLSDOIA(int type, boolean optional) {
|
||||
super(type, optional);
|
||||
}
|
||||
|
||||
public ECLSDOIA(String status, boolean optional) {
|
||||
super(status, optional);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ibm.eNetwork.ECL.screenreco;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand ECLSDString drop-in facade.
|
||||
*/
|
||||
public class ECLSDString extends haus.nightmare.lib3270j.ecl.screenreco.ECLSDString {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ECLSDString() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDString(String text) {
|
||||
super(text);
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int row, int col) {
|
||||
super(text, row, col);
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int row, int col, boolean caseSense) {
|
||||
super(text, row, col, caseSense);
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int row, int col, boolean caseSense, boolean optional) {
|
||||
super(text, row, col, caseSense, optional);
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
|
||||
super(text, srow, scol, erow, ecol, caseSense, optional);
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional, boolean wrap) {
|
||||
super(text, srow, scol, erow, ecol, caseSense, optional, wrap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ibm.eNetwork.ECL.screenreco;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand ECLScreenDescriptor drop-in facade.
|
||||
*/
|
||||
public abstract class ECLScreenDescriptor extends haus.nightmare.lib3270j.ecl.screenreco.ECLScreenDescriptor {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected ECLScreenDescriptor() {
|
||||
super();
|
||||
}
|
||||
|
||||
protected ECLScreenDescriptor(boolean optional) {
|
||||
super(optional);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ibm.eNetwork.beans.HOD;
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.ibm.eNetwork.ECL.ECLSession;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand Macro bean.
|
||||
*/
|
||||
public class Macro extends haus.nightmare.lib3270j.macro.Macro implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public Macro() {
|
||||
super();
|
||||
}
|
||||
|
||||
public Macro(ECLSession session) {
|
||||
super(session != null ? session.getDelegate() : null);
|
||||
}
|
||||
|
||||
public Macro(haus.nightmare.lib3270j.ecl.ECLSession session) {
|
||||
super(session);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ibm.eNetwork.beans.HOD;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroAction.
|
||||
*/
|
||||
public abstract class MacroAction extends haus.nightmare.lib3270j.macro.action.MacroAction implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ibm.eNetwork.beans.HOD;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroActions.
|
||||
*/
|
||||
public class MacroActions extends haus.nightmare.lib3270j.macro.MacroActions implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MacroActions() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ibm.eNetwork.beans.HOD;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroNextScreens.
|
||||
*/
|
||||
public class MacroNextScreens extends haus.nightmare.lib3270j.macro.MacroNextScreens implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MacroNextScreens() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ibm.eNetwork.beans.HOD;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroScreen.
|
||||
*/
|
||||
public class MacroScreen extends haus.nightmare.lib3270j.macro.MacroScreen implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MacroScreen() {
|
||||
super();
|
||||
}
|
||||
|
||||
public MacroScreen(String name) {
|
||||
super(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ibm.eNetwork.beans.HOD;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Compatibility facade for IBM Host On-Demand MacroScreens.
|
||||
*/
|
||||
public class MacroScreens extends haus.nightmare.lib3270j.macro.MacroScreens implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MacroScreens() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,9 @@ public class Telnet3270Client {
|
||||
ps.notifyAlarm();
|
||||
}
|
||||
@Override public void onKeyboardUnlocked() {
|
||||
if (screenBuffer != null) {
|
||||
screenBuffer.getHistoryBuffer().snapshot(screenBuffer);
|
||||
}
|
||||
ps.notifyKeyUnlocked();
|
||||
}
|
||||
});
|
||||
@@ -658,6 +661,9 @@ public class Telnet3270Client {
|
||||
public void setWordWrap(boolean b) { if (screenBuffer != null) screenBuffer.setEntryAssistWordWrap(b); if (oia != null) oia.notifyOIAChanged(); }
|
||||
public void toggleWordWrap() { setWordWrap(!isWordWrap()); }
|
||||
|
||||
public boolean isPasteWordBreak() { return inputProcessor != null && inputProcessor.isPasteWordBreak(); }
|
||||
public void setPasteWordBreak(boolean b) { if (inputProcessor != null) inputProcessor.setPasteWordBreak(b); }
|
||||
|
||||
/** APL Keyboard Mode operations. */
|
||||
public boolean isAplMode() { return inputProcessor != null && inputProcessor.isAplKeyboardMode(); }
|
||||
public void setAplMode(boolean b) { if (inputProcessor != null) inputProcessor.setAplKeyboardMode(b); }
|
||||
@@ -705,4 +711,8 @@ public class Telnet3270Client {
|
||||
public haus.nightmare.lib3270j.graphics.GocaDecoder getGocaDecoder() {
|
||||
return dsProcessor.getGocaDecoder();
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.screen.ScreenHistoryBuffer getScreenHistoryBuffer() {
|
||||
return screenBuffer != null ? screenBuffer.getHistoryBuffer() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ public class DataStreamProcessor {
|
||||
private byte currentBg = 0;
|
||||
private byte currentGr = 0;
|
||||
private byte currentCs = 0;
|
||||
private byte currentOl = 0;
|
||||
|
||||
/** Functional interface for sending output back through the telnet stack. */
|
||||
@FunctionalInterface
|
||||
@@ -250,6 +251,7 @@ public class DataStreamProcessor {
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
currentOl = 0;
|
||||
break;
|
||||
case CMD_WSF:
|
||||
case SNA_CMD_WSF:
|
||||
@@ -392,6 +394,7 @@ public class DataStreamProcessor {
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
currentOl = 0;
|
||||
}
|
||||
|
||||
if (eraseFirst) {
|
||||
@@ -401,6 +404,7 @@ public class DataStreamProcessor {
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
currentOl = 0;
|
||||
}
|
||||
|
||||
// Process orders and data starting at byte 2
|
||||
@@ -497,6 +501,7 @@ public class DataStreamProcessor {
|
||||
currentBg = 0;
|
||||
currentGr = 0;
|
||||
currentCs = 0;
|
||||
currentOl = 0;
|
||||
break;
|
||||
case XA_FOREGROUND:
|
||||
currentFg = (byte) attrValue;
|
||||
@@ -521,6 +526,10 @@ public class DataStreamProcessor {
|
||||
case XA_CHARSET:
|
||||
currentCs = (byte) attrValue;
|
||||
break;
|
||||
case XA_OUTLINING:
|
||||
case 0x84:
|
||||
currentOl = (byte) attrValue;
|
||||
break;
|
||||
}
|
||||
pos += 3;
|
||||
lastWasOrder = true;
|
||||
@@ -605,6 +614,7 @@ public class DataStreamProcessor {
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.cs = fillCs;
|
||||
ea.ol = currentOl;
|
||||
baddr = (baddr + 1) % size;
|
||||
} while (baddr != toAddr);
|
||||
screen.setBufferAddress(baddr);
|
||||
@@ -633,6 +643,7 @@ public class DataStreamProcessor {
|
||||
ea.bg = 0;
|
||||
ea.gr = 0;
|
||||
ea.cs = 0;
|
||||
ea.ol = 0;
|
||||
}
|
||||
}
|
||||
baddr = (baddr + 1) % size;
|
||||
@@ -657,6 +668,7 @@ public class DataStreamProcessor {
|
||||
ea.fg = currentFg;
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.ol = currentOl;
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
pos += 2;
|
||||
@@ -683,6 +695,7 @@ public class DataStreamProcessor {
|
||||
ea.bg = currentBg;
|
||||
ea.gr = (byte) currentGr;
|
||||
ea.cs = currentCs;
|
||||
ea.ol = currentOl;
|
||||
|
||||
baddr = (baddr + 1) % size;
|
||||
screen.setBufferAddress(baddr);
|
||||
@@ -995,6 +1008,7 @@ public class DataStreamProcessor {
|
||||
byte[] psData = new byte[fieldLen - 3];
|
||||
System.arraycopy(data, pos + 3, psData, 0, psData.length);
|
||||
programSymbolManager.loadps(psData);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA
|
||||
@@ -1305,6 +1319,7 @@ public class DataStreamProcessor {
|
||||
byte[] psData = new byte[fieldLen - 4];
|
||||
System.arraycopy(data, offset + 4, psData, 0, psData.length);
|
||||
programSymbolManager.loadps(psData);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
|
||||
|
||||
@@ -482,8 +482,8 @@ public class QueryReplyBuilder {
|
||||
cpgid = screen.getTranslator().getCpgid();
|
||||
}
|
||||
|
||||
if (graphicsMode == GraphicsMode.PROGRAMMED_SYMBOLS) {
|
||||
// Programmed Symbols only mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
||||
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
||||
// Programmed Symbols mode (3279 PS / 3179G with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
||||
out.write(0x00); // more flags
|
||||
@@ -681,16 +681,18 @@ public class QueryReplyBuilder {
|
||||
public byte[] buildGrSymbolSet() {
|
||||
int charW = getCharWidth();
|
||||
int charH = getCharHeight();
|
||||
int cgcsgid = 0x02B9;
|
||||
int cpgid = 0x0025;
|
||||
if (screen != null && screen.getTranslator() != null) {
|
||||
cgcsgid = screen.getTranslator().getCgcsgid();
|
||||
cpgid = screen.getTranslator().getCpgid();
|
||||
if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) {
|
||||
return new byte[]{
|
||||
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
||||
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00, (byte) charW, (byte) charH,
|
||||
0x00, 0x01, 0x4C, 0x01, 0x22, 0x00, 0x00, 0x12, 0x01, 0x00, 0x20, (byte) 0xF8, (byte) 0xC1, (byte) 0xC1,
|
||||
0x00, 0x00, 0x18, 0x18, 0x00, 0x01, 0x72, 0x01, 0x2C, 0x00, (byte) 0xFF
|
||||
};
|
||||
}
|
||||
return new byte[]{
|
||||
0x00, 0x00, (byte) charW, (byte) charH, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x12,
|
||||
0x01, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00,
|
||||
(byte) charW, (byte) charH, 0x00, 0x00, (byte) 0xF0, (byte) ((cgcsgid >> 8) & 0xFF), (byte) (cgcsgid & 0xFF), 0x00, 0x00
|
||||
0x01, 0x00, 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0xC1, 0x00, 0x00,
|
||||
(byte) charW, (byte) charH, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,34 @@ public class ECLConnection {
|
||||
if (props != null) {
|
||||
this.properties.putAll(props);
|
||||
convertData(this.properties);
|
||||
|
||||
String h = this.properties.getProperty(ECLSession.SESSION_HOST);
|
||||
if (h == null) h = this.properties.getProperty("host");
|
||||
if (h == null) h = this.properties.getProperty("Host");
|
||||
if (h == null) h = this.properties.getProperty("SESSION_PRINT_SESSION_HOST");
|
||||
if (h != null && !h.trim().isEmpty()) this.host = h.trim();
|
||||
|
||||
String p = this.properties.getProperty(ECLSession.SESSION_PORT);
|
||||
if (p == null) p = this.properties.getProperty("port");
|
||||
if (p == null) p = this.properties.getProperty("Port");
|
||||
if (p == null) p = this.properties.getProperty("SESSION_HOST_PORT");
|
||||
if (p == null) p = this.properties.getProperty("SESSION_PRINT_SESSION_HOST_PORT");
|
||||
if (p != null && !p.trim().isEmpty()) {
|
||||
try { this.port = Integer.parseInt(p.trim()); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
String cp = this.properties.getProperty(ECLSession.SESSION_CODE_PAGE);
|
||||
if (cp == null) cp = this.properties.getProperty("code_page");
|
||||
if (cp == null) cp = this.properties.getProperty("codepage");
|
||||
if (cp == null) cp = this.properties.getProperty("CodePage");
|
||||
if (cp != null && !cp.trim().isEmpty()) this.codePage = cp.trim();
|
||||
|
||||
String lu = this.properties.getProperty(ECLSession.SESSION_LU_NAME);
|
||||
if (lu == null) lu = this.properties.getProperty("lu_name");
|
||||
if (lu == null) lu = this.properties.getProperty("luname");
|
||||
if (lu == null) lu = this.properties.getProperty("SESSION_PRINT_SESSION_LU_NAME");
|
||||
if (lu != null && !lu.trim().isEmpty()) this.luName = lu.trim();
|
||||
|
||||
String asu = this.properties.getProperty(ECLSession.SESSION_AUTO_SYS_UNLOCK);
|
||||
if (asu != null) {
|
||||
this.autoSysUnlock = "true".equalsIgnoreCase(asu) || "1".equals(asu);
|
||||
|
||||
@@ -11,6 +11,8 @@ public class ECLErr extends Exception {
|
||||
private String id = "";
|
||||
private String text = "";
|
||||
|
||||
private int reasonCode = 0;
|
||||
|
||||
public ECLErr() {
|
||||
super();
|
||||
}
|
||||
@@ -20,6 +22,21 @@ public class ECLErr extends Exception {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, int reasonCode) {
|
||||
super(formatMsg(location, msgNum, "Reason " + reasonCode));
|
||||
this.tag = location;
|
||||
this.id = msgNum;
|
||||
this.text = "Reason " + reasonCode;
|
||||
this.reasonCode = reasonCode;
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum) {
|
||||
super(formatMsg(location, msgNum, ""));
|
||||
this.tag = location;
|
||||
this.id = msgNum;
|
||||
this.text = "";
|
||||
}
|
||||
|
||||
public ECLErr(String tag, String id, String text) {
|
||||
super(formatMsg(tag, id, text));
|
||||
this.tag = tag;
|
||||
@@ -27,11 +44,30 @@ public class ECLErr extends Exception {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public ECLErr(String tag, String id, String text, String extra) {
|
||||
super(formatMsg(tag, id, text + " " + extra));
|
||||
this.tag = tag;
|
||||
this.id = id;
|
||||
this.text = text + " " + extra;
|
||||
public ECLErr(String location, String msgNum, String insert1, int reasonCode) {
|
||||
super(formatMsg(location, msgNum, insert1 + " (Reason " + reasonCode + ")"));
|
||||
this.tag = location;
|
||||
this.id = msgNum;
|
||||
this.text = insert1 + " (Reason " + reasonCode + ")";
|
||||
this.reasonCode = reasonCode;
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2, boolean log) {
|
||||
super(formatMsg(location, msgNum, insert1 + " " + insert2));
|
||||
this.tag = location;
|
||||
this.id = msgNum;
|
||||
this.text = insert1 + " " + insert2;
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2) {
|
||||
this(location, msgNum, insert1, insert2, false);
|
||||
}
|
||||
|
||||
public ECLErr(String location, String msgNum, String insert1, String insert2, String insert3) {
|
||||
super(formatMsg(location, msgNum, insert1 + " " + insert2 + " " + insert3));
|
||||
this.tag = location;
|
||||
this.id = msgNum;
|
||||
this.text = insert1 + " " + insert2 + " " + insert3;
|
||||
}
|
||||
|
||||
public ECLErr(Throwable cause) {
|
||||
@@ -57,14 +93,33 @@ public class ECLErr extends Exception {
|
||||
public String getID() { return id; }
|
||||
public String GetID() { return id; }
|
||||
|
||||
public String getLocation() { return tag; }
|
||||
public String GetLocation() { return tag; }
|
||||
|
||||
public int getReasonCode() { return reasonCode; }
|
||||
public int GetReasonCode() { return reasonCode; }
|
||||
|
||||
public int getMsgNumber() {
|
||||
if (id != null && id.length() >= 7 && id.startsWith("ECL")) {
|
||||
try {
|
||||
return Integer.parseInt(id.substring(3, 7));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
public int GetMsgNumber() { return getMsgNumber(); }
|
||||
|
||||
public String getText() { return text; }
|
||||
public String GetText() { return text; }
|
||||
|
||||
public String getMsgText() { return getMessage(); }
|
||||
public String GetMsgText() { return getMessage(); }
|
||||
|
||||
public String getErrorText() { return getMessage(); }
|
||||
public String GetErrorText() { return getMessage(); }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ECLErr[tag=" + tag + ", id=" + id + ", text=" + text + "]";
|
||||
return "ECLErr[tag=" + tag + ", id=" + id + ", text=" + text + ", reasonCode=" + reasonCode + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ public class ECLField {
|
||||
this.attribute = attribute;
|
||||
}
|
||||
|
||||
public ECLPS getPS() { return ps; }
|
||||
public ECLPS GetPS() { return ps; }
|
||||
public byte getAttributeByte() { return attribute; }
|
||||
|
||||
/** Buffer address of the field attribute character. */
|
||||
public int getStart() { return startPos; }
|
||||
public int GetStart() { return getStart(); }
|
||||
@@ -154,6 +158,8 @@ public class ECLField {
|
||||
return ps.getString(dataStart, length);
|
||||
}
|
||||
public String GetText() { return getText(); }
|
||||
public String getString() { return getText(); }
|
||||
public String GetString() { return getText(); }
|
||||
|
||||
/**
|
||||
* Set the text contents of this field.
|
||||
@@ -162,7 +168,13 @@ public class ECLField {
|
||||
if (isProtected() || length <= 0) return;
|
||||
ps.setText(text, dataStart);
|
||||
}
|
||||
public void SetText(String text) { setText(text); }
|
||||
public void SetText(String text) throws ECLErr {
|
||||
if (isProtected()) {
|
||||
throw new ECLErr("ECLField:SetText", "ECL0015", "Field is protected");
|
||||
}
|
||||
setText(text);
|
||||
}
|
||||
public void SetString(String text) throws ECLErr { SetText(text); }
|
||||
|
||||
/**
|
||||
* Get selector light pen type.
|
||||
|
||||
@@ -7,15 +7,17 @@ import java.util.List;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Manages the collection of all 3270 fields currently present in the Presentation Space.
|
||||
* Conforms to IBM ECL ECLFieldList specification.
|
||||
*/
|
||||
public class ECLFieldList {
|
||||
public class ECLFieldList extends Vector<ECLField> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final ECLPS ps;
|
||||
private final ScreenBuffer screen;
|
||||
private final List<ECLField> fields = new ArrayList<>();
|
||||
|
||||
public ECLFieldList(ECLPS ps, ScreenBuffer screen) {
|
||||
this.ps = ps;
|
||||
@@ -23,11 +25,14 @@ public class ECLFieldList {
|
||||
refresh();
|
||||
}
|
||||
|
||||
public ECLPS getPS() { return ps; }
|
||||
public ScreenBuffer getScreenBuffer() { return screen; }
|
||||
|
||||
/**
|
||||
* Refresh the field list by scanning the presentation space buffer.
|
||||
*/
|
||||
public synchronized void refresh() {
|
||||
fields.clear();
|
||||
clear();
|
||||
if (screen == null || !screen.isFormatted()) {
|
||||
return;
|
||||
}
|
||||
@@ -66,35 +71,35 @@ public class ECLFieldList {
|
||||
}
|
||||
|
||||
byte faVal = screen.getCell(startPos).fa;
|
||||
fields.add(new ECLField(ps, startPos, dataStart, endPos, len, faVal));
|
||||
add(new ECLField(ps, startPos, dataStart, endPos, len, faVal));
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh() { refresh(); }
|
||||
|
||||
public synchronized int getFieldCount() {
|
||||
return fields.size();
|
||||
return size();
|
||||
}
|
||||
public int GetFieldCount() { return getFieldCount(); }
|
||||
|
||||
public synchronized List<ECLField> getFields() {
|
||||
return Collections.unmodifiableList(new ArrayList<>(fields));
|
||||
return Collections.unmodifiableList(new ArrayList<>(this));
|
||||
}
|
||||
public List<ECLField> GetFields() { return getFields(); }
|
||||
|
||||
public synchronized ECLField getFirstField() {
|
||||
if (fields.isEmpty()) return null;
|
||||
return fields.get(0);
|
||||
if (isEmpty()) return null;
|
||||
return get(0);
|
||||
}
|
||||
public ECLField GetFirstField() { return getFirstField(); }
|
||||
|
||||
public synchronized ECLField getNextField(ECLField prev) {
|
||||
if (prev == null || fields.isEmpty()) return getFirstField();
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
ECLField f = fields.get(i);
|
||||
if (prev == null || isEmpty()) return getFirstField();
|
||||
for (int i = 0; i < size(); i++) {
|
||||
ECLField f = get(i);
|
||||
if (f.equals(prev) || f.getStart() == prev.getStart()) {
|
||||
if (i + 1 < fields.size()) {
|
||||
return fields.get(i + 1);
|
||||
if (i + 1 < size()) {
|
||||
return get(i + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -107,12 +112,12 @@ public class ECLFieldList {
|
||||
* Find the field that contains the specified buffer position.
|
||||
*/
|
||||
public synchronized ECLField findField(int pos) {
|
||||
if (fields.isEmpty() || screen == null) return null;
|
||||
if (isEmpty() || screen == null) return null;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return null;
|
||||
pos = ((pos % size) + size) % size;
|
||||
|
||||
for (ECLField f : fields) {
|
||||
for (ECLField f : this) {
|
||||
int start = f.getStart();
|
||||
int end = f.getEnd();
|
||||
if (start <= end) {
|
||||
@@ -140,14 +145,14 @@ public class ECLFieldList {
|
||||
* Get the field preceding the given field in the field list.
|
||||
*/
|
||||
public synchronized ECLField getPreviousField(ECLField next) {
|
||||
if (next == null || fields.isEmpty()) return null;
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
ECLField f = fields.get(i);
|
||||
if (next == null || isEmpty()) return null;
|
||||
for (int i = 0; i < size(); i++) {
|
||||
ECLField f = get(i);
|
||||
if (f.equals(next) || f.getStart() == next.getStart()) {
|
||||
if (i > 0) {
|
||||
return fields.get(i - 1);
|
||||
return get(i - 1);
|
||||
} else {
|
||||
return fields.get(fields.size() - 1);
|
||||
return get(size() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,29 +200,29 @@ public class ECLFieldList {
|
||||
* Find field containing the given text string.
|
||||
*/
|
||||
public synchronized ECLField findField(String text, int startPos) {
|
||||
if (text == null || text.isEmpty() || fields.isEmpty()) return null;
|
||||
if (text == null || text.isEmpty() || isEmpty()) return null;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return null;
|
||||
startPos = ((startPos % size) + size) % size;
|
||||
|
||||
// Find starting index in field list
|
||||
int startIdx = 0;
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
if (fields.get(i).getStart() >= startPos) {
|
||||
for (int i = 0; i < size(); i++) {
|
||||
if (get(i).getStart() >= startPos) {
|
||||
startIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = startIdx; i < fields.size(); i++) {
|
||||
ECLField f = fields.get(i);
|
||||
for (int i = startIdx; i < size(); i++) {
|
||||
ECLField f = get(i);
|
||||
if (f.getText().contains(text)) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
// Wrap around search to beginning of field list
|
||||
for (int i = 0; i < startIdx; i++) {
|
||||
ECLField f = fields.get(i);
|
||||
ECLField f = get(i);
|
||||
if (f.getText().contains(text)) {
|
||||
return f;
|
||||
}
|
||||
@@ -265,18 +270,18 @@ public class ECLFieldList {
|
||||
public boolean MatchAttributes(ECLField f, int mask) { return matchAttributes(f, mask); }
|
||||
|
||||
public synchronized ECLField locateField(int attrMask, ECLField prev) {
|
||||
if (fields.isEmpty()) return null;
|
||||
if (isEmpty()) return null;
|
||||
int startIndex = 0;
|
||||
if (prev != null) {
|
||||
int idx = fields.indexOf(prev);
|
||||
if (idx >= 0 && idx + 1 < fields.size()) {
|
||||
int idx = indexOf(prev);
|
||||
if (idx >= 0 && idx + 1 < size()) {
|
||||
startIndex = idx + 1;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
for (int i = startIndex; i < fields.size(); i++) {
|
||||
ECLField f = fields.get(i);
|
||||
for (int i = startIndex; i < size(); i++) {
|
||||
ECLField f = get(i);
|
||||
if (matchAttributes(f, attrMask)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -109,6 +110,8 @@ public class ECLOIA implements ECLConstants {
|
||||
session != null && session.getClient() != null ? session.getClient().getTelnetFSM() : null);
|
||||
}
|
||||
|
||||
private final List<com.ibm.eNetwork.ECL.ECLOIANotify> hodOiaNotifies = new CopyOnWriteArrayList<>();
|
||||
|
||||
public void RegisterOIAEvent(haus.nightmare.lib3270j.ecl.ECLOIANotify listener) {
|
||||
if (listener != null && !listeners.contains(listener)) {
|
||||
listeners.add(listener);
|
||||
@@ -132,6 +135,29 @@ public class ECLOIA implements ECLConstants {
|
||||
UnregisterOIAEvent(listener);
|
||||
}
|
||||
|
||||
public void RegisterOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) {
|
||||
if (notify != null && !hodOiaNotifies.contains(notify)) {
|
||||
hodOiaNotifies.add(notify);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) {
|
||||
RegisterOIAEvent(notify);
|
||||
}
|
||||
|
||||
public void UnregisterOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) {
|
||||
if (notify != null) {
|
||||
hodOiaNotifies.remove(notify);
|
||||
try {
|
||||
notify.NotifyStop(new com.ibm.eNetwork.ECL.ECLOIA(this), STOP_UNREGISTER);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterOIAEvent(com.ibm.eNetwork.ECL.ECLOIANotify notify) {
|
||||
UnregisterOIAEvent(notify);
|
||||
}
|
||||
|
||||
public void RegisterOIAEvent(ECLOIAListener listener) {
|
||||
if (listener != null && !oiaListeners.contains(listener)) {
|
||||
oiaListeners.add(listener);
|
||||
@@ -166,6 +192,14 @@ public class ECLOIA implements ECLConstants {
|
||||
l.OIANotifyError(this, err);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (com.ibm.eNetwork.ECL.ECLOIANotify l : hodOiaNotifies) {
|
||||
try {
|
||||
com.ibm.eNetwork.ECL.ECLErr ferr = (err instanceof com.ibm.eNetwork.ECL.ECLErr)
|
||||
? (com.ibm.eNetwork.ECL.ECLErr) err
|
||||
: new com.ibm.eNetwork.ECL.ECLErr(err.getMessage());
|
||||
l.NotifyError(new com.ibm.eNetwork.ECL.ECLOIA(this), ferr);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyOIAStop(int reason) {
|
||||
@@ -179,6 +213,11 @@ public class ECLOIA implements ECLConstants {
|
||||
l.OIANotifyStop(this, reason);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (com.ibm.eNetwork.ECL.ECLOIANotify l : hodOiaNotifies) {
|
||||
try {
|
||||
l.NotifyStop(new com.ibm.eNetwork.ECL.ECLOIA(this), reason);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private final ReentrantLock oiaLock = new ReentrantLock();
|
||||
@@ -210,6 +249,11 @@ public class ECLOIA implements ECLConstants {
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (com.ibm.eNetwork.ECL.ECLOIANotify l : hodOiaNotifies) {
|
||||
try {
|
||||
l.OIANotifyEvent(new com.ibm.eNetwork.ECL.ECLOIAEvent(event));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInsertMode() {
|
||||
@@ -267,6 +311,11 @@ public class ECLOIA implements ECLConstants {
|
||||
public void setInputInhibited(int reason) {
|
||||
if (this.inhibitOverride != reason) {
|
||||
this.inhibitOverride = reason;
|
||||
if (reason == INHIBIT_NOT_INHIBITED && inputProcessor != null && inputProcessor.isKeyboardLocked()) {
|
||||
inputProcessor.setKeyboardLocked(false);
|
||||
} else if (reason == INHIBIT_SYSTEM_LOCK && inputProcessor != null && !inputProcessor.isKeyboardLocked()) {
|
||||
inputProcessor.setKeyboardLocked(true);
|
||||
}
|
||||
notifyOIAChanged();
|
||||
}
|
||||
}
|
||||
@@ -424,6 +473,14 @@ public class ECLOIA implements ECLConstants {
|
||||
return getInputInhibited();
|
||||
}
|
||||
|
||||
public boolean isKeyLocked() {
|
||||
return getInputInhibited() != INHIBIT_NOT_INHIBITED;
|
||||
}
|
||||
|
||||
public boolean IsKeyLocked() {
|
||||
return isKeyLocked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until the keyboard becomes unlocked and ready for user input, or timeout expires.
|
||||
* @return true if keyboard unlocked, false if timeout occurred.
|
||||
|
||||
@@ -5,6 +5,7 @@ import haus.nightmare.lib3270j.input.InputProcessor;
|
||||
import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
|
||||
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -155,20 +156,94 @@ public class ECLPS implements ECLConstants {
|
||||
public ECLPSTHAIServices GetECLPSTHAIServices() { return thaiServices; }
|
||||
public ECLPSTHAIServices getTHAIServices() { return thaiServices; }
|
||||
|
||||
public int getSize() { return screen.getRows() * screen.getCols(); }
|
||||
public int getRows() { return screen.getRows(); }
|
||||
public int getCols() { return screen.getCols(); }
|
||||
public int getCursorPos(){ return screen.getCursorAddress(); }
|
||||
public int getCursorRow(){ return screen.getCursorRow(); }
|
||||
public int getCursorCol(){ return screen.getCursorCol(); }
|
||||
public int getSize() { return screen != null ? screen.getRows() * screen.getCols() : 0; }
|
||||
public int getRows() { return screen != null ? screen.getRows() : 0; }
|
||||
public int getCols() { return screen != null ? screen.getCols() : 0; }
|
||||
public int GetSize() { return getSize(); }
|
||||
public int GetRows() { return getRows(); }
|
||||
public int GetCols() { return getCols(); }
|
||||
public int GetSizeRows() { return getRows(); }
|
||||
public int GetSizeCols() { return getCols(); }
|
||||
|
||||
public int getCursorPos(){ return screen != null ? screen.getCursorAddress() : 0; }
|
||||
public int getCursorRow(){ return screen != null ? screen.getCursorRow() : 0; }
|
||||
public int getCursorCol(){ return screen != null ? screen.getCursorCol() : 0; }
|
||||
|
||||
public synchronized int GetCursorPos() {
|
||||
return (screen != null) ? screen.getCursorAddress() + 1 : 1;
|
||||
}
|
||||
|
||||
public synchronized int GetCursorRow() {
|
||||
return (screen != null) ? screen.getCursorRow() + 1 : 1;
|
||||
}
|
||||
|
||||
public synchronized int GetCursorCol() {
|
||||
return (screen != null) ? screen.getCursorCol() + 1 : 1;
|
||||
}
|
||||
|
||||
public void setCursorPos(int pos) {
|
||||
screen.setCursorAddress(pos);
|
||||
if (screen != null) {
|
||||
screen.setCursorAddress(pos);
|
||||
}
|
||||
}
|
||||
|
||||
public void setCursorPos(int row, int col) {
|
||||
int pos = screen.rowColToAddress(row, col);
|
||||
screen.setCursorAddress(pos);
|
||||
if (screen != null) {
|
||||
int pos = screen.rowColToAddress(row, col);
|
||||
screen.setCursorAddress(pos);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void SetCursorPos(int pos) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
if (pos < 1 || pos > getSize()) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetCursorPos", "ECL0010", "1", String.valueOf(pos));
|
||||
}
|
||||
setCursorPos(pos - 1);
|
||||
notifyCursorMoved(0, pos - 1);
|
||||
}
|
||||
|
||||
public synchronized void SetCursorPos(int row, int col) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
SetCursorPos(ConvertRowColToPos(row, col));
|
||||
}
|
||||
|
||||
public synchronized int ConvertPosToRow(int pos) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
if (pos < 1 || pos > getSize()) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertPosToRow", "ECL0010", "1", String.valueOf(pos));
|
||||
}
|
||||
int cols = getCols();
|
||||
return (cols > 0) ? ((pos - 1) / cols + 1) : 1;
|
||||
}
|
||||
|
||||
public synchronized int ConvertPosToCol(int pos) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
if (pos < 1 || pos > getSize()) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertPosToCol", "ECL0010", "1", String.valueOf(pos));
|
||||
}
|
||||
int cols = getCols();
|
||||
return (cols > 0) ? ((pos - 1) % cols + 1) : 1;
|
||||
}
|
||||
|
||||
public synchronized int ConvertRowColToPos(int row, int col) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
if (row < 1 || row > getRows()) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertRowColToPos", "ECL0010", "\"row\"", String.valueOf(row));
|
||||
}
|
||||
if (col < 1 || col > getCols()) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertRowColToPos", "ECL0010", "\"col\"", String.valueOf(col));
|
||||
}
|
||||
return (row - 1) * getCols() + col;
|
||||
}
|
||||
|
||||
public synchronized void ConvertPosToRowCol(int pos, int[] rowCol) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
if (rowCol == null || rowCol.length < 2) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:ConvertPosToRowCol", "ECL0010", "\"rowCol\"", "invalid array");
|
||||
}
|
||||
rowCol[0] = ConvertPosToRow(pos);
|
||||
rowCol[1] = ConvertPosToCol(pos);
|
||||
}
|
||||
|
||||
public synchronized int[] ConvertPosToRowCol(int pos) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
int[] rc = new int[2];
|
||||
ConvertPosToRowCol(pos, rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,6 +352,60 @@ public class ECLPS implements ECLConstants {
|
||||
return getString(pos, length);
|
||||
}
|
||||
|
||||
public synchronized int GetString(char[] buffer, int len, int pos) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
return GetString(buffer, len, pos, len);
|
||||
}
|
||||
|
||||
public synchronized int GetString(char[] buffer, int len, int pos, int length) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
if (buffer == null) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0011", "1");
|
||||
}
|
||||
int size = getSize();
|
||||
if (pos < 1 || pos > size) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"position\"", String.valueOf(pos));
|
||||
}
|
||||
if (len <= 0) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"bufferLength\"", String.valueOf(len));
|
||||
}
|
||||
if (length <= 0) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"length\"", String.valueOf(length));
|
||||
}
|
||||
int copyCount = Math.min(Math.min(buffer.length, len), Math.min(length, size - pos + 1));
|
||||
char[] temp = new char[copyCount];
|
||||
getPlane(PLANE_TEXT, temp, pos - 1, copyCount);
|
||||
System.arraycopy(temp, 0, buffer, 0, copyCount);
|
||||
return copyCount;
|
||||
}
|
||||
|
||||
public synchronized int GetString(char[] buffer, int len, int row, int col, int length) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
return GetString(buffer, len, ConvertRowColToPos(row, col), length);
|
||||
}
|
||||
|
||||
public synchronized String GetString(int pos, int length) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
int size = getSize();
|
||||
if (pos < 1 || pos > size) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:GetString", "ECL0010", "\"pos\"", String.valueOf(pos));
|
||||
}
|
||||
return getString(pos - 1, length);
|
||||
}
|
||||
|
||||
public synchronized String GetString(int row, int col, int length) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
return GetString(ConvertRowColToPos(row, col), length);
|
||||
}
|
||||
|
||||
public synchronized int GetScreen(char[] buffer, int len, int row, int col, int length, int plane) {
|
||||
if (buffer == null || len <= 0 || length <= 0 || screen == null) return 0;
|
||||
int r = (row > 0) ? row - 1 : 0;
|
||||
int c = (col > 0) ? col - 1 : 0;
|
||||
int pos = screen.rowColToAddress(r, c);
|
||||
int copyCount = Math.min(Math.min(buffer.length, len), length);
|
||||
return getPlane(plane, buffer, pos, copyCount);
|
||||
}
|
||||
|
||||
public int getScreen(char[] buffer, int len, int row, int col, int length, int plane) {
|
||||
return GetScreen(buffer, len, row, col, length, plane);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert text directly into unprotected fields in the presentation space starting at pos.
|
||||
*/
|
||||
@@ -319,6 +448,34 @@ public class ECLPS implements ECLConstants {
|
||||
setText(text, p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert text at current cursor position.
|
||||
*/
|
||||
public void SetText(String text) {
|
||||
SetText(text, GetCursorPos());
|
||||
}
|
||||
public void setText(String text) {
|
||||
SetText(text);
|
||||
}
|
||||
|
||||
public synchronized void SetString(String text, int pos) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
if (text == null) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetString", "ECL0010", "\"text\"", "null");
|
||||
}
|
||||
if (pos < 1 || pos > getSize()) {
|
||||
throw new com.ibm.eNetwork.ECL.ECLErr("ECLPS:SetString", "ECL0010", "\"pos\"", String.valueOf(pos));
|
||||
}
|
||||
SetText(text, pos);
|
||||
}
|
||||
|
||||
public synchronized void SetString(String text, int row, int col) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
SetString(text, ConvertRowColToPos(row, col));
|
||||
}
|
||||
|
||||
public synchronized void SetString(String text) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
SetString(text, GetCursorPos());
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a string in the presentation space (0-based indexing).
|
||||
* Returns 0-based position, or -1 if not found.
|
||||
@@ -500,6 +657,86 @@ public class ECLPS implements ECLConstants {
|
||||
return SearchPSExt(text, startRow, startCol, endRow, endCol, dir, ignoreCase, wrap);
|
||||
}
|
||||
|
||||
// ========== IBM HoD SearchText / SearchString (1-based API) ==========
|
||||
|
||||
public synchronized int SearchText(String text) {
|
||||
return SearchText(text, 1, SEARCH_FORWARD);
|
||||
}
|
||||
|
||||
public synchronized int searchText(String text) {
|
||||
return SearchText(text);
|
||||
}
|
||||
|
||||
public synchronized int SearchText(String text, int dir) {
|
||||
return dir == SEARCH_BACKWARD ? SearchText(text, getSize(), dir) : SearchText(text, 1, dir);
|
||||
}
|
||||
|
||||
public synchronized int searchText(String text, int dir) {
|
||||
return SearchText(text, dir);
|
||||
}
|
||||
|
||||
public synchronized int SearchText(String text, int arg1, int arg2) {
|
||||
int startPos;
|
||||
int dir;
|
||||
if (arg1 > 2) {
|
||||
startPos = arg1;
|
||||
dir = arg2;
|
||||
} else if (arg2 > 2) {
|
||||
dir = arg1;
|
||||
startPos = arg2;
|
||||
} else {
|
||||
dir = arg1;
|
||||
startPos = arg2;
|
||||
}
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
int size = getSize();
|
||||
if (size <= 0) return 0;
|
||||
if (startPos < 1) startPos = 1;
|
||||
if (startPos > size) startPos = size;
|
||||
|
||||
char[] fullScreen = new char[size];
|
||||
getPlane(PLANE_TEXT, fullScreen, 0, size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (fullScreen[i] == '\0') fullScreen[i] = ' ';
|
||||
}
|
||||
String screenText = new String(fullScreen);
|
||||
int idx = (dir == SEARCH_BACKWARD) ? screenText.lastIndexOf(text, startPos - 1)
|
||||
: screenText.indexOf(text, startPos - 1);
|
||||
return idx == -1 ? 0 : idx + 1;
|
||||
}
|
||||
|
||||
public synchronized int searchText(String text, int arg1, int arg2) {
|
||||
return SearchText(text, arg1, arg2);
|
||||
}
|
||||
|
||||
public synchronized int SearchText(String text, int row, int col, int dir) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
return SearchText(text, ConvertRowColToPos(row, col), dir);
|
||||
}
|
||||
|
||||
public synchronized int searchText(String text, int row, int col, int dir) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
return SearchText(text, row, col, dir);
|
||||
}
|
||||
|
||||
public synchronized int SearchString(String text) {
|
||||
return SearchText(text);
|
||||
}
|
||||
|
||||
public synchronized int searchString(String text, int dir) {
|
||||
return SearchText(text, dir);
|
||||
}
|
||||
|
||||
public synchronized int SearchString(String text, int dir) {
|
||||
return SearchText(text, dir);
|
||||
}
|
||||
|
||||
public synchronized int SearchString(String text, int arg1, int arg2) {
|
||||
return SearchText(text, arg1, arg2);
|
||||
}
|
||||
|
||||
public synchronized int SearchString(String text, int row, int col, int dir) throws com.ibm.eNetwork.ECL.ECLErr {
|
||||
return SearchText(text, row, col, dir);
|
||||
}
|
||||
|
||||
private boolean matchesAt(String screenText, String target, int pos, int size) {
|
||||
int len = target.length();
|
||||
for (int j = 0; j < len; j++) {
|
||||
@@ -608,13 +845,37 @@ public class ECLPS implements ECLConstants {
|
||||
|
||||
public void SetPasteStopAtProtectedLine(boolean val) { setPasteStopAtProtectedLine(val); }
|
||||
|
||||
private boolean pasteWordBreak = true;
|
||||
|
||||
public boolean isPasteWordBreak() {
|
||||
if (session != null && session.getProperties() != null) {
|
||||
String p = session.getProperties().getProperty(ECLSession.PASTE_WORD_BREAK);
|
||||
if (p != null) return Boolean.parseBoolean(p);
|
||||
}
|
||||
return inputProcessor != null ? inputProcessor.isPasteWordBreak() : pasteWordBreak;
|
||||
}
|
||||
|
||||
public boolean IsPasteWordBreak() { return isPasteWordBreak(); }
|
||||
|
||||
public void setPasteWordBreak(boolean val) {
|
||||
this.pasteWordBreak = val;
|
||||
if (session != null && session.getProperties() != null) {
|
||||
session.getProperties().setProperty(ECLSession.PASTE_WORD_BREAK, String.valueOf(val));
|
||||
}
|
||||
if (inputProcessor != null) {
|
||||
inputProcessor.setPasteWordBreak(val);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPasteWordBreak(boolean val) { setPasteWordBreak(val); }
|
||||
|
||||
public synchronized int pasteFromExcel(String text, int row, int col) {
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
if (row >= 0 && col >= 0) {
|
||||
setCursorPos(row, col);
|
||||
}
|
||||
if (inputProcessor != null) {
|
||||
return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine(), isBlockPaste());
|
||||
return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine(), isBlockPaste(), isPasteWordBreak());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -630,7 +891,7 @@ public class ECLPS implements ECLConstants {
|
||||
if (text == null || text.isEmpty() || screen == null) return 0;
|
||||
if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine() || isBlockPaste())) {
|
||||
setCursorPos(row, col);
|
||||
return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine(), isBlockPaste());
|
||||
return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine(), isBlockPaste(), isPasteWordBreak());
|
||||
}
|
||||
int rows = screen.getRows();
|
||||
int cols = screen.getCols();
|
||||
@@ -920,6 +1181,10 @@ public class ECLPS implements ECLConstants {
|
||||
// ========== ECLPS Event Listener Management ==========
|
||||
|
||||
private final java.util.List<ECLPSListener> psListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
private final java.util.List<com.ibm.eNetwork.ECL.ECLPSNotify> psNotifies = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
private final java.util.Map<com.ibm.eNetwork.ECL.ECLPSNotify, Integer> notifyEventTypes = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private final java.util.Map<com.ibm.eNetwork.ECL.ECLPSNotify, Object> notifyDescriptors = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private final java.util.List<com.ibm.eNetwork.ECL.event.ECLPSListener> hodPsListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
|
||||
public boolean isCursorVisible() { return cursorVisible; }
|
||||
public boolean GetCursorVisible() { return cursorVisible; }
|
||||
@@ -1007,6 +1272,111 @@ public class ECLPS implements ECLConstants {
|
||||
UnregisterPSEvent(listener);
|
||||
}
|
||||
|
||||
public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) {
|
||||
RegisterPSEvent(notify, ALL_EVENTS);
|
||||
}
|
||||
|
||||
public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) {
|
||||
RegisterPSEvent(notify);
|
||||
}
|
||||
|
||||
public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, int eventType) {
|
||||
if (notify != null) {
|
||||
notifyEventTypes.put(notify, eventType);
|
||||
if (!psNotifies.contains(notify)) {
|
||||
psNotifies.add(notify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, int eventType) {
|
||||
RegisterPSEvent(notify, eventType);
|
||||
}
|
||||
|
||||
public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc) {
|
||||
RegisterPSEvent(notify, desc, ALL_EVENTS);
|
||||
}
|
||||
|
||||
public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc) {
|
||||
RegisterPSEvent(notify, desc);
|
||||
}
|
||||
|
||||
public void RegisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
|
||||
if (notify != null) {
|
||||
if (desc != null) {
|
||||
notifyDescriptors.put(notify, desc);
|
||||
} else {
|
||||
notifyDescriptors.remove(notify);
|
||||
}
|
||||
notifyEventTypes.put(notify, eventType);
|
||||
if (!psNotifies.contains(notify)) {
|
||||
psNotifies.add(notify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify, ECLScreenDesc desc, int eventType) {
|
||||
RegisterPSEvent(notify, desc, eventType);
|
||||
}
|
||||
|
||||
public void UnregisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) {
|
||||
if (notify != null) {
|
||||
psNotifies.remove(notify);
|
||||
notifyDescriptors.remove(notify);
|
||||
notifyEventTypes.remove(notify);
|
||||
try {
|
||||
notify.NotifyStop(new com.ibm.eNetwork.ECL.ECLPS(this), STOP_UNREGISTER);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterPSEvent(com.ibm.eNetwork.ECL.ECLPSNotify notify) {
|
||||
UnregisterPSEvent(notify);
|
||||
}
|
||||
|
||||
public void RegisterPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) {
|
||||
if (listener != null && !hodPsListeners.contains(listener)) {
|
||||
hodPsListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) {
|
||||
RegisterPSEvent(listener);
|
||||
}
|
||||
|
||||
public void UnregisterPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) {
|
||||
if (listener != null) {
|
||||
hodPsListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterPSEvent(com.ibm.eNetwork.ECL.event.ECLPSListener listener) {
|
||||
UnregisterPSEvent(listener);
|
||||
}
|
||||
|
||||
private static final String[] HACL_MNEMONICS = new String[]{
|
||||
"[attn]", "[backspace]", "[backtab]", "[clear]", "[down]", "[left]", "[right]", "[up]",
|
||||
"[delete]", "[enter]", "[tab]", "[eof]", "[eraseeof]", "[erasefld]", "[erinp]",
|
||||
"[pf1]", "[pf2]", "[pf3]", "[pf4]", "[pf5]", "[pf6]", "[pf7]", "[pf8]", "[pf9]",
|
||||
"[pf10]", "[pf11]", "[pf12]", "[pf13]", "[pf14]", "[pf15]", "[pf16]", "[pf17]",
|
||||
"[pf18]", "[pf19]", "[pf20]", "[pf21]", "[pf22]", "[pf23]", "[pf24]",
|
||||
"[newline]", "[pa1]", "[pa2]", "[pa3]", "[pagedn]", "[pageup]", "[sysreq]",
|
||||
"[wordleft]", "[wordright]", "[backtabword]", "[tabword]", "[previousword]",
|
||||
"[nextword]", "[deleteword]", "[insert]", "[home]", "[reset]", "[dup]",
|
||||
"[fieldmark]", "[fldext]", "[field+]", "[field-]", "[test]", "[help]",
|
||||
"[bof]", "[cursel]", "[selfld]", "[deselfld]", "[+cr]", "[altview]",
|
||||
"[printhost]", "[screenrev]", "[copy]", "[paste]", "[cut]", "[markleft]",
|
||||
"[markright]", "[markup]", "[markdown]", "[unmark]", "[enterreset]"
|
||||
};
|
||||
|
||||
public String[] GetSendKeyMnemonics() {
|
||||
return HACL_MNEMONICS.clone();
|
||||
}
|
||||
|
||||
public String[] getSendKeyMnemonics() {
|
||||
return GetSendKeyMnemonics();
|
||||
}
|
||||
|
||||
public void dispatchEvent(ECLPSEvent event) {
|
||||
notifyPSEvent(event);
|
||||
}
|
||||
@@ -1045,6 +1415,26 @@ public class ECLPS implements ECLConstants {
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (com.ibm.eNetwork.ECL.ECLPSNotify n : psNotifies) {
|
||||
Object d = notifyDescriptors.get(n);
|
||||
if (d instanceof ECLScreenDesc && !((ECLScreenDesc) d).Matches(this, session != null ? session.GetOIA() : null)) {
|
||||
continue;
|
||||
}
|
||||
int filter = notifyEventTypes.getOrDefault(n, ALL_EVENTS);
|
||||
int evtCategory = event.GetType();
|
||||
if (filter != ALL_EVENTS && (filter & evtCategory) == 0) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
n.NotifyEvent(new com.ibm.eNetwork.ECL.ECLPS(this));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (com.ibm.eNetwork.ECL.event.ECLPSListener hl : hodPsListeners) {
|
||||
try {
|
||||
com.ibm.eNetwork.ECL.event.ECLPSEvent facadeEvt = new com.ibm.eNetwork.ECL.event.ECLPSEvent(event);
|
||||
hl.PSNotifyEvent(facadeEvt);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyPSError(ECLErr err) {
|
||||
@@ -1053,6 +1443,19 @@ public class ECLPS implements ECLConstants {
|
||||
l.PSNotifyError(this, err);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
com.ibm.eNetwork.ECL.ECLErr ferr = (err instanceof com.ibm.eNetwork.ECL.ECLErr)
|
||||
? (com.ibm.eNetwork.ECL.ECLErr) err
|
||||
: new com.ibm.eNetwork.ECL.ECLErr(err != null ? err.getMessage() : "");
|
||||
for (com.ibm.eNetwork.ECL.ECLPSNotify n : psNotifies) {
|
||||
try {
|
||||
n.NotifyError(new com.ibm.eNetwork.ECL.ECLPS(this), ferr);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (com.ibm.eNetwork.ECL.event.ECLPSListener hl : hodPsListeners) {
|
||||
try {
|
||||
hl.PSNotifyError(new com.ibm.eNetwork.ECL.ECLPS(this), ferr);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyPSStop(int reason) {
|
||||
@@ -1061,6 +1464,16 @@ public class ECLPS implements ECLConstants {
|
||||
l.PSNotifyStop(this, reason);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (com.ibm.eNetwork.ECL.ECLPSNotify n : psNotifies) {
|
||||
try {
|
||||
n.NotifyStop(new com.ibm.eNetwork.ECL.ECLPS(this), reason);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
for (com.ibm.eNetwork.ECL.event.ECLPSListener hl : hodPsListeners) {
|
||||
try {
|
||||
hl.PSNotifyStop(new com.ibm.eNetwork.ECL.ECLPS(this), reason);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyPSUpdate(int startRow, int startCol, int endRow, int endCol, boolean full) {
|
||||
@@ -1128,6 +1541,25 @@ public class ECLPS implements ECLConstants {
|
||||
sendKeys(keys);
|
||||
}
|
||||
|
||||
public void sendKeys(String keys, int pos) {
|
||||
if (pos >= 1 && pos <= getSize()) {
|
||||
setCursorPos(pos - 1);
|
||||
}
|
||||
sendKeys(keys);
|
||||
}
|
||||
|
||||
public void SendKeys(String keys, int pos) {
|
||||
sendKeys(keys, pos);
|
||||
}
|
||||
|
||||
public void SendKeysNoAid(String string, boolean bl) {
|
||||
sendKeys(string);
|
||||
}
|
||||
|
||||
public void SendKeysNoAid(String string, int row, int col, boolean bl) {
|
||||
sendKeys(string, row, col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send keystrokes with configurable inter-character or inter-mnemonic delay.
|
||||
*/
|
||||
@@ -1154,6 +1586,17 @@ public class ECLPS implements ECLConstants {
|
||||
sendCharacters(keys, row, col, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int pos, int delayMs) {
|
||||
if (pos >= 1 && pos <= getSize()) {
|
||||
setCursorPos(pos - 1);
|
||||
}
|
||||
sendCharacters(keys, delayMs);
|
||||
}
|
||||
|
||||
public void SendCharacters(String keys, int pos, int delayMs) {
|
||||
sendCharacters(keys, pos, delayMs);
|
||||
}
|
||||
|
||||
public void sendCharacters(String keys, int delayMs) {
|
||||
if (keys == null || keys.isEmpty()) return;
|
||||
if (delayMs <= 0) {
|
||||
@@ -1161,37 +1604,36 @@ public class ECLPS implements ECLConstants {
|
||||
return;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
int len = keys.length();
|
||||
while (i < len) {
|
||||
if (keys.charAt(i) == '[') {
|
||||
if (i + 1 < len && keys.charAt(i + 1) == '[') {
|
||||
// Escaped bracket "[["
|
||||
sendKeys("[");
|
||||
i += 2;
|
||||
try {
|
||||
SendKeyTokenizer tokenizer = new SendKeyTokenizer(keys);
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
String token = tokenizer.nextToken();
|
||||
if (token == null) break;
|
||||
|
||||
if (token.length() == 1) {
|
||||
if (inputProcessor != null) inputProcessor.typeCharacter(token.charAt(0));
|
||||
if (delayMs > 0 && tokenizer.hasMoreTokens()) {
|
||||
Thread.sleep(delayMs);
|
||||
}
|
||||
} else if (token.startsWith("[") && token.endsWith("]")) {
|
||||
if (inputProcessor != null) inputProcessor.executeMnemonicToken(token);
|
||||
if (delayMs > 0 && tokenizer.hasMoreTokens()) {
|
||||
Thread.sleep(delayMs);
|
||||
}
|
||||
} else {
|
||||
int close = keys.indexOf(']', i);
|
||||
if (close > i) {
|
||||
String mnemonic = keys.substring(i, close + 1);
|
||||
sendKeys(mnemonic);
|
||||
i = close + 1;
|
||||
} else {
|
||||
sendKeys(keys.substring(i, i + 1));
|
||||
i++;
|
||||
for (int i = 0; i < token.length(); i++) {
|
||||
if (inputProcessor != null) inputProcessor.typeCharacter(token.charAt(i));
|
||||
if (delayMs > 0 && (i < token.length() - 1 || tokenizer.hasMoreTokens())) {
|
||||
Thread.sleep(delayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendKeys(keys.substring(i, i + 1));
|
||||
i++;
|
||||
}
|
||||
|
||||
if (delayMs > 0 && i < len) {
|
||||
try {
|
||||
Thread.sleep(delayMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (ECLErr e) {
|
||||
if (inputProcessor != null) {
|
||||
sendKeys(keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +196,14 @@ public class ECLPSEvent extends EventObject {
|
||||
return getECLPSUpdate();
|
||||
}
|
||||
|
||||
public ECLPSUpdate getPSUpdate() {
|
||||
return getECLPSUpdate();
|
||||
}
|
||||
|
||||
public ECLPSUpdate GetPSUpdate() {
|
||||
return getECLPSUpdate();
|
||||
}
|
||||
|
||||
public ECLPS getPS() {
|
||||
return (getSource() instanceof ECLPS) ? (ECLPS) getSource() : null;
|
||||
}
|
||||
|
||||
@@ -1,279 +1,487 @@
|
||||
package haus.nightmare.lib3270j.ecl;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import java.io.Serializable;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDAttrib;
|
||||
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDBlock;
|
||||
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDChecksum;
|
||||
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDCursor;
|
||||
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDFields;
|
||||
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDInputFields;
|
||||
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDOIA;
|
||||
import haus.nightmare.lib3270j.ecl.screenreco.ECLSDString;
|
||||
import haus.nightmare.lib3270j.ecl.screenreco.ECLScreenDescriptor;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching engine for IBM Host On-Demand ECL automation.
|
||||
* Screen descriptor composite matching engine for IBM Host On-Demand ECL automation.
|
||||
* Encapsulates criteria for matching host screens (strings, rectangular regions, cursor positions,
|
||||
* field counts, and OIA status).
|
||||
* field counts, OIA status, checksums, and attributes) and supports HACL XML schema serialization.
|
||||
*/
|
||||
public class ECLScreenDesc {
|
||||
public class ECLScreenDesc implements Serializable, Cloneable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final List<ScreenCondition> conditions = new ArrayList<>();
|
||||
|
||||
private interface ScreenCondition {
|
||||
boolean matches(ECLPS ps, ECLOIA oia);
|
||||
}
|
||||
private final List<ECLScreenDescriptor> descriptors = new CopyOnWriteArrayList<>();
|
||||
private String name = "";
|
||||
private boolean active = true;
|
||||
private boolean match = false;
|
||||
public boolean useOIAInhibitStatus = false;
|
||||
|
||||
public ECLScreenDesc() {}
|
||||
|
||||
/**
|
||||
* Clear all conditions from this descriptor.
|
||||
*/
|
||||
public synchronized void Clear() {
|
||||
conditions.clear();
|
||||
public ECLScreenDesc(String xml) {
|
||||
if (xml != null && !xml.trim().isEmpty()) {
|
||||
setXML(xml);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
Clear();
|
||||
}
|
||||
// ========== Descriptor Management ==========
|
||||
|
||||
/**
|
||||
* Add a condition that the given string must appear anywhere on the presentation space.
|
||||
*/
|
||||
public synchronized void AddString(String text) {
|
||||
public void AddDescriptor(ECLScreenDescriptor descriptor) {
|
||||
if (descriptor != null) {
|
||||
descriptors.add(descriptor);
|
||||
}
|
||||
}
|
||||
public void addDescriptor(ECLScreenDescriptor descriptor) { AddDescriptor(descriptor); }
|
||||
|
||||
public void RemoveDescriptor(ECLScreenDescriptor descriptor) {
|
||||
if (descriptor != null) {
|
||||
descriptors.remove(descriptor);
|
||||
}
|
||||
}
|
||||
public void removeDescriptor(ECLScreenDescriptor descriptor) { RemoveDescriptor(descriptor); }
|
||||
|
||||
public List<ECLScreenDescriptor> GetDescriptors() {
|
||||
return Collections.unmodifiableList(new ArrayList<>(descriptors));
|
||||
}
|
||||
public List<ECLScreenDescriptor> getDescriptors() { return GetDescriptors(); }
|
||||
|
||||
public ECLScreenDescriptor Get(int index) {
|
||||
return descriptors.get(index);
|
||||
}
|
||||
public ECLScreenDescriptor get(int index) { return Get(index); }
|
||||
|
||||
public int Size() { return descriptors.size(); }
|
||||
public int size() { return Size(); }
|
||||
public int getConditionCount() { return Size(); }
|
||||
|
||||
public void Clear() { descriptors.clear(); }
|
||||
public void clear() { Clear(); }
|
||||
|
||||
public String GetName() { return name; }
|
||||
public String getName() { return name; }
|
||||
public void SetName(String name) { this.name = (name != null) ? name : ""; }
|
||||
public void setName(String name) { SetName(name); }
|
||||
|
||||
public boolean IsActive() { return active; }
|
||||
public boolean isActive() { return active; }
|
||||
public void SetActive(boolean active) { this.active = active; }
|
||||
public void setActive(boolean active) { SetActive(active); }
|
||||
|
||||
public boolean IsMatch() { return match; }
|
||||
public boolean isMatch() { return match; }
|
||||
public void SetMatch(boolean match) { this.match = match; }
|
||||
public void setMatch(boolean match) { SetMatch(match); }
|
||||
|
||||
// ========== Programmatic Add* Methods ==========
|
||||
|
||||
public void AddString(String text) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
conditions.add((ps, oia) -> {
|
||||
if (ps == null) return false;
|
||||
return ps.searchString(text) >= 0;
|
||||
});
|
||||
AddDescriptor(new ECLSDString(text));
|
||||
}
|
||||
public void addString(String text) { AddString(text); }
|
||||
|
||||
public synchronized void addString(String text) {
|
||||
AddString(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a condition that text must appear at 1-based (row, col) position (case-insensitive by default).
|
||||
*/
|
||||
public synchronized void AddString(String text, int row, int col) {
|
||||
public void AddString(String text, int row, int col) {
|
||||
AddString(text, row, col, true);
|
||||
}
|
||||
public void addString(String text, int row, int col) { AddString(text, row, col); }
|
||||
|
||||
public synchronized void addString(String text, int row, int col) {
|
||||
AddString(text, row, col, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a condition that text must appear at (row, col) with case sensitivity control.
|
||||
* Note: 1-based indexing conforming to IBM ECL specification (row 1..rows, col 1..cols).
|
||||
*/
|
||||
public synchronized void AddString(String text, int row, int col, boolean caseSense) {
|
||||
public void AddString(String text, int row, int col, boolean caseSense) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
conditions.add((ps, oia) -> {
|
||||
if (ps == null) return false;
|
||||
int r = (row > 0) ? row - 1 : 0;
|
||||
int c = (col > 0) ? col - 1 : 0;
|
||||
String onScreen = ps.getString(r, c, text.length());
|
||||
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
|
||||
});
|
||||
AddDescriptor(new ECLSDString(text, row, col, caseSense));
|
||||
}
|
||||
public void addString(String text, int row, int col, boolean caseSense) { AddString(text, row, col, caseSense); }
|
||||
|
||||
public synchronized void addString(String text, int row, int col, boolean caseSense) {
|
||||
AddString(text, row, col, caseSense);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a condition that text must appear at a 1-based linear buffer position.
|
||||
*/
|
||||
public synchronized void AddString(String text, int pos, boolean caseSense) {
|
||||
public void AddString(String text, int pos, boolean caseSense) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
conditions.add((ps, oia) -> {
|
||||
if (ps == null) return false;
|
||||
int p0 = (pos > 0) ? pos - 1 : 0;
|
||||
String onScreen = ps.getString(p0, text.length());
|
||||
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
|
||||
});
|
||||
AddDescriptor(new ECLSDString(text, pos, caseSense));
|
||||
}
|
||||
public void addString(String text, int pos, boolean caseSense) { AddString(text, pos, caseSense); }
|
||||
|
||||
public synchronized void addString(String text, int pos, boolean caseSense) {
|
||||
AddString(text, pos, caseSense);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a condition that text must appear within a rectangular region.
|
||||
*/
|
||||
public synchronized void AddStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
|
||||
public void AddStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
|
||||
if (text == null || text.isEmpty()) return;
|
||||
conditions.add((ps, oia) -> {
|
||||
if (ps == null) return false;
|
||||
int sr = (sRow > 0) ? sRow - 1 : 0;
|
||||
int sc = (sCol > 0) ? sCol - 1 : 0;
|
||||
int er = (eRow > 0) ? eRow - 1 : 0;
|
||||
int ec = (eCol > 0) ? eCol - 1 : 0;
|
||||
String block = ps.copyString(sr, sc, er, ec);
|
||||
if (block == null) return false;
|
||||
return caseSense ? block.contains(text) : block.toLowerCase().contains(text.toLowerCase());
|
||||
});
|
||||
AddDescriptor(new ECLSDString(text, sRow, sCol, eRow, eCol, caseSense, false, false));
|
||||
}
|
||||
|
||||
public synchronized void addStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
|
||||
public void addStringInRect(String text, int sRow, int sCol, int eRow, int eCol, boolean caseSense) {
|
||||
AddStringInRect(text, sRow, sCol, eRow, eCol, caseSense);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a condition that the cursor must be located at 1-based (row, col).
|
||||
*/
|
||||
public synchronized void AddCursorPos(int row, int col) {
|
||||
conditions.add((ps, oia) -> {
|
||||
if (ps == null) return false;
|
||||
int r = (row > 0) ? row - 1 : 0;
|
||||
int c = (col > 0) ? col - 1 : 0;
|
||||
return ps.getCursorRow() == r && ps.getCursorCol() == c;
|
||||
});
|
||||
public void AddCursorPos(int row, int col) {
|
||||
AddDescriptor(new ECLSDCursor(row, col));
|
||||
}
|
||||
public void addCursorPos(int row, int col) { AddCursorPos(row, col); }
|
||||
|
||||
public synchronized void addCursorPos(int row, int col) {
|
||||
AddCursorPos(row, col);
|
||||
public void AddCursorPos(int pos) {
|
||||
AddDescriptor(new ECLSDCursor(pos));
|
||||
}
|
||||
public void addCursorPos(int pos) { AddCursorPos(pos); }
|
||||
|
||||
/**
|
||||
* Add a condition that the cursor must be located at 1-based linear buffer position.
|
||||
*/
|
||||
public synchronized void AddCursorPos(int pos) {
|
||||
conditions.add((ps, oia) -> {
|
||||
if (ps == null) return false;
|
||||
int p0 = (pos > 0) ? pos - 1 : 0;
|
||||
return ps.getCursorPos() == p0;
|
||||
});
|
||||
public void AddNumFields(int count) {
|
||||
AddDescriptor(new ECLSDFields(count));
|
||||
}
|
||||
public void addNumFields(int count) { AddNumFields(count); }
|
||||
|
||||
public synchronized void addCursorPos(int pos) {
|
||||
AddCursorPos(pos);
|
||||
public void AddNumInputFields(int count) {
|
||||
AddDescriptor(new ECLSDInputFields(count));
|
||||
}
|
||||
public void addNumInputFields(int count) { AddNumInputFields(count); }
|
||||
|
||||
/**
|
||||
* Add a condition on total field count on the formatted screen.
|
||||
*/
|
||||
public synchronized void AddNumFields(int count) {
|
||||
conditions.add((ps, oia) -> {
|
||||
if (ps == null) return false;
|
||||
return ps.getFieldList().getFieldCount() == count;
|
||||
});
|
||||
public void AddOIAStatus(int status) {
|
||||
ECLSDOIA oia = new ECLSDOIA();
|
||||
oia.setExplicitInhibitStatus(status);
|
||||
AddDescriptor(oia);
|
||||
}
|
||||
public void addOIAStatus(int status) { AddOIAStatus(status); }
|
||||
|
||||
public synchronized void addNumFields(int count) {
|
||||
AddNumFields(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a condition on number of unprotected input fields on the screen.
|
||||
*/
|
||||
public synchronized void AddNumInputFields(int count) {
|
||||
conditions.add((ps, oia) -> {
|
||||
if (ps == null) return false;
|
||||
int inputCount = 0;
|
||||
for (ECLField f : ps.getFieldList().getFields()) {
|
||||
if (!f.isProtected()) {
|
||||
inputCount++;
|
||||
}
|
||||
}
|
||||
return inputCount == count;
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized void addNumInputFields(int count) {
|
||||
AddNumInputFields(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a condition on OIA inhibit status.
|
||||
*/
|
||||
public synchronized void AddOIAStatus(int status) {
|
||||
conditions.add((ps, oia) -> {
|
||||
if (oia == null) return true;
|
||||
return oia.getInputInhibited() == status;
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized void addOIAStatus(int status) {
|
||||
public void AddOIAInhibitStatus(int status) {
|
||||
AddOIAStatus(status);
|
||||
}
|
||||
public void addOIAInhibitStatus(int status) { AddOIAInhibitStatus(status); }
|
||||
|
||||
public void AddAttrib(char c, int row, int col, int plane) {
|
||||
AddDescriptor(new ECLSDAttrib(c, row, col, plane));
|
||||
}
|
||||
public void addAttrib(char c, int row, int col, int plane) { AddAttrib(c, row, col, plane); }
|
||||
|
||||
public void AddAttrib(char c, int row, int col, int plane, boolean optional) {
|
||||
AddDescriptor(new ECLSDAttrib(c, row, col, plane, optional));
|
||||
}
|
||||
public void addAttrib(char c, int row, int col, int plane, boolean optional) {
|
||||
AddAttrib(c, row, col, plane, optional);
|
||||
}
|
||||
|
||||
public void AddFieldAttr(int pos, int attr) {
|
||||
AddDescriptor(new ECLScreenDescriptor() {
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Override
|
||||
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
|
||||
if (ps == null) return false;
|
||||
int p0 = (pos > 0) ? pos - 1 : 0;
|
||||
ECLField f = ps.getField(p0);
|
||||
if (f == null) return false;
|
||||
return ps.getFieldList().matchAttributes(f, attr);
|
||||
}
|
||||
@Override
|
||||
public String format(int indent, boolean full) {
|
||||
return "<attrib pos=\"" + pos + "\" attr=\"" + attr + "\" />";
|
||||
}
|
||||
});
|
||||
}
|
||||
public void addFieldAttr(int pos, int attr) { AddFieldAttr(pos, attr); }
|
||||
|
||||
public void AddFieldAttr(int row, int col, int attr) {
|
||||
AddDescriptor(new ECLScreenDescriptor() {
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Override
|
||||
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
|
||||
if (ps == null) return false;
|
||||
int r = (row > 0) ? row - 1 : 0;
|
||||
int c = (col > 0) ? col - 1 : 0;
|
||||
ECLField f = ps.getField(r, c);
|
||||
if (f == null) return false;
|
||||
return ps.getFieldList().matchAttributes(f, attr);
|
||||
}
|
||||
@Override
|
||||
public String format(int indent, boolean full) {
|
||||
return "<attrib row=\"" + row + "\" col=\"" + col + "\" attr=\"" + attr + "\" />";
|
||||
}
|
||||
});
|
||||
}
|
||||
public void addFieldAttr(int row, int col, int attr) { AddFieldAttr(row, col, attr); }
|
||||
|
||||
public void AddChecksum(int checksum) {
|
||||
AddDescriptor(new ECLSDChecksum(checksum));
|
||||
}
|
||||
public void addChecksum(int checksum) { AddChecksum(checksum); }
|
||||
|
||||
public void AddChecksum(int checksum, boolean optional) {
|
||||
AddDescriptor(new ECLSDChecksum(checksum, optional));
|
||||
}
|
||||
public void addChecksum(int checksum, boolean optional) { AddChecksum(checksum, optional); }
|
||||
|
||||
public void AddBlock(String[] strings, int sRow, int sCol, boolean caseSense) {
|
||||
AddDescriptor(new ECLSDBlock(strings, sRow, sCol, caseSense));
|
||||
}
|
||||
public void addBlock(String[] strings, int sRow, int sCol, boolean caseSense) {
|
||||
AddBlock(strings, sRow, sCol, caseSense);
|
||||
}
|
||||
|
||||
public void AddBlock(ECLSDBlock block) {
|
||||
AddDescriptor(block);
|
||||
}
|
||||
public void addBlock(ECLSDBlock block) { AddBlock(block); }
|
||||
|
||||
// ========== Matching Evaluation ==========
|
||||
|
||||
/**
|
||||
* Check if the current screen state matches all conditions in this descriptor.
|
||||
*/
|
||||
public synchronized boolean Matches(ECLPS ps, ECLOIA oia) {
|
||||
if (conditions.isEmpty()) {
|
||||
if (descriptors.isEmpty()) {
|
||||
this.match = true;
|
||||
return true;
|
||||
}
|
||||
for (ScreenCondition cond : conditions) {
|
||||
if (!cond.matches(ps, oia)) {
|
||||
return false;
|
||||
if (ps == null) {
|
||||
this.match = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean hasNonOptional = false;
|
||||
boolean allNonOptionalMatch = true;
|
||||
boolean hasOptional = false;
|
||||
boolean anyOptionalMatch = false;
|
||||
|
||||
for (ECLScreenDescriptor desc : descriptors) {
|
||||
boolean m = desc.matches(ps, oia);
|
||||
if (desc.isOptional()) {
|
||||
hasOptional = true;
|
||||
if (m) anyOptionalMatch = true;
|
||||
} else {
|
||||
hasNonOptional = true;
|
||||
if (!m) allNonOptionalMatch = false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
boolean res;
|
||||
if (hasNonOptional) {
|
||||
res = allNonOptionalMatch;
|
||||
} else if (hasOptional) {
|
||||
res = anyOptionalMatch;
|
||||
} else {
|
||||
res = true;
|
||||
}
|
||||
|
||||
this.match = res;
|
||||
return res;
|
||||
}
|
||||
|
||||
public boolean matches(ECLPS ps, ECLOIA oia) {
|
||||
return Matches(ps, oia);
|
||||
}
|
||||
public boolean matches(ECLPS ps, ECLOIA oia) { return Matches(ps, oia); }
|
||||
|
||||
public boolean Matches(ECLSession session) {
|
||||
if (session == null) return false;
|
||||
return Matches(session.GetPS(), session.GetOIA());
|
||||
}
|
||||
|
||||
public boolean matches(ECLSession session) {
|
||||
return Matches(session);
|
||||
}
|
||||
public boolean matches(ECLSession session) { return Matches(session); }
|
||||
|
||||
public boolean Matches(Telnet3270Client client) {
|
||||
if (client == null) return false;
|
||||
return Matches(client.getPS(), client.getOIA());
|
||||
}
|
||||
public boolean matches(Telnet3270Client client) { return Matches(client); }
|
||||
|
||||
public boolean matches(Telnet3270Client client) {
|
||||
return Matches(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a condition on field attributes at a 1-based buffer position.
|
||||
*/
|
||||
public synchronized void AddFieldAttr(int pos, int attr) {
|
||||
conditions.add((ps, oia) -> {
|
||||
if (ps == null) return false;
|
||||
int p0 = (pos > 0) ? pos - 1 : 0;
|
||||
ECLField f = ps.getField(p0);
|
||||
if (f == null) return false;
|
||||
return ps.getFieldList().matchAttributes(f, attr);
|
||||
});
|
||||
}
|
||||
public synchronized void addFieldAttr(int pos, int attr) {
|
||||
AddFieldAttr(pos, attr);
|
||||
}
|
||||
|
||||
public synchronized void AddFieldAttr(int row, int col, int attr) {
|
||||
conditions.add((ps, oia) -> {
|
||||
if (ps == null) return false;
|
||||
int r = (row > 0) ? row - 1 : 0;
|
||||
int c = (col > 0) ? col - 1 : 0;
|
||||
ECLField f = ps.getField(r, c);
|
||||
if (f == null) return false;
|
||||
return ps.getFieldList().matchAttributes(f, attr);
|
||||
});
|
||||
}
|
||||
public synchronized void addFieldAttr(int row, int col, int attr) {
|
||||
AddFieldAttr(row, col, attr);
|
||||
}
|
||||
|
||||
public boolean useOIAInhibitStatus = false;
|
||||
|
||||
/**
|
||||
* Check if a raw ScreenBuffer matches all criteria in this screen descriptor.
|
||||
*/
|
||||
public boolean Matches(ScreenBuffer buf) {
|
||||
if (buf == null) return false;
|
||||
return Matches(new ECLPS(buf, null, null), null);
|
||||
}
|
||||
public boolean matches(ScreenBuffer buf) {
|
||||
return Matches(buf);
|
||||
public boolean matches(ScreenBuffer buf) { return Matches(buf); }
|
||||
|
||||
// ========== XML Serialization & Parsing ==========
|
||||
|
||||
public String getXML() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<description>\n");
|
||||
for (ECLScreenDescriptor desc : descriptors) {
|
||||
sb.append(" ").append(desc.format(0, false)).append("\n");
|
||||
}
|
||||
sb.append("</description>");
|
||||
return sb.toString();
|
||||
}
|
||||
public String GetXML() { return getXML(); }
|
||||
|
||||
public void setXML(String xml) {
|
||||
Clear();
|
||||
if (xml == null || xml.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
try {
|
||||
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
} catch (Throwable ignored) {}
|
||||
try {
|
||||
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
|
||||
} catch (Throwable ignored) {}
|
||||
try {
|
||||
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document doc = builder.parse(new InputSource(new StringReader(xml.trim())));
|
||||
Element root = doc.getDocumentElement();
|
||||
|
||||
Element descElement = root;
|
||||
if (!root.getTagName().equalsIgnoreCase("description")) {
|
||||
NodeList list = root.getElementsByTagName("description");
|
||||
if (list.getLength() > 0) {
|
||||
descElement = (Element) list.item(0);
|
||||
}
|
||||
}
|
||||
|
||||
NodeList children = descElement.getChildNodes();
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node node = children.item(i);
|
||||
if (node.getNodeType() != Node.ELEMENT_NODE) continue;
|
||||
Element elem = (Element) node;
|
||||
String tag = elem.getTagName().toLowerCase();
|
||||
|
||||
boolean optional = "true".equalsIgnoreCase(elem.getAttribute("optional"));
|
||||
boolean invertmatch = "true".equalsIgnoreCase(elem.getAttribute("invertmatch"));
|
||||
String hostid = elem.getAttribute("hostid");
|
||||
|
||||
if (tag.equals("string")) {
|
||||
String value = elem.getAttribute("value");
|
||||
int row = parseIntAttr(elem, "row", 0);
|
||||
int col = parseIntAttr(elem, "col", 0);
|
||||
int erow = parseIntAttr(elem, "erow", -1);
|
||||
int ecol = parseIntAttr(elem, "ecol", -1);
|
||||
boolean casesense = "true".equalsIgnoreCase(elem.getAttribute("casesense"));
|
||||
boolean wrap = "true".equalsIgnoreCase(elem.getAttribute("wrap"));
|
||||
|
||||
ECLSDString s;
|
||||
if (row > 0 && col > 0) {
|
||||
if (erow > 0 || ecol > 0) {
|
||||
s = new ECLSDString(value, row, col, erow, ecol, casesense, optional, wrap);
|
||||
} else {
|
||||
s = new ECLSDString(value, row, col, casesense, optional);
|
||||
}
|
||||
} else {
|
||||
s = new ECLSDString(value);
|
||||
s.setCaseSense(casesense);
|
||||
s.setOptional(optional);
|
||||
}
|
||||
s.setInvertMatch(invertmatch);
|
||||
s.setHostid(hostid);
|
||||
AddDescriptor(s);
|
||||
} else if (tag.equals("cursor")) {
|
||||
int row = parseIntAttr(elem, "row", 1);
|
||||
int col = parseIntAttr(elem, "col", 1);
|
||||
int pos = parseIntAttr(elem, "position", -1);
|
||||
ECLSDCursor c;
|
||||
if (pos > 0) {
|
||||
c = new ECLSDCursor(pos, optional);
|
||||
} else {
|
||||
c = new ECLSDCursor(row, col, optional);
|
||||
}
|
||||
c.setInvertMatch(invertmatch);
|
||||
c.setHostid(hostid);
|
||||
AddDescriptor(c);
|
||||
} else if (tag.equals("numfields") || tag.equals("fields")) {
|
||||
int num = parseIntAttr(elem, elem.hasAttribute("number") ? "number" : "value", 0);
|
||||
ECLSDFields f = new ECLSDFields(num, optional);
|
||||
f.setInvertMatch(invertmatch);
|
||||
f.setHostid(hostid);
|
||||
AddDescriptor(f);
|
||||
} else if (tag.equals("numinputfields") || tag.equals("inputfields")) {
|
||||
int num = parseIntAttr(elem, elem.hasAttribute("number") ? "number" : "value", 0);
|
||||
ECLSDInputFields inf = new ECLSDInputFields(num, optional);
|
||||
inf.setInvertMatch(invertmatch);
|
||||
inf.setHostid(hostid);
|
||||
AddDescriptor(inf);
|
||||
} else if (tag.equals("oia")) {
|
||||
String status = elem.hasAttribute("status") ? elem.getAttribute("status") :
|
||||
(elem.hasAttribute("input") ? elem.getAttribute("input") : "NOTINHIBITED");
|
||||
ECLSDOIA oia = new ECLSDOIA(status, optional);
|
||||
oia.setInvertMatch(invertmatch);
|
||||
oia.setHostid(hostid);
|
||||
AddDescriptor(oia);
|
||||
} else if (tag.equals("checksum")) {
|
||||
int value = parseIntAttr(elem, "value", 0);
|
||||
ECLSDChecksum cs = new ECLSDChecksum(value, optional);
|
||||
cs.setInvertMatch(invertmatch);
|
||||
cs.setHostid(hostid);
|
||||
AddDescriptor(cs);
|
||||
} else if (tag.equals("attrib")) {
|
||||
String valStr = elem.getAttribute("value");
|
||||
char val = ECLSDAttrib.StringToAttrib(valStr);
|
||||
int row = parseIntAttr(elem, "row", 1);
|
||||
int col = parseIntAttr(elem, "col", 1);
|
||||
int plane = ECLSDAttrib.StringToPlane(elem.getAttribute("plane"));
|
||||
ECLSDAttrib attr = new ECLSDAttrib(val, row, col, plane, optional);
|
||||
attr.setInvertMatch(invertmatch);
|
||||
attr.setHostid(hostid);
|
||||
AddDescriptor(attr);
|
||||
} else if (tag.equals("block")) {
|
||||
int row = parseIntAttr(elem, "row", 1);
|
||||
int col = parseIntAttr(elem, "col", 1);
|
||||
int erow = parseIntAttr(elem, "erow", -1);
|
||||
int ecol = parseIntAttr(elem, "ecol", -1);
|
||||
boolean casesense = "true".equalsIgnoreCase(elem.getAttribute("casesense"));
|
||||
|
||||
ECLSDBlock blk;
|
||||
if (erow > 0 || ecol > 0) {
|
||||
blk = new ECLSDBlock(row, col, erow, ecol, casesense, optional);
|
||||
} else {
|
||||
blk = new ECLSDBlock(row, col, casesense, optional);
|
||||
}
|
||||
blk.setInvertMatch(invertmatch);
|
||||
blk.setHostid(hostid);
|
||||
|
||||
NodeList subList = elem.getElementsByTagName("string");
|
||||
for (int j = 0; j < subList.getLength(); j++) {
|
||||
Element strElem = (Element) subList.item(j);
|
||||
blk.addString(strElem.getAttribute("value"));
|
||||
}
|
||||
AddDescriptor(blk);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse ECLScreenDesc XML: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
public void SetXML(String xml) { setXML(xml); }
|
||||
|
||||
private static int parseIntAttr(Element elem, String attrName, int defaultVal) {
|
||||
if (!elem.hasAttribute(attrName)) return defaultVal;
|
||||
try {
|
||||
return Integer.parseInt(elem.getAttribute(attrName).trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized int getConditionCount() {
|
||||
return conditions.size();
|
||||
@Override
|
||||
public Object clone() {
|
||||
try {
|
||||
ECLScreenDesc copy = (ECLScreenDesc) super.clone();
|
||||
copy.descriptors.clear();
|
||||
for (ECLScreenDescriptor d : descriptors) {
|
||||
copy.descriptors.add((ECLScreenDescriptor) d.clone());
|
||||
}
|
||||
return copy;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
ECLScreenDesc copy = new ECLScreenDesc();
|
||||
copy.SetName(this.name);
|
||||
copy.SetActive(this.active);
|
||||
for (ECLScreenDescriptor d : descriptors) {
|
||||
copy.AddDescriptor((ECLScreenDescriptor) d.clone());
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getXML();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,21 +11,28 @@ public class ECLScreenReco {
|
||||
|
||||
private final ECLSession session;
|
||||
private final ECLPS ps;
|
||||
private final ECLOIA oia;
|
||||
private final Map<ECLScreenDesc, ECLScreenNotify> notifyMap = new ConcurrentHashMap<>();
|
||||
private final Map<ECLScreenDesc, ECLPSListener> listenerMap = new ConcurrentHashMap<>();
|
||||
private final ECLPSListener internalListener;
|
||||
private final ECLOIANotify internalOiaListener;
|
||||
|
||||
public ECLScreenReco() {
|
||||
this((ECLPS) null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLSession session) {
|
||||
this(session != null ? session.GetPS() : null);
|
||||
this(session != null ? session.GetPS() : null, session != null ? session.GetOIA() : null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLPS ps) {
|
||||
this(ps, (ECLOIA) null);
|
||||
}
|
||||
|
||||
public ECLScreenReco(ECLPS ps, ECLOIA oia) {
|
||||
this.ps = ps;
|
||||
this.session = (ps != null) ? ps.GetParent() : null;
|
||||
this.oia = (oia != null) ? oia : ((session != null) ? session.GetOIA() : null);
|
||||
|
||||
this.internalListener = new ECLPSListener() {
|
||||
@Override
|
||||
@@ -34,9 +41,14 @@ public class ECLScreenReco {
|
||||
}
|
||||
};
|
||||
|
||||
this.internalOiaListener = event -> checkAllScreens();
|
||||
|
||||
if (this.ps != null) {
|
||||
this.ps.RegisterPSEvent(this.internalListener);
|
||||
}
|
||||
if (this.oia != null) {
|
||||
this.oia.RegisterOIAEvent(this.internalOiaListener);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Static Recognition Helpers ==========
|
||||
@@ -95,6 +107,16 @@ public class ECLScreenReco {
|
||||
UnregisterScreen(desc, notify);
|
||||
}
|
||||
|
||||
public void UnregisterScreen(ECLScreenDesc desc) {
|
||||
if (desc != null) {
|
||||
notifyMap.remove(desc);
|
||||
listenerMap.remove(desc);
|
||||
}
|
||||
}
|
||||
public void unregisterScreen(ECLScreenDesc desc) {
|
||||
UnregisterScreen(desc);
|
||||
}
|
||||
|
||||
public void RegisterScreen(ECLScreenDesc desc, ECLPSListener listener) {
|
||||
if (desc != null && listener != null) {
|
||||
listenerMap.put(desc, listener);
|
||||
@@ -120,7 +142,10 @@ public class ECLScreenReco {
|
||||
}
|
||||
|
||||
public boolean IsMatch(ECLScreenDesc desc) {
|
||||
return IsMatch(ps, desc);
|
||||
if (desc == null) return true;
|
||||
if (ps == null) return false;
|
||||
ECLOIA activeOia = (oia != null) ? oia : ((ps.GetParent() != null) ? ps.GetParent().GetOIA() : null);
|
||||
return desc.Matches(ps, activeOia);
|
||||
}
|
||||
public boolean isMatch(ECLScreenDesc desc) {
|
||||
return IsMatch(desc);
|
||||
@@ -142,6 +167,9 @@ public class ECLScreenReco {
|
||||
if (ps != null && internalListener != null) {
|
||||
ps.UnregisterPSEvent(internalListener);
|
||||
}
|
||||
if (oia != null && internalOiaListener != null) {
|
||||
oia.UnregisterOIAEvent(internalOiaListener);
|
||||
}
|
||||
notifyMap.clear();
|
||||
listenerMap.clear();
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ public class ECLSession {
|
||||
public static final String BLOCK_PASTE = "blockPaste";
|
||||
public static final String PASTE_FIELD_WRAP = "pasteFieldWrap";
|
||||
public static final String PASTE_LINE_WRAP = "pasteLineWrap";
|
||||
public static final String PASTE_WORD_BREAK = "pasteWordBreak";
|
||||
public static final String ENTRYASSIST_DOCMODE = "EntryAssist_DOCmode";
|
||||
public static final String ENTRYASSIST_DOCWORDWRAP = "EntryAssist_DOCwordWrap";
|
||||
public static final String ENTRYASSIST_STARTCOL = "EntryAssist_startCol";
|
||||
|
||||
@@ -456,11 +456,26 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
||||
this.bytesTransferred = 0;
|
||||
this.totalBytes = (config.isSend() && localFile.exists()) ? localFile.length() : 0;
|
||||
|
||||
// Auto-detect DBCS from translator codepage if not explicitly set
|
||||
if (!config.isDbcs() && translator != null && translator.getCodePage() != null && translator.getCodePage().isDBCS()) {
|
||||
config.setDbcs(true);
|
||||
}
|
||||
|
||||
// Wait for keyboard unlock before typing command
|
||||
waitForKeyboardUnlock(2000);
|
||||
|
||||
// If clear is requested, send Clear AID first to wipe intermediate screen status
|
||||
if (config.isClear() && input != null && !input.isNvtMode()) {
|
||||
input.sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_CLEAR);
|
||||
try { Thread.sleep(150); } catch (InterruptedException ignored) {}
|
||||
waitForKeyboardUnlock(2000);
|
||||
}
|
||||
|
||||
String command = config.buildCommand();
|
||||
log.info("ECLXfer: initiating transfer: " + command);
|
||||
|
||||
int capacity = input.kybdPrime();
|
||||
if (capacity < command.length()) {
|
||||
int capacity = input != null ? input.kybdPrime() : 0;
|
||||
if (input != null && capacity < command.length()) {
|
||||
String err = "Input field capacity insufficient for command (" + capacity + " chars)";
|
||||
fireEvent(ECLXferEvent.XFER_ABORTED, FTConstants.ECL_ERR_XFER_ABORT, err);
|
||||
return FTConstants.ECL_ERR_XFER_ABORT;
|
||||
@@ -469,10 +484,25 @@ public class ECLXfer implements FTCut.FTCutListener, FTDft.FTDftListener {
|
||||
setState(FTState.AWAIT_ACK);
|
||||
fireEvent(ECLXferEvent.XFER_STARTED, 0, "Transfer initiated");
|
||||
|
||||
input.emulateInput(command + "\n");
|
||||
if (input != null) {
|
||||
input.emulateInput(command + "\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void waitForKeyboardUnlock(long timeoutMs) {
|
||||
if (input == null) return;
|
||||
long start = System.currentTimeMillis();
|
||||
while (input.isKeyboardLocked() && (System.currentTimeMillis() - start) < timeoutMs) {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseOptionsIntoConfig(FTConfig config, String options) {
|
||||
if (options == null || options.trim().isEmpty()) return;
|
||||
config.parseOptions(options);
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package haus.nightmare.lib3270j.ecl.screenreco;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching field or presentation plane attributes matching IBM Host On-Demand ECLSDAttrib.
|
||||
*/
|
||||
public class ECLSDAttrib extends ECLScreenDescriptor {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final int TEXT_PLANE = 1;
|
||||
public static final int COLOR_PLANE = 2;
|
||||
public static final int DBCS_PLANE = 4;
|
||||
public static final int GRID_PLANE = 8;
|
||||
public static final int FIELD_PLANE = 16;
|
||||
public static final int EXFIELD_PLANE = 32;
|
||||
|
||||
private char attrib = 0x00;
|
||||
private int row = 1;
|
||||
private int col = 1;
|
||||
private int plane = FIELD_PLANE;
|
||||
|
||||
public ECLSDAttrib() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDAttrib(char attrib, int row, int col, int plane) {
|
||||
this(attrib, row, col, plane, false);
|
||||
}
|
||||
|
||||
public ECLSDAttrib(char attrib, int row, int col, int plane, boolean optional) {
|
||||
super(optional);
|
||||
this.attrib = attrib;
|
||||
this.row = row;
|
||||
this.col = col;
|
||||
this.plane = plane;
|
||||
}
|
||||
|
||||
public char getAttrib() { return attrib; }
|
||||
public char GetAttrib() { return attrib; }
|
||||
public void setAttrib(char attrib) { this.attrib = attrib; }
|
||||
public void SetAttrib(char attrib) { setAttrib(attrib); }
|
||||
|
||||
public int getRow() { return row; }
|
||||
public int GetRow() { return row; }
|
||||
public void setRow(int row) { this.row = row; }
|
||||
public void SetRow(int row) { setRow(row); }
|
||||
|
||||
public int getCol() { return col; }
|
||||
public int GetCol() { return col; }
|
||||
public void setCol(int col) { this.col = col; }
|
||||
public void SetCol(int col) { setCol(col); }
|
||||
|
||||
public int getPlane() { return plane; }
|
||||
public int GetPlane() { return plane; }
|
||||
public void setPlane(int plane) { this.plane = plane; }
|
||||
public void SetPlane(int plane) { setPlane(plane); }
|
||||
|
||||
public static String PlaneToString(int plane) {
|
||||
switch (plane) {
|
||||
case TEXT_PLANE: return "TEXT_PLANE";
|
||||
case COLOR_PLANE: return "COLOR_PLANE";
|
||||
case DBCS_PLANE: return "DBCS_PLANE";
|
||||
case GRID_PLANE: return "GRID_PLANE";
|
||||
case FIELD_PLANE: return "FIELD_PLANE";
|
||||
case EXFIELD_PLANE: return "EXFIELD_PLANE";
|
||||
default: return "FIELD_PLANE";
|
||||
}
|
||||
}
|
||||
|
||||
public static int StringToPlane(String str) {
|
||||
if (str == null) return FIELD_PLANE;
|
||||
if (str.equalsIgnoreCase("TEXT_PLANE")) return TEXT_PLANE;
|
||||
if (str.equalsIgnoreCase("COLOR_PLANE")) return COLOR_PLANE;
|
||||
if (str.equalsIgnoreCase("DBCS_PLANE")) return DBCS_PLANE;
|
||||
if (str.equalsIgnoreCase("GRID_PLANE")) return GRID_PLANE;
|
||||
if (str.equalsIgnoreCase("FIELD_PLANE")) return FIELD_PLANE;
|
||||
if (str.equalsIgnoreCase("EXFIELD_PLANE")) return EXFIELD_PLANE;
|
||||
try {
|
||||
return Integer.parseInt(str);
|
||||
} catch (NumberFormatException e) {
|
||||
return FIELD_PLANE;
|
||||
}
|
||||
}
|
||||
|
||||
public static String AttribToString(char c) {
|
||||
return "0x" + Integer.toHexString(c & 0xFFFF);
|
||||
}
|
||||
|
||||
public static char StringToAttrib(String str) {
|
||||
if (str == null || str.isEmpty()) return 0;
|
||||
str = str.trim();
|
||||
int xIdx = str.toLowerCase().indexOf('x');
|
||||
if (xIdx != -1) {
|
||||
return (char) Integer.parseInt(str.substring(xIdx + 1), 16);
|
||||
}
|
||||
return (char) Integer.parseInt(str);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
|
||||
if (ps == null) return false;
|
||||
int totalRows = ps.getRows();
|
||||
int totalCols = ps.getCols();
|
||||
|
||||
if (row < 1 || row > totalRows || col < 1 || col > totalCols) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int addr = (row - 1) * totalCols + (col - 1);
|
||||
char[] buf = new char[1];
|
||||
ps.getPlane(plane, buf, addr, 1);
|
||||
return buf[0] == attrib;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent, boolean full) {
|
||||
StringBuilder sb = new StringBuilder("<attrib ");
|
||||
sb.append("value=\"").append(AttribToString(attrib)).append("\" ");
|
||||
sb.append("row=\"").append(row).append("\" ");
|
||||
sb.append("col=\"").append(col).append("\" ");
|
||||
sb.append("plane=\"").append(PlaneToString(plane)).append("\" ");
|
||||
return formatAttributes(sb.toString(), full);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package haus.nightmare.lib3270j.ecl.screenreco;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching a rectangular block of text matching IBM Host On-Demand ECLSDBlock.
|
||||
*/
|
||||
public class ECLSDBlock extends ECLScreenDescriptor {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final int BLOCK_ABSOLUTE = 1;
|
||||
public static final int BLOCK_INRECT = 2;
|
||||
|
||||
private int blockType = BLOCK_INRECT;
|
||||
private int srow = 1;
|
||||
private int scol = 1;
|
||||
private int erow = -1;
|
||||
private int ecol = -1;
|
||||
private boolean caseSense = false;
|
||||
private ECLSDBlockString[] strings = new ECLSDBlockString[0];
|
||||
|
||||
public ECLSDBlock() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDBlock(int srow, int scol, boolean caseSense, boolean optional) {
|
||||
super(optional);
|
||||
this.blockType = BLOCK_ABSOLUTE;
|
||||
this.srow = srow;
|
||||
this.scol = scol;
|
||||
this.caseSense = caseSense;
|
||||
}
|
||||
|
||||
public ECLSDBlock(int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
|
||||
super(optional);
|
||||
this.blockType = BLOCK_INRECT;
|
||||
this.srow = srow;
|
||||
this.scol = scol;
|
||||
this.erow = erow;
|
||||
this.ecol = ecol;
|
||||
this.caseSense = caseSense;
|
||||
}
|
||||
|
||||
public ECLSDBlock(String[] textLines, int srow, int scol, boolean caseSense) {
|
||||
super(false);
|
||||
this.blockType = BLOCK_ABSOLUTE;
|
||||
this.srow = srow;
|
||||
this.scol = scol;
|
||||
this.caseSense = caseSense;
|
||||
setStrings(textLines);
|
||||
}
|
||||
|
||||
public int getBlockType() { return blockType; }
|
||||
public int GetBlockType() { return blockType; }
|
||||
public void setBlockType(int blockType) { this.blockType = blockType; }
|
||||
public void SetBlockType(int blockType) { setBlockType(blockType); }
|
||||
|
||||
public int getSRow() { return srow; }
|
||||
public int GetSRow() { return srow; }
|
||||
public void setSRow(int srow) { this.srow = srow; }
|
||||
public void SetSRow(int srow) { setSRow(srow); }
|
||||
|
||||
public int getSCol() { return scol; }
|
||||
public int GetSCol() { return scol; }
|
||||
public void setSCol(int scol) { this.scol = scol; }
|
||||
public void SetSCol(int scol) { setSCol(scol); }
|
||||
|
||||
public int getERow() { return erow; }
|
||||
public int GetERow() { return erow; }
|
||||
public void setERow(int erow) { this.erow = erow; }
|
||||
public void SetERow(int erow) { setERow(erow); }
|
||||
|
||||
public int getECol() { return ecol; }
|
||||
public int GetECol() { return ecol; }
|
||||
public void setECol(int ecol) { this.ecol = ecol; }
|
||||
public void SetECol(int ecol) { setECol(ecol); }
|
||||
|
||||
public boolean isCaseSense() { return caseSense; }
|
||||
public boolean IsCaseSense() { return caseSense; }
|
||||
public void setCaseSense(boolean caseSense) { this.caseSense = caseSense; }
|
||||
public void SetCaseSense(boolean caseSense) { setCaseSense(caseSense); }
|
||||
|
||||
public String[] getStrings() {
|
||||
String[] arr = new String[strings.length];
|
||||
for (int i = 0; i < strings.length; i++) {
|
||||
arr[i] = (strings[i] != null) ? strings[i].getText() : "";
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
public String[] GetStrings() { return getStrings(); }
|
||||
|
||||
public void setStrings(String[] textLines) {
|
||||
if (textLines == null) {
|
||||
this.strings = new ECLSDBlockString[0];
|
||||
return;
|
||||
}
|
||||
this.strings = new ECLSDBlockString[textLines.length];
|
||||
for (int i = 0; i < textLines.length; i++) {
|
||||
this.strings[i] = new ECLSDBlockString(textLines[i]);
|
||||
}
|
||||
}
|
||||
public void SetStrings(String[] textLines) { setStrings(textLines); }
|
||||
|
||||
public void setBlockStrings(ECLSDBlockString[] blockStrings) {
|
||||
this.strings = (blockStrings != null) ? blockStrings : new ECLSDBlockString[0];
|
||||
}
|
||||
|
||||
public void addString(String text) {
|
||||
ECLSDBlockString[] newArr = new ECLSDBlockString[strings.length + 1];
|
||||
System.arraycopy(strings, 0, newArr, 0, strings.length);
|
||||
newArr[strings.length] = new ECLSDBlockString(text);
|
||||
this.strings = newArr;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
|
||||
if (ps == null) return false;
|
||||
if (strings == null || strings.length == 0) return true;
|
||||
|
||||
int totalRows = ps.getRows();
|
||||
int totalCols = ps.getCols();
|
||||
String[] lines = getStrings();
|
||||
|
||||
// 1. Absolute matching at (srow, scol)
|
||||
if (blockType == BLOCK_ABSOLUTE || (erow <= 0 && ecol <= 0 && srow > 0 && scol > 0)) {
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
int r0 = srow + i - 1;
|
||||
int c0 = scol - 1;
|
||||
if (r0 < 0 || r0 >= totalRows || c0 < 0 || c0 >= totalCols) return false;
|
||||
String target = lines[i];
|
||||
String onScreen = ps.getString(r0, c0, target.length());
|
||||
if (onScreen == null) return false;
|
||||
boolean eq = caseSense ? target.equals(onScreen) : target.equalsIgnoreCase(onScreen);
|
||||
if (!eq) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Rectangular region matching
|
||||
int sr = (srow > 0) ? srow : 1;
|
||||
int sc = (scol > 0) ? scol : 1;
|
||||
int er = (erow > 0) ? Math.min(erow, totalRows) : totalRows;
|
||||
int ec = (ecol > 0) ? Math.min(ecol, totalCols) : totalCols;
|
||||
|
||||
if (sr > er || sc > ec) return false;
|
||||
int numLines = lines.length;
|
||||
if (er - sr + 1 < numLines) return false;
|
||||
|
||||
int rectWidth = ec - sc + 1;
|
||||
List<String> screenLines = new ArrayList<>();
|
||||
for (int r = sr; r <= er; r++) {
|
||||
String s = ps.getString(r - 1, sc - 1, rectWidth);
|
||||
screenLines.add(s != null ? s : "");
|
||||
}
|
||||
|
||||
String firstTarget = caseSense ? lines[0] : lines[0].toLowerCase();
|
||||
for (int i = 0; i <= screenLines.size() - numLines; i++) {
|
||||
String rowStr = caseSense ? screenLines.get(i) : screenLines.get(i).toLowerCase();
|
||||
int matchIdx = rowStr.indexOf(firstTarget);
|
||||
while (matchIdx != -1) {
|
||||
boolean allMatch = true;
|
||||
for (int j = 1; j < numLines; j++) {
|
||||
String nextRowStr = caseSense ? screenLines.get(i + j) : screenLines.get(i + j).toLowerCase();
|
||||
String nextTarget = caseSense ? lines[j] : lines[j].toLowerCase();
|
||||
if (matchIdx + nextTarget.length() > nextRowStr.length() ||
|
||||
!nextRowStr.startsWith(nextTarget, matchIdx)) {
|
||||
allMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allMatch) {
|
||||
return true;
|
||||
}
|
||||
matchIdx = rowStr.indexOf(firstTarget, matchIdx + 1);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent, boolean full) {
|
||||
StringBuilder sb = new StringBuilder("<block ");
|
||||
if (srow > 0 || full) sb.append("row=\"").append(srow).append("\" ");
|
||||
if (scol > 0 || full) sb.append("col=\"").append(scol).append("\" ");
|
||||
if (blockType == BLOCK_INRECT && (erow > 0 || ecol > 0 || full)) {
|
||||
if (erow > 0 || full) sb.append("erow=\"").append(erow).append("\" ");
|
||||
if (ecol > 0 || full) sb.append("ecol=\"").append(ecol).append("\" ");
|
||||
}
|
||||
if (caseSense || full) sb.append("casesense=\"").append(caseSense).append("\" ");
|
||||
if (optional || full) sb.append("optional=\"").append(optional).append("\" ");
|
||||
if (invert || full) sb.append("invertmatch=\"").append(invert).append("\" ");
|
||||
if (hostid != null && !hostid.isEmpty()) sb.append("hostid=\"").append(escapeChars(hostid)).append("\" ");
|
||||
sb.append(">\n");
|
||||
|
||||
for (ECLSDBlockString s : strings) {
|
||||
sb.append(" ").append(s.format(indent + 1, full)).append("\n");
|
||||
}
|
||||
sb.append("</block>");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package haus.nightmare.lib3270j.ecl.screenreco;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Encapsulates an individual string line within an ECLSDBlock matching IBM Host On-Demand.
|
||||
*/
|
||||
public class ECLSDBlockString implements Serializable, Cloneable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String text = "";
|
||||
|
||||
public ECLSDBlockString() {}
|
||||
|
||||
public ECLSDBlockString(String text) {
|
||||
this.text = (text != null) ? text : "";
|
||||
}
|
||||
|
||||
public String getText() { return text; }
|
||||
public String GetText() { return text; }
|
||||
public String GetString() { return text; }
|
||||
public String getString() { return text; }
|
||||
public void setText(String text) { this.text = (text != null) ? text : ""; }
|
||||
public void SetText(String text) { setText(text); }
|
||||
public void SetString(String text) { setText(text); }
|
||||
public void setString(String text) { setText(text); }
|
||||
|
||||
public String format(int indent, boolean full) {
|
||||
return "<string value=\"" + ECLScreenDescriptor.escapeChars(text) + "\" />";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() {
|
||||
try {
|
||||
return super.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package haus.nightmare.lib3270j.ecl.screenreco;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching screen buffer checksum matching IBM Host On-Demand ECLSDChecksum.
|
||||
*/
|
||||
public class ECLSDChecksum extends ECLScreenDescriptor {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int value = 0;
|
||||
|
||||
public ECLSDChecksum() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDChecksum(int value) {
|
||||
this(value, false);
|
||||
}
|
||||
|
||||
public ECLSDChecksum(int value, boolean optional) {
|
||||
super(optional);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() { return value; }
|
||||
public int GetValue() { return value; }
|
||||
public void setValue(int value) { this.value = value; }
|
||||
public void SetValue(int value) { setValue(value); }
|
||||
|
||||
public static int CalculateChecksum(ECLFieldList fieldList, int psSize) {
|
||||
int sum = 0;
|
||||
if (fieldList != null && fieldList.getFieldCount() > 0) {
|
||||
int count = fieldList.getFieldCount();
|
||||
sum = 17;
|
||||
ECLField current = null;
|
||||
for (int i = 0; i < count - 1; i++) {
|
||||
if (i == 0) {
|
||||
current = fieldList.getFirstField();
|
||||
}
|
||||
ECLField next = fieldList.getNextField(current);
|
||||
if (current != null && next != null) {
|
||||
int delta = next.getStart() - current.getStart() - 1;
|
||||
sum = sum * 37 + delta;
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
ECLField last = (current == null) ? fieldList.getFirstField() : current;
|
||||
int endDelta = (last != null) ? (psSize - last.getStart()) : 0;
|
||||
sum = sum * 37 + endDelta;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
|
||||
if (ps == null) return false;
|
||||
int calc = CalculateChecksum(ps.getFieldList(), ps.getSize());
|
||||
return calc == value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent, boolean full) {
|
||||
StringBuilder sb = new StringBuilder("<checksum ");
|
||||
sb.append("value=\"").append(value).append("\" ");
|
||||
return formatAttributes(sb.toString(), full);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package haus.nightmare.lib3270j.ecl.screenreco;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching cursor position matching IBM Host On-Demand ECLSDCursor.
|
||||
*/
|
||||
public class ECLSDCursor extends ECLScreenDescriptor {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int row = 1;
|
||||
private int col = 1;
|
||||
private int pos = -1;
|
||||
|
||||
public ECLSDCursor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDCursor(int pos) {
|
||||
this(pos, false);
|
||||
}
|
||||
|
||||
public ECLSDCursor(int pos, boolean optional) {
|
||||
super(optional);
|
||||
this.pos = pos;
|
||||
}
|
||||
|
||||
public ECLSDCursor(int row, int col) {
|
||||
this(row, col, false);
|
||||
}
|
||||
|
||||
public ECLSDCursor(int row, int col, boolean optional) {
|
||||
super(optional);
|
||||
this.row = row;
|
||||
this.col = col;
|
||||
}
|
||||
|
||||
public int getPos() { return pos; }
|
||||
public int GetPos() { return pos; }
|
||||
public void setPos(int pos) { this.pos = pos; }
|
||||
public void SetPos(int pos) { setPos(pos); }
|
||||
|
||||
public int getRow() { return row; }
|
||||
public int GetRow() { return row; }
|
||||
public void setRow(int row) { this.row = row; }
|
||||
public void SetRow(int row) { setRow(row); }
|
||||
|
||||
public int getCol() { return col; }
|
||||
public int GetCol() { return col; }
|
||||
public void setCol(int col) { this.col = col; }
|
||||
public void SetCol(int col) { setCol(col); }
|
||||
|
||||
@Override
|
||||
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
|
||||
if (ps == null) return false;
|
||||
if (pos > 0) {
|
||||
return (ps.getCursorPos() + 1) == pos;
|
||||
}
|
||||
// In ECLPS, getCursorRow() and getCursorCol() return 0-based indices.
|
||||
int curRow = ps.getCursorRow() + 1;
|
||||
int curCol = ps.getCursorCol() + 1;
|
||||
return (curRow == row && curCol == col);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent, boolean full) {
|
||||
StringBuilder sb = new StringBuilder("<cursor ");
|
||||
if (pos > 0) {
|
||||
sb.append("position=\"").append(pos).append("\" ");
|
||||
} else {
|
||||
sb.append("row=\"").append(row).append("\" ");
|
||||
sb.append("col=\"").append(col).append("\" ");
|
||||
}
|
||||
return formatAttributes(sb.toString(), full);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package haus.nightmare.lib3270j.ecl.screenreco;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching total number of fields matching IBM Host On-Demand ECLSDFields.
|
||||
*/
|
||||
public class ECLSDFields extends ECLScreenDescriptor {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int num = 0;
|
||||
|
||||
public ECLSDFields() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDFields(int num) {
|
||||
this(num, false);
|
||||
}
|
||||
|
||||
public ECLSDFields(int num, boolean optional) {
|
||||
super(optional);
|
||||
this.num = num;
|
||||
}
|
||||
|
||||
public int getNum() { return num; }
|
||||
public int GetNum() { return num; }
|
||||
public void setNum(int num) { this.num = num; }
|
||||
public void SetNum(int num) { setNum(num); }
|
||||
|
||||
@Override
|
||||
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
|
||||
if (ps == null) return false;
|
||||
ECLFieldList fl = ps.getFieldList();
|
||||
if (fl == null) return false;
|
||||
return fl.getFieldCount() == num;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent, boolean full) {
|
||||
StringBuilder sb = new StringBuilder("<numfields ");
|
||||
sb.append("number=\"").append(num).append("\" ");
|
||||
return formatAttributes(sb.toString(), full);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package haus.nightmare.lib3270j.ecl.screenreco;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLField;
|
||||
import haus.nightmare.lib3270j.ecl.ECLFieldList;
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching unprotected input field count matching IBM Host On-Demand ECLSDInputFields.
|
||||
*/
|
||||
public class ECLSDInputFields extends ECLScreenDescriptor {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int num = 0;
|
||||
|
||||
public ECLSDInputFields() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDInputFields(int num) {
|
||||
this(num, false);
|
||||
}
|
||||
|
||||
public ECLSDInputFields(int num, boolean optional) {
|
||||
super(optional);
|
||||
this.num = num;
|
||||
}
|
||||
|
||||
public int getNum() { return num; }
|
||||
public int GetNum() { return num; }
|
||||
public void setNum(int num) { this.num = num; }
|
||||
public void SetNum(int num) { setNum(num); }
|
||||
|
||||
@Override
|
||||
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
|
||||
if (ps == null) return false;
|
||||
ECLFieldList fl = ps.getFieldList();
|
||||
if (fl == null) return false;
|
||||
int inputCount = 0;
|
||||
for (ECLField f : fl.getFields()) {
|
||||
if (!f.isProtected()) {
|
||||
inputCount++;
|
||||
}
|
||||
}
|
||||
return inputCount == num;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent, boolean full) {
|
||||
StringBuilder sb = new StringBuilder("<numinputfields ");
|
||||
sb.append("number=\"").append(num).append("\" ");
|
||||
return formatAttributes(sb.toString(), full);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package haus.nightmare.lib3270j.ecl.screenreco;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching Operator Information Area (OIA) status matching IBM Host On-Demand ECLSDOIA.
|
||||
*/
|
||||
public class ECLSDOIA extends ECLScreenDescriptor {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final int OIA_DONTCARE = 0;
|
||||
public static final int OIA_NOTINHIBITED = 1;
|
||||
|
||||
private int oiaType = OIA_NOTINHIBITED;
|
||||
private Integer explicitInhibitStatus = null;
|
||||
|
||||
public ECLSDOIA() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDOIA(int type) {
|
||||
this(type, false);
|
||||
}
|
||||
|
||||
public ECLSDOIA(int type, boolean optional) {
|
||||
super(optional);
|
||||
this.oiaType = type;
|
||||
}
|
||||
|
||||
public ECLSDOIA(String status, boolean optional) {
|
||||
super(optional);
|
||||
setOIAType(status);
|
||||
}
|
||||
|
||||
public int getOIAType() { return oiaType; }
|
||||
public int GetOIAType() { return oiaType; }
|
||||
public void setOIAType(int type) { this.oiaType = type; this.explicitInhibitStatus = null; }
|
||||
public void SetOIAType(int type) { setOIAType(type); }
|
||||
|
||||
public void setOIAType(String status) {
|
||||
if (status == null) {
|
||||
this.oiaType = OIA_NOTINHIBITED;
|
||||
return;
|
||||
}
|
||||
if (status.equalsIgnoreCase("DONTCARE")) {
|
||||
this.oiaType = OIA_DONTCARE;
|
||||
} else if (status.equalsIgnoreCase("NOTINHIBITED") || status.equalsIgnoreCase("true")) {
|
||||
this.oiaType = OIA_NOTINHIBITED;
|
||||
} else if (status.equalsIgnoreCase("false")) {
|
||||
// Inhibited: inverted NOTINHIBITED or explicit
|
||||
this.oiaType = OIA_NOTINHIBITED;
|
||||
this.invert = true;
|
||||
} else {
|
||||
try {
|
||||
this.explicitInhibitStatus = Integer.parseInt(status);
|
||||
} catch (NumberFormatException e) {
|
||||
this.oiaType = OIA_NOTINHIBITED;
|
||||
}
|
||||
}
|
||||
}
|
||||
public void SetOIAType(String status) { setOIAType(status); }
|
||||
|
||||
public Integer getExplicitInhibitStatus() { return explicitInhibitStatus; }
|
||||
public void setExplicitInhibitStatus(Integer status) { this.explicitInhibitStatus = status; }
|
||||
|
||||
@Override
|
||||
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
|
||||
if (oia == null) return true;
|
||||
if (oiaType == OIA_DONTCARE) return true;
|
||||
|
||||
if (explicitInhibitStatus != null) {
|
||||
return oia.getInputInhibited() == explicitInhibitStatus;
|
||||
}
|
||||
|
||||
// OIA_NOTINHIBITED: input must not be inhibited
|
||||
return oia.getInputInhibited() == ECLOIA.INHIBIT_NOTINHIBITED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent, boolean full) {
|
||||
StringBuilder sb = new StringBuilder("<oia ");
|
||||
if (explicitInhibitStatus != null) {
|
||||
sb.append("status=\"").append(explicitInhibitStatus).append("\" ");
|
||||
} else if (oiaType == OIA_DONTCARE) {
|
||||
sb.append("status=\"DONTCARE\" ");
|
||||
} else {
|
||||
sb.append("status=\"NOTINHIBITED\" input=\"true\" ");
|
||||
}
|
||||
return formatAttributes(sb.toString(), full);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package haus.nightmare.lib3270j.ecl.screenreco;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
|
||||
/**
|
||||
* Screen descriptor matching text on presentation space matching IBM Host On-Demand ECLSDString.
|
||||
*/
|
||||
public class ECLSDString extends ECLScreenDescriptor {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final int STRING_ABSOLUTE = 1;
|
||||
public static final int STRING_INRECT = 2;
|
||||
|
||||
private int stringType = STRING_INRECT;
|
||||
private String text = "";
|
||||
private int srow = 1;
|
||||
private int scol = 1;
|
||||
private int erow = -1;
|
||||
private int ecol = -1;
|
||||
private boolean caseSense = false;
|
||||
private int pos = -1;
|
||||
private boolean wrap = false;
|
||||
|
||||
public ECLSDString() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ECLSDString(String text) {
|
||||
super(false);
|
||||
this.text = (text != null) ? text : "";
|
||||
this.srow = 0;
|
||||
this.scol = 0;
|
||||
this.erow = 0;
|
||||
this.ecol = 0;
|
||||
this.stringType = STRING_INRECT;
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int pos, boolean caseSense) {
|
||||
this(text, pos, caseSense, false);
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int pos, boolean caseSense, boolean optional) {
|
||||
super(optional);
|
||||
this.text = (text != null) ? text : "";
|
||||
this.pos = pos;
|
||||
this.caseSense = caseSense;
|
||||
this.stringType = STRING_ABSOLUTE;
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int row, int col) {
|
||||
this(text, row, col, false, false);
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int row, int col, boolean caseSense) {
|
||||
this(text, row, col, caseSense, false);
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int row, int col, boolean caseSense, boolean optional) {
|
||||
super(optional);
|
||||
this.text = (text != null) ? text : "";
|
||||
this.srow = row;
|
||||
this.scol = col;
|
||||
this.erow = -1;
|
||||
this.ecol = -1;
|
||||
this.caseSense = caseSense;
|
||||
this.stringType = STRING_ABSOLUTE;
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional) {
|
||||
this(text, srow, scol, erow, ecol, caseSense, optional, false);
|
||||
}
|
||||
|
||||
public ECLSDString(String text, int srow, int scol, int erow, int ecol, boolean caseSense, boolean optional, boolean wrap) {
|
||||
super(optional);
|
||||
this.text = (text != null) ? text : "";
|
||||
this.srow = srow;
|
||||
this.scol = scol;
|
||||
this.erow = erow;
|
||||
this.ecol = ecol;
|
||||
this.caseSense = caseSense;
|
||||
this.wrap = wrap;
|
||||
this.stringType = STRING_INRECT;
|
||||
}
|
||||
|
||||
public String getText() { return text; }
|
||||
public String GetText() { return text; }
|
||||
public String GetString() { return text; }
|
||||
public String getString() { return text; }
|
||||
public void setText(String text) { this.text = (text != null) ? text : ""; }
|
||||
public void SetText(String text) { setText(text); }
|
||||
public void SetString(String text) { setText(text); }
|
||||
public void setString(String text) { setText(text); }
|
||||
|
||||
public int getRow() { return srow; }
|
||||
public int GetRow() { return srow; }
|
||||
public int getSRow() { return srow; }
|
||||
public int GetSRow() { return srow; }
|
||||
public void setRow(int row) { this.srow = row; }
|
||||
public void SetRow(int row) { setRow(row); }
|
||||
public void setSRow(int row) { setRow(row); }
|
||||
public void SetSRow(int row) { setRow(row); }
|
||||
|
||||
public int getCol() { return scol; }
|
||||
public int GetCol() { return scol; }
|
||||
public int getSCol() { return scol; }
|
||||
public int GetSCol() { return scol; }
|
||||
public void setCol(int col) { this.scol = col; }
|
||||
public void SetCol(int col) { setCol(col); }
|
||||
public void setSCol(int col) { setCol(col); }
|
||||
public void SetSCol(int col) { setCol(col); }
|
||||
|
||||
public int getERow() { return erow; }
|
||||
public int GetERow() { return erow; }
|
||||
public void setERow(int erow) { this.erow = erow; }
|
||||
public void SetERow(int erow) { setERow(erow); }
|
||||
|
||||
public int getECol() { return ecol; }
|
||||
public int GetECol() { return ecol; }
|
||||
public void setECol(int ecol) { this.ecol = ecol; }
|
||||
public void SetECol(int ecol) { setECol(ecol); }
|
||||
|
||||
public boolean isCaseSense() { return caseSense; }
|
||||
public boolean IsCaseSense() { return caseSense; }
|
||||
public void setCaseSense(boolean caseSense) { this.caseSense = caseSense; }
|
||||
public void SetCaseSense(boolean caseSense) { setCaseSense(caseSense); }
|
||||
|
||||
public boolean isWrap() { return wrap; }
|
||||
public boolean IsWrap() { return wrap; }
|
||||
public void setWrap(boolean wrap) { this.wrap = wrap; }
|
||||
public void SetWrap(boolean wrap) { setWrap(wrap); }
|
||||
|
||||
public int getStringType() { return stringType; }
|
||||
public int GetStringType() { return stringType; }
|
||||
public void setStringType(int stringType) { this.stringType = stringType; }
|
||||
public void SetStringType(int stringType) { setStringType(stringType); }
|
||||
|
||||
@Override
|
||||
protected boolean checkMatch(ECLPS ps, ECLOIA oia) {
|
||||
if (ps == null || text == null) return false;
|
||||
if (text.isEmpty()) return true;
|
||||
|
||||
int totalRows = ps.getRows();
|
||||
int totalCols = ps.getCols();
|
||||
|
||||
if (pos > 0) {
|
||||
int p0 = pos - 1;
|
||||
String onScreen = ps.getString(p0, text.length());
|
||||
if (onScreen == null) return false;
|
||||
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
|
||||
}
|
||||
|
||||
// 1. Search anywhere on screen if row and col are not set (<= 0)
|
||||
if (srow <= 0 && scol <= 0 && erow <= 0 && ecol <= 0) {
|
||||
String fullScreen = ps.getString(0, totalRows * totalCols);
|
||||
if (fullScreen == null) return false;
|
||||
return caseSense ? fullScreen.contains(text) : fullScreen.toLowerCase().contains(text.toLowerCase());
|
||||
}
|
||||
|
||||
// 2. Absolute match at (srow, scol)
|
||||
if (stringType == STRING_ABSOLUTE || (erow <= 0 && ecol <= 0 && srow > 0 && scol > 0)) {
|
||||
int r0 = srow - 1;
|
||||
int c0 = scol - 1;
|
||||
if (r0 < 0 || r0 >= totalRows || c0 < 0 || c0 >= totalCols) return false;
|
||||
String onScreen = ps.getString(r0, c0, text.length());
|
||||
if (onScreen == null) return false;
|
||||
return caseSense ? text.equals(onScreen) : text.equalsIgnoreCase(onScreen);
|
||||
}
|
||||
|
||||
// 3. Rectangular region match (srow, scol, erow, ecol)
|
||||
int sr = (srow > 0) ? srow : 1;
|
||||
int sc = (scol > 0) ? scol : 1;
|
||||
int er = (erow > 0) ? Math.min(erow, totalRows) : totalRows;
|
||||
int ec = (ecol > 0) ? Math.min(ecol, totalCols) : totalCols;
|
||||
|
||||
if (sr > er || sc > ec) return false;
|
||||
|
||||
if (!wrap) {
|
||||
int len = ec - sc + 1;
|
||||
for (int r = sr; r <= er; r++) {
|
||||
String line = ps.getString(r - 1, sc - 1, len);
|
||||
if (line != null) {
|
||||
boolean found = caseSense ? line.contains(text) : line.toLowerCase().contains(text.toLowerCase());
|
||||
if (found) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int len = ec - sc + 1;
|
||||
for (int r = sr; r <= er; r++) {
|
||||
String line = ps.getString(r - 1, sc - 1, len);
|
||||
if (line != null) sb.append(line);
|
||||
}
|
||||
String block = sb.toString();
|
||||
return caseSense ? block.contains(text) : block.toLowerCase().contains(text.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(int indent, boolean full) {
|
||||
StringBuilder sb = new StringBuilder("<string ");
|
||||
sb.append("value=\"").append(escapeChars(text)).append("\" ");
|
||||
if (srow > 0 || full) {
|
||||
sb.append("row=\"").append(srow).append("\" ");
|
||||
}
|
||||
if (scol > 0 || full) {
|
||||
sb.append("col=\"").append(scol).append("\" ");
|
||||
}
|
||||
if (stringType == STRING_INRECT && (erow > 0 || ecol > 0 || full)) {
|
||||
if (erow > 0 || full) sb.append("erow=\"").append(erow).append("\" ");
|
||||
if (ecol > 0 || full) sb.append("ecol=\"").append(ecol).append("\" ");
|
||||
}
|
||||
if (caseSense || full) {
|
||||
sb.append("casesense=\"").append(caseSense).append("\" ");
|
||||
}
|
||||
if (wrap || full) {
|
||||
sb.append("wrap=\"").append(wrap).append("\" ");
|
||||
}
|
||||
return formatAttributes(sb.toString(), full);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package haus.nightmare.lib3270j.ecl.screenreco;
|
||||
|
||||
import java.io.Serializable;
|
||||
import haus.nightmare.lib3270j.ecl.ECLPS;
|
||||
import haus.nightmare.lib3270j.ecl.ECLOIA;
|
||||
|
||||
/**
|
||||
* Abstract base class for composite screen recognition descriptors matching IBM Host On-Demand.
|
||||
*/
|
||||
public abstract class ECLScreenDescriptor implements Serializable, Cloneable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
protected boolean optional = false;
|
||||
protected boolean match = false;
|
||||
protected boolean invert = false;
|
||||
protected String hostid = "";
|
||||
|
||||
protected ECLScreenDescriptor() {}
|
||||
|
||||
protected ECLScreenDescriptor(boolean optional) {
|
||||
this.optional = optional;
|
||||
}
|
||||
|
||||
public boolean isOptional() { return optional; }
|
||||
public boolean IsOptional() { return optional; }
|
||||
public void setOptional(boolean optional) { this.optional = optional; }
|
||||
public void SetOptional(boolean optional) { setOptional(optional); }
|
||||
|
||||
public boolean isMatch() { return match; }
|
||||
public boolean IsMatch() { return match; }
|
||||
public void setMatch(boolean match) { this.match = match; }
|
||||
public void SetMatch(boolean match) { setMatch(match); }
|
||||
|
||||
public boolean isInvertMatch() { return invert; }
|
||||
public boolean IsInvertMatch() { return invert; }
|
||||
public void setInvertMatch(boolean invert) { this.invert = invert; }
|
||||
public void SetInvertMatch(boolean invert) { setInvertMatch(invert); }
|
||||
|
||||
public String getHostid() { return hostid; }
|
||||
public String GetHostid() { return hostid; }
|
||||
public void setHostid(String hostid) { this.hostid = (hostid != null) ? hostid : ""; }
|
||||
public void SetHostid(String hostid) { setHostid(hostid); }
|
||||
|
||||
/**
|
||||
* Checks whether this descriptor matches against presentation space and/or OIA.
|
||||
* Takes invertmatch into account.
|
||||
*/
|
||||
public boolean matches(ECLPS ps, ECLOIA oia) {
|
||||
boolean m = checkMatch(ps, oia);
|
||||
boolean res = invert ? !m : m;
|
||||
setMatch(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
public boolean Matches(ECLPS ps, ECLOIA oia) {
|
||||
return matches(ps, oia);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation of descriptor check without inversion.
|
||||
*/
|
||||
protected abstract boolean checkMatch(ECLPS ps, ECLOIA oia);
|
||||
|
||||
/**
|
||||
* Format this descriptor as an XML string.
|
||||
*/
|
||||
public abstract String format(int indent, boolean full);
|
||||
public String Format(int indent, boolean full) { return format(indent, full); }
|
||||
|
||||
protected String formatAttributes(String tagAndSpecificAttrs, boolean full) {
|
||||
StringBuilder sb = new StringBuilder(tagAndSpecificAttrs);
|
||||
if (optional || full) {
|
||||
sb.append("optional=\"").append(optional).append("\" ");
|
||||
}
|
||||
if (invert || full) {
|
||||
sb.append("invertmatch=\"").append(invert).append("\" ");
|
||||
}
|
||||
if (hostid != null && !hostid.isEmpty()) {
|
||||
sb.append("hostid=\"").append(escapeChars(hostid)).append("\" ");
|
||||
}
|
||||
sb.append("/>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String escapeChars(String string) {
|
||||
if (string == null) return "";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < string.length(); i++) {
|
||||
char c = string.charAt(i);
|
||||
switch (c) {
|
||||
case '&': sb.append("&"); break;
|
||||
case '<': sb.append("<"); break;
|
||||
case '>': sb.append(">"); break;
|
||||
case '"': sb.append("""); break;
|
||||
case '\'': sb.append("'"); break;
|
||||
default: sb.append(c); break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() {
|
||||
try {
|
||||
return super.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package haus.nightmare.lib3270j.ecl.util;
|
||||
|
||||
import haus.nightmare.lib3270j.ecl.ECLErr;
|
||||
|
||||
/**
|
||||
* Parses SendKeys strings into keystroke and mnemonic tokens.
|
||||
* Conforms 1:1 to IBM Host On-Demand (HoD) com.ibm.eNetwork.ECL.util.SendKeyTokenizer.
|
||||
*/
|
||||
public class SendKeyTokenizer {
|
||||
|
||||
private String sendString;
|
||||
private int stringIndex;
|
||||
private int stringLength;
|
||||
protected String className = this.getClass().getName();
|
||||
private boolean chunkPlainText = true;
|
||||
|
||||
public SendKeyTokenizer() {
|
||||
this.setSendKeyString(null);
|
||||
}
|
||||
|
||||
public SendKeyTokenizer(String string) {
|
||||
this.setSendKeyString(string);
|
||||
}
|
||||
|
||||
public SendKeyTokenizer(String string, boolean chunkPlainText) {
|
||||
this.setSendKeyString(string);
|
||||
this.chunkPlainText = chunkPlainText;
|
||||
}
|
||||
|
||||
public void setSendKeyString(String string) {
|
||||
this.stringLength = string != null ? string.length() : 0;
|
||||
this.sendString = string;
|
||||
this.stringIndex = 0;
|
||||
}
|
||||
|
||||
public String remainder() {
|
||||
if (this.stringIndex >= this.stringLength) {
|
||||
return "";
|
||||
}
|
||||
return this.sendString.substring(this.stringIndex);
|
||||
}
|
||||
|
||||
public boolean hasMoreTokens() {
|
||||
return this.stringLength > this.stringIndex;
|
||||
}
|
||||
|
||||
public boolean isChunkPlainText() {
|
||||
return chunkPlainText;
|
||||
}
|
||||
|
||||
public void setChunkPlainText(boolean chunkPlainText) {
|
||||
this.chunkPlainText = chunkPlainText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next keystroke or mnemonic token.
|
||||
*
|
||||
* Escaped brackets:
|
||||
* "[[" returns "["
|
||||
* "]] " returns "]"
|
||||
*
|
||||
* Unterminated or unmatched brackets throw ECLErr("ECL0012").
|
||||
*/
|
||||
public String nextToken() throws ECLErr {
|
||||
if (this.stringIndex >= this.stringLength) {
|
||||
return null;
|
||||
}
|
||||
|
||||
char c = this.sendString.charAt(this.stringIndex);
|
||||
switch (c) {
|
||||
case '[': {
|
||||
this.stringIndex++;
|
||||
if (this.stringIndex >= this.stringLength) {
|
||||
throw new ECLErr(this.className + ":1", "ECL0012", 1);
|
||||
}
|
||||
char next = this.sendString.charAt(this.stringIndex);
|
||||
if (next == '[') {
|
||||
this.stringIndex++;
|
||||
return "[";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('[');
|
||||
boolean foundClose = false;
|
||||
while (this.stringIndex < this.stringLength) {
|
||||
char ch = this.sendString.charAt(this.stringIndex);
|
||||
if (ch == ']') {
|
||||
sb.append(']');
|
||||
this.stringIndex++;
|
||||
foundClose = true;
|
||||
break;
|
||||
}
|
||||
sb.append(ch);
|
||||
this.stringIndex++;
|
||||
}
|
||||
|
||||
if (!foundClose) {
|
||||
throw new ECLErr(this.className + ":2", "ECL0012", 1);
|
||||
}
|
||||
|
||||
String token = sb.toString();
|
||||
// HoD numeric keypad single digit shortcut [0]..[9] -> "0".."9"
|
||||
if (token.length() == 3 && token.charAt(0) == '[' && token.charAt(2) == ']') {
|
||||
char digit = token.charAt(1);
|
||||
if (digit >= '0' && digit <= '9') {
|
||||
return String.valueOf(digit);
|
||||
}
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
case ']': {
|
||||
this.stringIndex++;
|
||||
if (this.stringIndex >= this.stringLength) {
|
||||
throw new ECLErr(this.className + ":3", "ECL0012", 1);
|
||||
}
|
||||
char next = this.sendString.charAt(this.stringIndex);
|
||||
if (next == ']') {
|
||||
this.stringIndex++;
|
||||
return "]";
|
||||
}
|
||||
throw new ECLErr(this.className + ":4", "ECL0012", 1);
|
||||
}
|
||||
|
||||
default: {
|
||||
if (!this.chunkPlainText) {
|
||||
this.stringIndex++;
|
||||
return String.valueOf(c);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (this.stringIndex < this.stringLength) {
|
||||
char ch = this.sendString.charAt(this.stringIndex);
|
||||
if (ch == '[' || ch == ']') {
|
||||
break;
|
||||
}
|
||||
sb.append(ch);
|
||||
this.stringIndex++;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the next character token without AID key translations.
|
||||
*/
|
||||
public String nextTokenNoAid() throws ECLErr {
|
||||
if (this.stringIndex >= this.stringLength) {
|
||||
throw new ECLErr(this.className + ":1", "ECL0012", 1);
|
||||
}
|
||||
char c = this.sendString.charAt(this.stringIndex);
|
||||
this.stringIndex++;
|
||||
return String.valueOf(c);
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,11 @@ public class FTConfig {
|
||||
private String codePage = null;
|
||||
private boolean unicode = false;
|
||||
private int unicodeType = 0; // 0 = UCS2, 1 = UTF8
|
||||
private String programName = null;
|
||||
private boolean dbcs = false;
|
||||
private boolean optionNew = false;
|
||||
private boolean clear = true;
|
||||
private boolean optionNoso = false;
|
||||
|
||||
// ========== Derived convenience getters ==========
|
||||
|
||||
@@ -91,6 +96,37 @@ public class FTConfig {
|
||||
}
|
||||
public boolean isAppend() { return existAction == ExistAction.APPEND; }
|
||||
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
|
||||
public boolean isOptionNew() { return optionNew; }
|
||||
public void setOptionNew(boolean optionNew) { this.optionNew = optionNew; }
|
||||
public boolean isOptionNoso() { return optionNoso; }
|
||||
public void setOptionNoso(boolean optionNoso) { this.optionNoso = optionNoso; }
|
||||
public boolean isDbcs() { return dbcs; }
|
||||
public void setDbcs(boolean dbcs) { this.dbcs = dbcs; }
|
||||
public boolean isClear() { return clear; }
|
||||
public void setClear(boolean clear) { this.clear = clear; }
|
||||
|
||||
public String getProgramName() {
|
||||
if (programName != null && !programName.trim().isEmpty()) {
|
||||
return programName.trim();
|
||||
}
|
||||
if (dbcs || isDbcsCodePage(codePage)) {
|
||||
return "APVUFILE";
|
||||
}
|
||||
return "IND$FILE";
|
||||
}
|
||||
|
||||
public void setProgramName(String programName) {
|
||||
this.programName = programName;
|
||||
}
|
||||
|
||||
private static boolean isDbcsCodePage(String cp) {
|
||||
if (cp == null || cp.trim().isEmpty()) return false;
|
||||
String clean = cp.trim().toUpperCase();
|
||||
if (clean.startsWith("CP")) clean = clean.substring(2);
|
||||
return clean.equals("930") || clean.equals("939") || clean.equals("935") ||
|
||||
clean.equals("1388") || clean.equals("933") || clean.equals("937") ||
|
||||
clean.equals("1371");
|
||||
}
|
||||
|
||||
// ========== Standard getters and setters ==========
|
||||
|
||||
@@ -303,10 +339,22 @@ public class FTConfig {
|
||||
|
||||
if (upper.contains("APPEND")) {
|
||||
this.existAction = ExistAction.APPEND;
|
||||
} else if (upper.contains("NEW") && !upper.contains("NONEW")) {
|
||||
this.optionNew = true;
|
||||
} else if (upper.contains("REPLACE") || upper.contains("OVERWRITE")) {
|
||||
this.existAction = ExistAction.REPLACE;
|
||||
}
|
||||
|
||||
if (upper.contains("NOSO")) {
|
||||
this.optionNoso = true;
|
||||
}
|
||||
|
||||
if (upper.contains("APVUFILE")) {
|
||||
this.programName = "APVUFILE";
|
||||
} else if (upper.contains("IND$FILE")) {
|
||||
this.programName = "IND$FILE";
|
||||
}
|
||||
|
||||
if (upper.contains("CICS")) {
|
||||
this.hostType = HostType.CICS;
|
||||
} else if (upper.contains("CMS") || upper.contains("VM")) {
|
||||
@@ -332,6 +380,10 @@ public class FTConfig {
|
||||
|
||||
String hostTrimmed = hostFilename.trim();
|
||||
|
||||
if (hostType == HostType.TSO && hostTrimmed.contains(" ") && validateCmsFilename(hostTrimmed) == null) {
|
||||
hostType = HostType.CMS;
|
||||
}
|
||||
|
||||
if (hostType == HostType.TSO) {
|
||||
String tsoError = validateTsoDatasetName(hostTrimmed);
|
||||
if (tsoError != null) return tsoError;
|
||||
@@ -448,8 +500,8 @@ public class FTConfig {
|
||||
public String buildCommand() {
|
||||
StringBuilder cmd = new StringBuilder();
|
||||
|
||||
// IND$FILE GET/PUT hostfile
|
||||
cmd.append("IND$FILE ");
|
||||
// Host program selection: IND$FILE or APVUFILE
|
||||
cmd.append(getProgramName()).append(" ");
|
||||
cmd.append(isReceive() ? "GET " : "PUT ");
|
||||
cmd.append(hostFilename != null ? hostFilename.trim() : "");
|
||||
|
||||
@@ -477,16 +529,23 @@ public class FTConfig {
|
||||
opts.append("NOCRLF");
|
||||
}
|
||||
|
||||
// Append (send only)
|
||||
if (isAppend() && isSend()) {
|
||||
if (isOptionNoso()) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("APPEND");
|
||||
opts.append("NOSO");
|
||||
}
|
||||
|
||||
// Overwrite / Replace (if send or explicitly requested)
|
||||
if (isOverwrite() && isSend()) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("REPLACE");
|
||||
// Append / New / Replace (send only)
|
||||
if (isSend()) {
|
||||
if (isAppend()) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("APPEND");
|
||||
} else if (isOptionNew()) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("NEW");
|
||||
} else if (isOverwrite()) {
|
||||
if (opts.length() > 0) opts.append(" ");
|
||||
opts.append("REPLACE");
|
||||
}
|
||||
}
|
||||
|
||||
// Host-specific send options
|
||||
|
||||
@@ -123,15 +123,69 @@ public final class FTConstants {
|
||||
// Error codes
|
||||
public static final int TR_ERR_EOF = 0x2200;
|
||||
public static final int TR_ERR_CMDFAIL = 0x0100;
|
||||
public static final int TR_ERR_OPEN_FAILED = 0x0100;
|
||||
public static final int TR_ERR_SEQ_NOT_ALLOWED = 0x0200;
|
||||
public static final int TR_ERR_NOT_AUTHORIZED = 0x3E00;
|
||||
public static final int TR_ERR_SYNTAX_ERROR = 0x6000;
|
||||
public static final int TR_ERR_GET_PAST_EOF = 0x2200;
|
||||
|
||||
// DFT buffer size limits
|
||||
// DFT buffer size limits (Default matching IBM HoD bmtuSize = 2500)
|
||||
public static final int DFT_MIN_BUF = 256;
|
||||
public static final int DFT_MAX_BUF = 32768;
|
||||
public static final int DFT_BUF = 4096;
|
||||
public static final int DFT_BUF = 2500;
|
||||
|
||||
// AID for structured field response
|
||||
public static final int AID_SF = 0x88;
|
||||
|
||||
// DDM Stream Names (ASCII and EBCDIC)
|
||||
public static final String STREAM_NAME_FT_DATA = "FT:DATA";
|
||||
public static final String STREAM_NAME_FT_MSG = "FT:MSG";
|
||||
public static final byte[] STREAM_NAME_FT_DATA_BYTES_ASCII = {
|
||||
0x46, 0x54, 0x3A, 0x44, 0x41, 0x54, 0x41 // 'F','T',':','D','A','T','A'
|
||||
};
|
||||
public static final byte[] STREAM_NAME_FT_DATA_BYTES_EBCDIC = {
|
||||
(byte) 0xC6, (byte) 0xE3, 0x7A, (byte) 0xC4, (byte) 0xC1, (byte) 0xE3, (byte) 0xC1
|
||||
};
|
||||
public static final byte[] STREAM_NAME_FT_MSG_BYTES_ASCII = {
|
||||
0x46, 0x54, 0x3A, 0x4D, 0x53, 0x47, 0x20 // 'F','T',':','M','S','G',' '
|
||||
};
|
||||
public static final byte[] STREAM_NAME_FT_MSG_BYTES_EBCDIC = {
|
||||
(byte) 0xC6, (byte) 0xE3, 0x7A, (byte) 0xD4, (byte) 0xE2, (byte) 0xC7, 0x40
|
||||
};
|
||||
|
||||
// HoD Binary Packet Templates (Xfer3270.java)
|
||||
public static final byte[] RB_OPEN_ACK = {
|
||||
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x00, 0x09, (byte) 0xFF, (byte) 0xEF
|
||||
};
|
||||
public static final byte[] RB_CLOSE_ACK = {
|
||||
(byte) 0x88, 0x00, 0x05, (byte) 0xD0, 0x41, 0x09, (byte) 0xFF, (byte) 0xEF
|
||||
};
|
||||
public static final byte[] RB_OPEN_FAILED = {
|
||||
0x00, 0x09, (byte) 0xD0, 0x00, 0x08, 0x69, 0x04, 0x01, 0x00, (byte) 0xFF, (byte) 0xEF
|
||||
};
|
||||
public static final byte[] RB_GET_PAST_EOF = {
|
||||
(byte) 0x88, 0x00, 0x09, (byte) 0xD0, 0x46, 0x08, 0x69, 0x04, 0x22, 0x00, (byte) 0xFF, (byte) 0xEF
|
||||
};
|
||||
public static final byte[] RB_UPLOAD_DATA_HEADER = {
|
||||
0x11, 0x11, (byte) 0xD0, 0x46, 0x05, 0x63, 0x06, 0x00, 0x00, 0x00, 0x00,
|
||||
(byte) 0xC0, (byte) 0x80, 0x61, 0x00, 0x00
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds exact 14-byte Data Acknowledgement matching HoD Xfer3270:
|
||||
* [0x88, 0x00, 0x0B, 0xD0, 0x47, 0x05, 0x63, 0x06, rec_b0, rec_b1, rec_b2, rec_b3, 0xFF, 0xEF]
|
||||
*/
|
||||
public static byte[] createDataAck(long recnum) {
|
||||
return new byte[] {
|
||||
(byte) 0x88, 0x00, 0x0B, (byte) 0xD0, 0x47, 0x05, 0x63, 0x06,
|
||||
(byte) ((recnum >> 24) & 0xFF),
|
||||
(byte) ((recnum >> 16) & 0xFF),
|
||||
(byte) ((recnum >> 8) & 0xFF),
|
||||
(byte) (recnum & 0xFF),
|
||||
(byte) 0xFF, (byte) 0xEF
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Transfer State ==========
|
||||
|
||||
public enum FTState {
|
||||
|
||||
@@ -215,11 +215,13 @@ public class FTCut {
|
||||
// CUT frames MUST have a skip field attribute at O_SF (1919)
|
||||
byte sfAttr = screen.getCellFAByte(O_SF);
|
||||
if (sfAttr == 0 || !isSkip(sfAttr)) {
|
||||
// Not a CUT frame (likely local echo, menu return, or intermediate screen)
|
||||
// Not a CUT frame (likely local echo, menu return, or host error screen)
|
||||
if (xferInProgress) {
|
||||
log.warning("CUT: Received non-CUT frame while transfer in progress. Host aborted.");
|
||||
xferInProgress = false;
|
||||
listener.onTransferAborted("Host aborted transfer (returned to menu)");
|
||||
String hostErr = scanScreenForErrors();
|
||||
String abortMsg = hostErr != null ? hostErr : "Host aborted transfer (returned to menu)";
|
||||
listener.onTransferAborted(abortMsg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -245,6 +247,38 @@ public class FTCut {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans rows 1-24 of the 3270 screen buffer for host error indicators.
|
||||
* Checks for TSO (IKJ*), CMS (DMS*), CICS (DFH*), and generic transfer abort messages.
|
||||
*/
|
||||
public String scanScreenForErrors() {
|
||||
if (screen == null) return null;
|
||||
int rows = screen.getRows();
|
||||
int cols = screen.getCols();
|
||||
String[] errorPrefixes = {
|
||||
"IKJ", "DMS", "DFH", "TRANS", "NOT FOUND", "ERROR", "REJECTED", "INVALID", "COMMAND NOT FOUND", "FILE NOT FOUND"
|
||||
};
|
||||
for (int r = 0; r < rows; r++) {
|
||||
StringBuilder lineSb = new StringBuilder();
|
||||
for (int c = 0; c < cols; c++) {
|
||||
int pos = r * cols + c;
|
||||
int ec = screen.getCellEC(pos);
|
||||
if (ec != 0) {
|
||||
lineSb.append(translator.ebcdicToUnicode(ec));
|
||||
}
|
||||
}
|
||||
String line = lineSb.toString().trim();
|
||||
if (line.isEmpty()) continue;
|
||||
String lineUpper = line.toUpperCase();
|
||||
for (String prefix : errorPrefixes) {
|
||||
if (lineUpper.contains(prefix)) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void checkForHostPrompts() {
|
||||
if (screen == null) return;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
@@ -299,6 +333,10 @@ public class FTCut {
|
||||
cutAck();
|
||||
|
||||
String msg = extractHostMessage();
|
||||
if ("Host cancelled transfer".equals(msg)) {
|
||||
String hostErr = scanScreenForErrors();
|
||||
if (hostErr != null) msg = hostErr;
|
||||
}
|
||||
listener.onTransferAborted(msg);
|
||||
break;
|
||||
|
||||
@@ -388,7 +426,7 @@ public class FTCut {
|
||||
log.fine("CUT: > DATA seq=" + seq + " len=" + count);
|
||||
expandedLength += count;
|
||||
listener.onBytesTransferred(expandedLength);
|
||||
input.sendAidForFT(AID_ENTER);
|
||||
paceAndSendAid(AID_ENTER);
|
||||
}
|
||||
|
||||
// ========== Data (Download) ==========
|
||||
@@ -467,11 +505,11 @@ public class FTCut {
|
||||
screen.setCellFA(O_DR_SF, attr);
|
||||
|
||||
log.fine("CUT: Retransmitting last upload frame (len=" + lastUploadCount + ")");
|
||||
input.sendAidForFT(AID_ENTER);
|
||||
paceAndSendAid(AID_ENTER);
|
||||
} else {
|
||||
// In download mode, send ACK_RETRANSMIT (PF1)
|
||||
log.fine("CUT: Requesting frame retransmit from host via PF1");
|
||||
input.sendAidForFT(ACK_RETRANSMIT);
|
||||
paceAndSendAid(ACK_RETRANSMIT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,7 +517,7 @@ public class FTCut {
|
||||
|
||||
private void cutAck() {
|
||||
log.fine("CUT: > ACK (Enter)");
|
||||
input.sendAidForFT(AID_ENTER);
|
||||
paceAndSendAid(AID_ENTER);
|
||||
}
|
||||
|
||||
// ========== Abort ==========
|
||||
@@ -492,10 +530,24 @@ public class FTCut {
|
||||
screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF);
|
||||
screen.setCell(RO_REASON_CODE + 1, reason & 0xFF);
|
||||
|
||||
input.sendAidForFT(AID_PF2);
|
||||
paceAndSendAid(AID_PF2);
|
||||
listener.onTransferAborted(message);
|
||||
}
|
||||
|
||||
private void paceAndSendAid(int aid) {
|
||||
if (input == null) return;
|
||||
long start = System.currentTimeMillis();
|
||||
while (input.isKeyboardLocked() && System.currentTimeMillis() - start < 1000) {
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
input.sendAidForFT((byte) aid);
|
||||
}
|
||||
|
||||
// ========== Character Translation ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,6 +49,14 @@ public class FTDft {
|
||||
private int customMtuSize = 0;
|
||||
private int pendingByte = -1;
|
||||
|
||||
// Record formatting buffers and SO/SI tracking
|
||||
private final ByteArrayOutputStream downloadRecordBuffer = new ByteArrayOutputStream();
|
||||
private boolean dbcsMode = false;
|
||||
private boolean hasPendingByteInDBCS = false;
|
||||
private byte pendingByteDBCS = 0;
|
||||
private byte[] pendingUploadRecord = null;
|
||||
private int pendingUploadRecordOffset = 0;
|
||||
|
||||
// Savebuf for Read Modified retransmit
|
||||
private byte[] dftSaveBuf = null;
|
||||
private int dftSaveBufLen = 0;
|
||||
@@ -159,6 +167,12 @@ public class FTDft {
|
||||
dftSaveBufLen = 0;
|
||||
lastCr = false;
|
||||
pendingByte = -1;
|
||||
downloadRecordBuffer.reset();
|
||||
dbcsMode = false;
|
||||
hasPendingByteInDBCS = false;
|
||||
pendingByteDBCS = 0;
|
||||
pendingUploadRecord = null;
|
||||
pendingUploadRecordOffset = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,6 +215,12 @@ public class FTDft {
|
||||
|
||||
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
|
||||
|
||||
// Check if this is a host error reply (e.g. TR_ERROR_REPLY 0x08, or contains TR_ERROR_HDR 0x6904)
|
||||
if ((requestCode & 0xFF) == TR_ERROR_REPLY || isErrorField(data, payloadStart, offset + length - payloadStart)) {
|
||||
handleHostErrorReply(data, payloadStart, offset + length - payloadStart);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case TR_OPEN_REQ:
|
||||
dftOpenRequest(data, offset, length);
|
||||
@@ -228,8 +248,74 @@ public class FTDft {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isErrorField(byte[] data, int start, int length) {
|
||||
int end = Math.min(start + length, data.length);
|
||||
for (int i = start; i + 3 < end; i++) {
|
||||
int hdr = ((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF);
|
||||
if (hdr == TR_ERROR_HDR) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleHostErrorReply(byte[] data, int payloadStart, int length) {
|
||||
int end = Math.min(payloadStart + length, data.length);
|
||||
int errCode = 0;
|
||||
for (int i = payloadStart; i + 3 < end; i++) {
|
||||
int hdr = ((data[i] & 0xFF) << 8) | (data[i + 1] & 0xFF);
|
||||
if (hdr == TR_ERROR_HDR) {
|
||||
errCode = ((data[i + 2] & 0xFF) << 8) | (data[i + 3] & 0xFF);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String message;
|
||||
switch (errCode) {
|
||||
case TR_ERR_OPEN_FAILED:
|
||||
message = "Host file open failed (file not found, cannot open, or dataset does not exist)";
|
||||
break;
|
||||
case TR_ERR_SEQ_NOT_ALLOWED:
|
||||
message = "Host transfer sequence not allowed (state violation)";
|
||||
break;
|
||||
case TR_ERR_GET_PAST_EOF:
|
||||
if (listener != null && listener.getCurrentState() == FTState.RUNNING) {
|
||||
log.info("DFT: Host reported end of file (0x2200)");
|
||||
listener.onTransferComplete(null);
|
||||
return;
|
||||
}
|
||||
message = "Host reached end of file";
|
||||
break;
|
||||
case TR_ERR_NOT_AUTHORIZED:
|
||||
message = "Host file access not authorized (security violation)";
|
||||
break;
|
||||
case TR_ERR_SYNTAX_ERROR:
|
||||
message = "Host command syntax error or invalid transfer options";
|
||||
break;
|
||||
default:
|
||||
message = "Host transfer error (code 0x" + Integer.toHexString(errCode) + ")";
|
||||
break;
|
||||
}
|
||||
log.warning("DFT host error reply: " + message + " (errCode=0x" + Integer.toHexString(errCode) + ")");
|
||||
dftAbort(message, TR_DATA_INSERT);
|
||||
}
|
||||
|
||||
// ========== Open Request ==========
|
||||
|
||||
private static boolean containsBytes(byte[] data, int start, int end, byte[] pattern) {
|
||||
if (data == null || pattern == null || pattern.length == 0) return false;
|
||||
int max = Math.min(end, data.length) - pattern.length;
|
||||
for (int i = Math.max(0, start); i <= max; i++) {
|
||||
boolean match = true;
|
||||
for (int j = 0; j < pattern.length; j++) {
|
||||
if (data[i + j] != pattern[j]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void dftOpenRequest(byte[] data, int sfOffset, int sfLength) {
|
||||
log.fine("DFT: Open request");
|
||||
|
||||
@@ -247,7 +333,10 @@ public class FTDft {
|
||||
// Check for host DDM file attributes embedded in open request
|
||||
parseDdmAttributes(data, sfOffset + 3, sfLength - 3);
|
||||
|
||||
if (isMessageStream(nameBuf)) {
|
||||
int sfEnd = Math.min(sfOffset + sfLength, data.length);
|
||||
if (containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_ASCII) ||
|
||||
containsBytes(data, sfOffset, sfEnd, STREAM_NAME_FT_MSG_BYTES_EBCDIC) ||
|
||||
isMessageStream(nameBuf)) {
|
||||
messageFlag = true;
|
||||
log.info("DFT: Open request for message stream (" + nameBuf + ")");
|
||||
} else {
|
||||
@@ -258,7 +347,7 @@ public class FTDft {
|
||||
dftEof = false;
|
||||
recnum = 1;
|
||||
|
||||
// Acknowledge Open matching x3270 (SF_TRANSFER_DATA + 0x0009)
|
||||
// Acknowledge Open matching IBM Host On-Demand
|
||||
dftOpenAck();
|
||||
}
|
||||
|
||||
@@ -322,12 +411,7 @@ public class FTDft {
|
||||
}
|
||||
|
||||
private void dftOpenAck() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
|
||||
out.write(AID_SF);
|
||||
out.write(0); out.write(5); // SF length
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
out.write(0); out.write(9); // OpenAck response code 0x0009
|
||||
input.sendStructuredFieldData(out.toByteArray());
|
||||
input.sendStructuredFieldData(RB_OPEN_ACK.clone());
|
||||
}
|
||||
|
||||
// ========== Insert Request (host sending data for download) ==========
|
||||
@@ -441,31 +525,125 @@ public class FTDft {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write download data to the local file, handling ASCII conversion.
|
||||
* Matching x3270 upload_convert logic.
|
||||
* Write download data to the local file, handling ASCII conversion, RECFM F/V formatting, and EOF stripping.
|
||||
* Conforms to IBM Host On-Demand specification.
|
||||
*/
|
||||
private void writeDownloadData(byte[] data, int offset, int length,
|
||||
FTConfig config) throws IOException {
|
||||
if (outputStream == null) return;
|
||||
if (outputStream == null || length <= 0) return;
|
||||
|
||||
if (config != null && config.isUnicode()) {
|
||||
outputStream.write(data, offset, length);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.isAscii()) {
|
||||
if (config != null && !config.isAscii()) {
|
||||
outputStream.write(data, offset, length);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
|
||||
"V".equalsIgnoreCase(hostRecfm);
|
||||
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
|
||||
"F".equalsIgnoreCase(hostRecfm);
|
||||
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
||||
|
||||
if (isRecfmV) {
|
||||
downloadRecordBuffer.write(data, offset, length);
|
||||
processVariableDownloadRecords(config);
|
||||
} else if (isRecfmF && lrecl > 0) {
|
||||
downloadRecordBuffer.write(data, offset, length);
|
||||
processFixedDownloadRecords(lrecl, config);
|
||||
} else {
|
||||
writeConvertedChunk(data, offset, length, config);
|
||||
}
|
||||
}
|
||||
|
||||
private void processVariableDownloadRecords(FTConfig config) throws IOException {
|
||||
while (downloadRecordBuffer.size() >= 4) {
|
||||
byte[] buf = downloadRecordBuffer.toByteArray();
|
||||
int rdwLen = ((buf[0] & 0xFF) << 8) | (buf[1] & 0xFF);
|
||||
if (rdwLen < 4) {
|
||||
downloadRecordBuffer.reset();
|
||||
if (buf.length > 1) {
|
||||
downloadRecordBuffer.write(buf, 1, buf.length - 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (buf.length < rdwLen) {
|
||||
break;
|
||||
}
|
||||
int recDataLen = rdwLen - 4;
|
||||
writeConvertedChunk(buf, 4, recDataLen, config);
|
||||
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
downloadRecordBuffer.reset();
|
||||
if (buf.length > rdwLen) {
|
||||
downloadRecordBuffer.write(buf, rdwLen, buf.length - rdwLen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processFixedDownloadRecords(int lrecl, FTConfig config) throws IOException {
|
||||
while (downloadRecordBuffer.size() >= lrecl) {
|
||||
byte[] buf = downloadRecordBuffer.toByteArray();
|
||||
int end = lrecl;
|
||||
while (end > 0) {
|
||||
int b = buf[end - 1] & 0xFF;
|
||||
if (b == 0x20 || b == 0x40 || (config != null && config.isRemapFlag() && b == FT2ASC[0x40])) {
|
||||
end--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
writeConvertedChunk(buf, 0, end, config);
|
||||
outputStream.write(System.lineSeparator().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
downloadRecordBuffer.reset();
|
||||
if (buf.length > lrecl) {
|
||||
downloadRecordBuffer.write(buf, lrecl, buf.length - lrecl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void flushRemainingDownloadRecords() throws IOException {
|
||||
if (outputStream == null || downloadRecordBuffer.size() == 0) return;
|
||||
FTConfig config = listener != null ? listener.getConfig() : null;
|
||||
byte[] remaining = downloadRecordBuffer.toByteArray();
|
||||
downloadRecordBuffer.reset();
|
||||
if (config != null && config.isAscii()) {
|
||||
int end = remaining.length;
|
||||
while (end > 0) {
|
||||
int b = remaining[end - 1] & 0xFF;
|
||||
if (b == 0x20 || b == 0x40 || (config.isRemapFlag() && b == FT2ASC[0x40])) {
|
||||
end--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (end > 0) {
|
||||
writeConvertedChunk(remaining, 0, end, config);
|
||||
}
|
||||
} else {
|
||||
outputStream.write(remaining);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
|
||||
private void writeConvertedChunk(byte[] data, int offset, int length, FTConfig config) throws IOException {
|
||||
if (outputStream == null || length <= 0) return;
|
||||
for (int i = 0; i < length; i++) {
|
||||
int b = data[offset + i] & 0xFF;
|
||||
|
||||
if (config.isCrFlag() && (b == '\r' || b == EOF_CTRL_Z || b == EOF_CTRL_D)) {
|
||||
continue; // Strip CR and EOF (^Z / ^D)
|
||||
if (b == EOF_CTRL_Z || b == EOF_CTRL_D) {
|
||||
continue; // Strip EOF (^Z / ^D)
|
||||
}
|
||||
|
||||
if (!config.isRemapFlag()) {
|
||||
if (config != null && config.isCrFlag() && b == '\r') {
|
||||
continue; // Strip CR
|
||||
}
|
||||
|
||||
if (config == null || !config.isRemapFlag()) {
|
||||
outputStream.write(b);
|
||||
continue;
|
||||
}
|
||||
@@ -506,30 +684,42 @@ public class FTDft {
|
||||
byte[] readBuf = new byte[numbytes];
|
||||
int totalRead = 0;
|
||||
|
||||
boolean isRecfmF = (config != null && config.getRecfm() == FTConfig.RecordFormat.FIXED) ||
|
||||
"F".equalsIgnoreCase(hostRecfm);
|
||||
boolean isRecfmV = (config != null && config.getRecfm() == FTConfig.RecordFormat.VARIABLE) ||
|
||||
"V".equalsIgnoreCase(hostRecfm);
|
||||
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
||||
|
||||
try {
|
||||
while (!dftEof && totalRead < numbytes) {
|
||||
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
|
||||
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
|
||||
if (n <= 0) {
|
||||
dftEof = true;
|
||||
break;
|
||||
if (config != null && config.isAscii() && isRecfmF && lrecl > 0) {
|
||||
totalRead = readFixedRecordUpload(readBuf, numbytes, lrecl, config);
|
||||
} else if (config != null && config.isAscii() && isRecfmV) {
|
||||
totalRead = readVariableRecordUpload(readBuf, numbytes, config);
|
||||
} else {
|
||||
while (!dftEof && totalRead < numbytes) {
|
||||
if (config != null && config.isUnicode() && inputStream instanceof haus.nightmare.lib3270j.xfer.XferFileInputUnicode) {
|
||||
int n = ((haus.nightmare.lib3270j.xfer.XferFileInputUnicode) inputStream).readData(readBuf, totalRead, numbytes - totalRead);
|
||||
if (n <= 0) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
totalRead += n;
|
||||
} else if (config != null && config.isAscii()) {
|
||||
int b = dftAsciiRead(config);
|
||||
if (b == -1) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
readBuf[totalRead++] = (byte) b;
|
||||
} else {
|
||||
if (inputStream == null) { dftEof = true; break; }
|
||||
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
|
||||
if (n <= 0) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
totalRead += n;
|
||||
}
|
||||
totalRead += n;
|
||||
} else if (config.isAscii()) {
|
||||
int b = dftAsciiRead(config);
|
||||
if (b == -1) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
readBuf[totalRead++] = (byte) b;
|
||||
} else {
|
||||
if (inputStream == null) { dftEof = true; break; }
|
||||
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
|
||||
if (n <= 0) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
totalRead += n;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -537,17 +727,17 @@ public class FTDft {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
|
||||
out.write(AID_SF);
|
||||
|
||||
int sfLenPos = out.size();
|
||||
out.write(0); out.write(0);
|
||||
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
|
||||
if (totalRead > 0) {
|
||||
log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes");
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
|
||||
out.write(AID_SF);
|
||||
|
||||
int sfLenPos = out.size();
|
||||
out.write(0); out.write(0);
|
||||
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
|
||||
out.write((TR_GET_REPLY >> 8) & 0xFF);
|
||||
out.write(TR_GET_REPLY & 0xFF);
|
||||
|
||||
@@ -570,31 +760,167 @@ public class FTDft {
|
||||
out.write(readBuf, 0, totalRead);
|
||||
|
||||
bytesTransferred += totalRead;
|
||||
|
||||
byte[] result = out.toByteArray();
|
||||
int sfLen = result.length - 1;
|
||||
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
|
||||
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
|
||||
|
||||
dftSaveBuf = result.clone();
|
||||
dftSaveBufLen = result.length;
|
||||
|
||||
input.sendStructuredFieldData(result);
|
||||
listener.onBytesTransferred(bytesTransferred);
|
||||
} else {
|
||||
log.fine("DFT: > GetReply EOF");
|
||||
|
||||
out.write((TR_GET_REQ >> 8) & 0xFF);
|
||||
out.write(TR_ERROR_REPLY & 0xFF);
|
||||
|
||||
out.write((TR_ERROR_HDR >> 8) & 0xFF);
|
||||
out.write(TR_ERROR_HDR & 0xFF);
|
||||
|
||||
out.write((TR_ERR_EOF >> 8) & 0xFF);
|
||||
out.write(TR_ERR_EOF & 0xFF);
|
||||
|
||||
dftEof = true;
|
||||
dftSaveBuf = RB_GET_PAST_EOF.clone();
|
||||
dftSaveBufLen = RB_GET_PAST_EOF.length;
|
||||
input.sendStructuredFieldData(RB_GET_PAST_EOF.clone());
|
||||
listener.onBytesTransferred(bytesTransferred);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readNextUploadLineConverted(FTConfig config) throws IOException {
|
||||
if (inputStream == null || dftEof) return null;
|
||||
ByteArrayOutputStream lineBytes = new ByteArrayOutputStream();
|
||||
int c;
|
||||
boolean hasData = false;
|
||||
while ((c = inputStream.read()) != -1) {
|
||||
hasData = true;
|
||||
if (c == '\r') {
|
||||
continue; // ignore CR
|
||||
}
|
||||
if (c == '\n') {
|
||||
break;
|
||||
}
|
||||
if (c == EOF_CTRL_Z || c == EOF_CTRL_D) {
|
||||
break;
|
||||
}
|
||||
lineBytes.write(c);
|
||||
}
|
||||
if (!hasData && c == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] result = out.toByteArray();
|
||||
int sfLen = result.length - 1;
|
||||
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
|
||||
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
|
||||
byte[] raw = lineBytes.toByteArray();
|
||||
return convertLineForUpload(raw, config);
|
||||
}
|
||||
|
||||
dftSaveBuf = result.clone();
|
||||
dftSaveBufLen = result.length;
|
||||
private byte[] convertLineForUpload(byte[] raw, FTConfig config) {
|
||||
String lineStr = new String(raw, StandardCharsets.UTF_8);
|
||||
ByteArrayOutputStream converted = new ByteArrayOutputStream(raw.length + 8);
|
||||
boolean isDbcs = (config != null && config.isDbcs()) || (translator != null && translator.getCodePage() != null && translator.getCodePage().isDBCS());
|
||||
boolean noSo = config != null && config.isOptionNoso();
|
||||
boolean inDbcsState = false;
|
||||
|
||||
input.sendStructuredFieldData(result);
|
||||
listener.onBytesTransferred(bytesTransferred);
|
||||
for (int i = 0; i < lineStr.length(); i++) {
|
||||
char ch = lineStr.charAt(i);
|
||||
int ebc = translator != null ? translator.unicodeToEbcdic(ch) : -1;
|
||||
boolean charIsDbcs = isDbcs && (ch > 0xFF || ebc > 0xFF);
|
||||
if (charIsDbcs) {
|
||||
if (!inDbcsState && !noSo) {
|
||||
converted.write(0x0E); // Shift-Out
|
||||
inDbcsState = true;
|
||||
}
|
||||
converted.write((ebc >> 8) & 0xFF);
|
||||
converted.write(ebc & 0xFF);
|
||||
} else {
|
||||
if (inDbcsState && !noSo) {
|
||||
converted.write(0x0F); // Shift-In
|
||||
inDbcsState = false;
|
||||
}
|
||||
converted.write(convertAsciiByte(ch, config));
|
||||
}
|
||||
}
|
||||
if (inDbcsState && !noSo) {
|
||||
converted.write(0x0F); // Shift-In at end of record
|
||||
}
|
||||
return converted.toByteArray();
|
||||
}
|
||||
|
||||
private int readFixedRecordUpload(byte[] outBuf, int maxBytes, int lrecl, FTConfig config) throws IOException {
|
||||
int totalRead = 0;
|
||||
int padByte = (config != null && config.isRemapFlag()) ? FT2ASC[0x40] : 0x20;
|
||||
|
||||
while (totalRead + lrecl <= maxBytes && !dftEof) {
|
||||
byte[] rec;
|
||||
if (pendingUploadRecord != null) {
|
||||
rec = pendingUploadRecord;
|
||||
pendingUploadRecord = null;
|
||||
} else {
|
||||
rec = readNextUploadLineConverted(config);
|
||||
if (rec == null) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (rec.length <= lrecl) {
|
||||
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
|
||||
totalRead += rec.length;
|
||||
int padCount = lrecl - rec.length;
|
||||
for (int p = 0; p < padCount; p++) {
|
||||
outBuf[totalRead++] = (byte) padByte;
|
||||
}
|
||||
} else {
|
||||
// Longer than lrecl: split
|
||||
System.arraycopy(rec, 0, outBuf, totalRead, lrecl);
|
||||
totalRead += lrecl;
|
||||
int rem = rec.length - lrecl;
|
||||
pendingUploadRecord = new byte[rem];
|
||||
System.arraycopy(rec, lrecl, pendingUploadRecord, 0, rem);
|
||||
}
|
||||
}
|
||||
return totalRead;
|
||||
}
|
||||
|
||||
private int readVariableRecordUpload(byte[] outBuf, int maxBytes, FTConfig config) throws IOException {
|
||||
int totalRead = 0;
|
||||
while (totalRead + 4 < maxBytes && !dftEof) {
|
||||
byte[] rec;
|
||||
if (pendingUploadRecord != null) {
|
||||
rec = pendingUploadRecord;
|
||||
pendingUploadRecord = null;
|
||||
} else {
|
||||
rec = readNextUploadLineConverted(config);
|
||||
if (rec == null) {
|
||||
dftEof = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int needed = rec.length + 4;
|
||||
if (totalRead + needed <= maxBytes) {
|
||||
outBuf[totalRead++] = (byte) ((needed >> 8) & 0xFF);
|
||||
outBuf[totalRead++] = (byte) (needed & 0xFF);
|
||||
outBuf[totalRead++] = 0;
|
||||
outBuf[totalRead++] = 0;
|
||||
System.arraycopy(rec, 0, outBuf, totalRead, rec.length);
|
||||
totalRead += rec.length;
|
||||
} else {
|
||||
if (totalRead > 0) {
|
||||
// Save for next MTU buffer
|
||||
pendingUploadRecord = rec;
|
||||
break;
|
||||
} else {
|
||||
// Single record exceeds entire MTU: split
|
||||
int chunk = maxBytes - 4;
|
||||
int rdwLen = chunk + 4;
|
||||
outBuf[totalRead++] = (byte) ((rdwLen >> 8) & 0xFF);
|
||||
outBuf[totalRead++] = (byte) (rdwLen & 0xFF);
|
||||
outBuf[totalRead++] = 0;
|
||||
outBuf[totalRead++] = 0;
|
||||
System.arraycopy(rec, 0, outBuf, totalRead, chunk);
|
||||
totalRead += chunk;
|
||||
int rem = rec.length - chunk;
|
||||
pendingUploadRecord = new byte[rem];
|
||||
System.arraycopy(rec, chunk, pendingUploadRecord, 0, rem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalRead;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -717,14 +1043,13 @@ public class FTDft {
|
||||
private void dftCloseRequest() {
|
||||
log.fine("DFT: Close");
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
|
||||
out.write(AID_SF);
|
||||
out.write(0); out.write(5);
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
out.write((TR_CLOSE_REPLY >> 8) & 0xFF);
|
||||
out.write(TR_CLOSE_REPLY & 0xFF);
|
||||
try {
|
||||
flushRemainingDownloadRecords();
|
||||
} catch (IOException e) {
|
||||
log.warning("DFT: Error flushing download buffer on close: " + e.getMessage());
|
||||
}
|
||||
|
||||
input.sendStructuredFieldData(out.toByteArray());
|
||||
input.sendStructuredFieldData(RB_CLOSE_ACK.clone());
|
||||
|
||||
if (listener.getCurrentState() != FTState.NONE) {
|
||||
log.info("DFT: File transfer completed on close request (" + bytesTransferred + " bytes)");
|
||||
@@ -735,21 +1060,7 @@ public class FTDft {
|
||||
// ========== Data Acknowledgement ==========
|
||||
|
||||
private void dftDataAck() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(12);
|
||||
out.write(AID_SF);
|
||||
out.write(0); out.write(11); // SF length = 11 (0x000B)
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
|
||||
out.write(TR_NORMAL_REPLY & 0xFF);
|
||||
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
|
||||
out.write(TR_RECNUM_HDR & 0xFF);
|
||||
out.write((int) ((recnum >> 24) & 0xFF));
|
||||
out.write((int) ((recnum >> 16) & 0xFF));
|
||||
out.write((int) ((recnum >> 8) & 0xFF));
|
||||
out.write((int) (recnum & 0xFF));
|
||||
|
||||
recnum++;
|
||||
input.sendStructuredFieldData(out.toByteArray());
|
||||
input.sendStructuredFieldData(createDataAck(recnum++));
|
||||
}
|
||||
|
||||
// ========== Abort ==========
|
||||
@@ -757,18 +1068,22 @@ public class FTDft {
|
||||
private void dftAbort(String message, int code) {
|
||||
log.warning("DFT: ABORT — " + message);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
|
||||
out.write(AID_SF);
|
||||
out.write(0); out.write(9);
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
out.write((code >> 8) & 0xFF);
|
||||
out.write(TR_ERROR_REPLY & 0xFF);
|
||||
out.write((TR_ERROR_HDR >> 8) & 0xFF);
|
||||
out.write(TR_ERROR_HDR & 0xFF);
|
||||
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
|
||||
out.write(TR_ERR_CMDFAIL & 0xFF);
|
||||
if (code == TR_OPEN_REQ) {
|
||||
input.sendStructuredFieldData(RB_OPEN_FAILED.clone());
|
||||
} else {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
|
||||
out.write(AID_SF);
|
||||
out.write(0); out.write(9);
|
||||
out.write(SF_TRANSFER_DATA);
|
||||
out.write((code >> 8) & 0xFF);
|
||||
out.write(TR_ERROR_REPLY & 0xFF);
|
||||
out.write((TR_ERROR_HDR >> 8) & 0xFF);
|
||||
out.write(TR_ERROR_HDR & 0xFF);
|
||||
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
|
||||
out.write(TR_ERR_CMDFAIL & 0xFF);
|
||||
|
||||
input.sendStructuredFieldData(out.toByteArray());
|
||||
input.sendStructuredFieldData(out.toByteArray());
|
||||
}
|
||||
listener.onTransferAborted(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,14 @@ import java.util.List;
|
||||
* <p>A GOCA fillet curve passes from the initial point P_0 to the terminal point P_{N-1},
|
||||
* bending tangentially toward each intermediate control point P_i.
|
||||
*/
|
||||
public class FilletPts {
|
||||
public class FilletPts extends HODEllipse {
|
||||
|
||||
public static final int DEFAULT_STEPS_PER_SEGMENT = 24;
|
||||
|
||||
public FilletPts() {
|
||||
super(0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates interpolated spline vertices across control points using double precision.
|
||||
*
|
||||
@@ -116,17 +120,115 @@ public class FilletPts {
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates fillet points into provided output buffers.
|
||||
* Interpolates fillet points into provided output buffers using HoD's exact rational quadratic
|
||||
* polynomial spline algorithm.
|
||||
* Matches IBM Host On-Demand FilletPts.getFilletPoints.
|
||||
*/
|
||||
public int getFilletPoints(int[] xIn, int[] yIn, int n, int[] xOut, int[] yOut) {
|
||||
if (xIn == null || yIn == null || n <= 0 || xOut == null || yOut == null) {
|
||||
public int getFilletPoints(int[] nArray, int[] nArray2, int n, int[] nArray3, int[] nArray4) {
|
||||
if (nArray == null || nArray2 == null || n <= 0 || nArray3 == null || nArray4 == null) {
|
||||
return 0;
|
||||
}
|
||||
int[][] pts = calculate(xIn, yIn, n, 16);
|
||||
int count = Math.min(pts[0].length, Math.min(xOut.length, yOut.length));
|
||||
System.arraycopy(pts[0], 0, xOut, 0, count);
|
||||
System.arraycopy(pts[1], 0, yOut, 0, count);
|
||||
return count;
|
||||
|
||||
int n2;
|
||||
int n3 = 0;
|
||||
int n4 = 0;
|
||||
boolean bl = false;
|
||||
int n5 = 0;
|
||||
for (n2 = 1; n2 < n; ++n2) {
|
||||
if (nArray[n2] != nArray[n5] || nArray2[n2] != nArray2[n5]) continue;
|
||||
bl = true;
|
||||
}
|
||||
if (bl) {
|
||||
int[] nArray5 = new int[n];
|
||||
int[] nArray6 = new int[n];
|
||||
nArray5[0] = nArray[0];
|
||||
nArray6[0] = nArray2[0];
|
||||
n5 = 0;
|
||||
for (n2 = 1; n2 < n; ++n2) {
|
||||
if (nArray[n2] == nArray[n5] && nArray2[n2] == nArray2[n5]) continue;
|
||||
nArray5[++n5] = nArray[n2];
|
||||
nArray6[n5] = nArray2[n2];
|
||||
}
|
||||
nArray = nArray5;
|
||||
nArray2 = nArray6;
|
||||
n = n5 + 1;
|
||||
}
|
||||
nArray3[n3] = nArray[0];
|
||||
nArray4[n3] = nArray2[0];
|
||||
int n6 = nArray[0];
|
||||
int n9 = nArray2[0];
|
||||
n3++;
|
||||
|
||||
if (n == 1) {
|
||||
nArray3[n3] = nArray3[0];
|
||||
nArray4[n3++] = nArray4[0];
|
||||
return n3;
|
||||
}
|
||||
if (n == 2) {
|
||||
nArray3[n3] = nArray[1];
|
||||
nArray4[n3++] = nArray2[1];
|
||||
return n3;
|
||||
}
|
||||
int[] nArray7 = new int[3];
|
||||
int[] nArray8 = new int[3];
|
||||
nArray7[0] = nArray[n4];
|
||||
nArray8[0] = nArray2[n4++];
|
||||
--n;
|
||||
nArray7[1] = nArray[n4];
|
||||
nArray8[1] = nArray2[n4++];
|
||||
--n;
|
||||
while (true) {
|
||||
if (n == 0) {
|
||||
nArray3[n3] = nArray7[1];
|
||||
nArray4[n3++] = nArray8[1];
|
||||
return n3;
|
||||
}
|
||||
nArray7[2] = nArray[n4];
|
||||
nArray8[2] = nArray2[n4];
|
||||
if (n > 1) {
|
||||
nArray7[2] = (nArray7[2] + nArray7[1]) / 2;
|
||||
nArray8[2] = (nArray8[2] + nArray8[1]) / 2;
|
||||
}
|
||||
--n;
|
||||
this.dCenterX = nArray7[0] + nArray7[2] - nArray7[1];
|
||||
this.dCenterY = nArray8[0] + nArray8[2] - nArray8[1];
|
||||
this.dP = (double) nArray7[0] - this.dCenterX;
|
||||
this.dQ = (double) nArray8[2] - this.dCenterY;
|
||||
this.dR = (double) nArray7[2] - this.dCenterX;
|
||||
this.dS = (double) nArray8[0] - this.dCenterY;
|
||||
int n10 = (int) this.dP;
|
||||
int n11 = (int) this.dQ;
|
||||
int n12 = (int) this.dR;
|
||||
int n13 = (int) this.dS;
|
||||
int n14 = Math.max(Math.abs(n10) + Math.abs(n13), Math.abs(n12) + Math.abs(n11));
|
||||
this.dRad = n14;
|
||||
if (n14 == 0) {
|
||||
return n3;
|
||||
}
|
||||
int n15 = this.bres(this.dRad);
|
||||
for (n2 = 0; n2 <= n15; ++n2) {
|
||||
this.filletHODSegment2(nArray3, nArray4, n3, this.dXq[n2], this.dYq[n2]);
|
||||
if (n6 == nArray3[n3] && n9 == nArray4[n3]) continue;
|
||||
n6 = nArray3[n3];
|
||||
n9 = nArray4[n3++];
|
||||
}
|
||||
for (n2 = n15; n2 >= 0; --n2) {
|
||||
this.filletHODSegment2(nArray3, nArray4, n3, this.dYq[n2], this.dXq[n2]);
|
||||
if (n6 == nArray3[n3] && n9 == nArray4[n3]) continue;
|
||||
n6 = nArray3[n3];
|
||||
n9 = nArray4[n3++];
|
||||
}
|
||||
if (n6 != nArray7[2] || n9 != nArray8[2]) {
|
||||
nArray3[n3] = nArray7[2];
|
||||
nArray4[n3++] = nArray8[2];
|
||||
}
|
||||
if (n == 0) {
|
||||
return n3;
|
||||
}
|
||||
nArray7[0] = n6 = nArray7[2];
|
||||
nArray8[0] = n9 = nArray8[2];
|
||||
nArray7[1] = nArray[n4];
|
||||
nArray8[1] = nArray2[n4++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,19 @@ public class GddmCoordinateTransform {
|
||||
private double transformX = 1.0;
|
||||
private double transformY = 1.0;
|
||||
|
||||
// Aspect Ratio parameters (P, Q, R, S) per IBM Host On-Demand (HODDecoder / GSAP)
|
||||
public static final int DEFAULT_P = 1;
|
||||
public static final int DEFAULT_Q = 1;
|
||||
public static final int DEFAULT_R = 0;
|
||||
public static final int DEFAULT_S = 0;
|
||||
|
||||
private int paramP = DEFAULT_P;
|
||||
private int paramQ = DEFAULT_Q;
|
||||
private int paramR = DEFAULT_R;
|
||||
private int paramS = DEFAULT_S;
|
||||
private double scaleX = 1.0;
|
||||
private double scaleY = 1.0;
|
||||
|
||||
public GddmCoordinateTransform() {
|
||||
this(80, 24, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
|
||||
}
|
||||
@@ -270,4 +283,68 @@ public class GddmCoordinateTransform {
|
||||
public int convertYtoRow(int y) {
|
||||
return (defaultCharHeight > 0) ? (y / defaultCharHeight) + 1 : 1;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Aspect Ratio & Coordinate Inversion (Phase 17.2)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Sets GOCA aspect ratio parameters P, Q, R, S and updates scaling factors.
|
||||
* ScaleX = P/Q, ScaleY = R/S. Defaults: P=1, Q=1, R=0, S=0.
|
||||
*/
|
||||
public void setAspectRatio(int p, int q, int r, int s) {
|
||||
this.paramP = p;
|
||||
this.paramQ = q;
|
||||
this.paramR = r;
|
||||
this.paramS = s;
|
||||
this.scaleX = (q != 0) ? ((double) p / (double) q) : 1.0;
|
||||
this.scaleY = (s != 0) ? ((double) r / (double) s) : 1.0;
|
||||
if (this.scaleX <= 0.0) this.scaleX = 1.0;
|
||||
if (this.scaleY <= 0.0) this.scaleY = 1.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets aspect ratio parameters to defaults: P=1, Q=1, R=0, S=0.
|
||||
*/
|
||||
public void resetAspectRatio() {
|
||||
setAspectRatio(DEFAULT_P, DEFAULT_Q, DEFAULT_R, DEFAULT_S);
|
||||
}
|
||||
|
||||
public int getParamP() { return paramP; }
|
||||
public int getParamQ() { return paramQ; }
|
||||
public int getParamR() { return paramR; }
|
||||
public int getParamS() { return paramS; }
|
||||
|
||||
public double getScaleX() { return scaleX; }
|
||||
public double getScaleY() { return scaleY; }
|
||||
|
||||
/**
|
||||
* Inverts GOCA Cartesian Y (positive upward from bottom-left) to raster Y (top-down):
|
||||
* Y_screen = (originY - gocaY) * ScaleY.
|
||||
*/
|
||||
public int mapGocaYToScreen(int originY, int gocaY) {
|
||||
return (int) Math.round((originY - gocaY) * scaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverts GOCA Cartesian Y to raster screen Y using the transform's default origin YMax.
|
||||
*/
|
||||
public int mapGocaYToScreen(int gocaY) {
|
||||
return mapGocaYToScreen(getYMax(), gocaY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverts raster Y (top-down) back to GOCA Cartesian Y (positive upward from bottom-left):
|
||||
* Y_goca = originY - (screenY / ScaleY).
|
||||
*/
|
||||
public int mapScreenYToGoca(int originY, int screenY) {
|
||||
return originY - (scaleY > 0 ? (int) Math.round(screenY / scaleY) : screenY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverts raster screen Y back to GOCA Cartesian Y using the transform's default origin YMax.
|
||||
*/
|
||||
public int mapScreenYToGoca(int screenY) {
|
||||
return mapScreenYToGoca(getYMax(), screenY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,15 @@ public class GocaDecoder {
|
||||
private double charHeight = 16.0;
|
||||
private int charSet = 0;
|
||||
private int charPrecision = GocaConstants.CP_STRING;
|
||||
private int arcParamP = 1;
|
||||
private int arcParamQ = 0;
|
||||
private int arcParamR = 0;
|
||||
private int arcParamS = 1;
|
||||
public static final int DEFAULT_ARC_P = 1;
|
||||
public static final int DEFAULT_ARC_Q = 1;
|
||||
public static final int DEFAULT_ARC_R = 0;
|
||||
public static final int DEFAULT_ARC_S = 0;
|
||||
|
||||
private int arcParamP = DEFAULT_ARC_P;
|
||||
private int arcParamQ = DEFAULT_ARC_Q;
|
||||
private int arcParamR = DEFAULT_ARC_R;
|
||||
private int arcParamS = DEFAULT_ARC_S;
|
||||
|
||||
private boolean segChained = false;
|
||||
private boolean segDynamic = false;
|
||||
@@ -77,6 +82,7 @@ public class GocaDecoder {
|
||||
private int imgHeight = 0;
|
||||
private int imgBitDepth = GocaConstants.BPP_1;
|
||||
private int imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
private int imgMultiplier = 1;
|
||||
private final List<Byte> imgBuffer = new ArrayList<>();
|
||||
|
||||
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
|
||||
@@ -368,7 +374,15 @@ public class GocaDecoder {
|
||||
inImage = false;
|
||||
imgBitDepth = GocaConstants.BPP_1;
|
||||
imgCompression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
imgMultiplier = 1;
|
||||
imgBuffer.clear();
|
||||
arcParamP = DEFAULT_ARC_P;
|
||||
arcParamQ = DEFAULT_ARC_Q;
|
||||
arcParamR = DEFAULT_ARC_R;
|
||||
arcParamS = DEFAULT_ARC_S;
|
||||
if (plane != null && plane.getTransform() != null) {
|
||||
plane.getTransform().setAspectRatio(arcParamP, arcParamQ, arcParamR, arcParamS);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] partialOrderBuffer = new byte[0];
|
||||
@@ -567,7 +581,10 @@ public class GocaDecoder {
|
||||
int w = 0, h = 0;
|
||||
int bitDepth = GocaConstants.BPP_1;
|
||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
int imult = 1;
|
||||
if (payloadLen >= 6 && idx + 2 + payloadLen <= end) {
|
||||
imult = readCoord(inputData, idx + 2);
|
||||
if (imult <= 0) imult = 1;
|
||||
w = readCoord(inputData, idx + 4);
|
||||
h = readCoord(inputData, idx + 6);
|
||||
if (payloadLen >= 7) {
|
||||
@@ -592,7 +609,7 @@ public class GocaDecoder {
|
||||
compression = inputData[idx + 7] & 0xFF;
|
||||
}
|
||||
}
|
||||
beginImage(curX, curY, w, h, bitDepth, compression);
|
||||
beginImage(curX, curY, w, h, bitDepth, compression, imult);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
@@ -672,13 +689,25 @@ public class GocaDecoder {
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSAP: { // Arc Parameters (0x22)
|
||||
case GocaConstants.G_GSAP: { // Arc Parameters / Set Aspect Ratio (0x22)
|
||||
if (idx + 9 < end) {
|
||||
arcParamP = readCoord(inputData, idx + 2);
|
||||
arcParamQ = readCoord(inputData, idx + 4);
|
||||
arcParamR = readCoord(inputData, idx + 6);
|
||||
arcParamS = readCoord(inputData, idx + 8);
|
||||
} else {
|
||||
arcParamP = DEFAULT_ARC_P;
|
||||
arcParamQ = DEFAULT_ARC_Q;
|
||||
arcParamR = DEFAULT_ARC_R;
|
||||
arcParamS = DEFAULT_ARC_S;
|
||||
}
|
||||
if (plane != null && plane.getTransform() != null) {
|
||||
plane.getTransform().setAspectRatio(arcParamP, arcParamQ, arcParamR, arcParamS);
|
||||
}
|
||||
logger.info(String.format("GOCA GSAP: P=%d Q=%d R=%d S=%d (ScaleX=%.2f ScaleY=%.2f)",
|
||||
arcParamP, arcParamQ, arcParamR, arcParamS,
|
||||
(arcParamQ != 0 ? (double) arcParamP / arcParamQ : 1.0),
|
||||
(arcParamS != 0 ? (double) arcParamR / arcParamS : 1.0)));
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
@@ -1012,7 +1041,10 @@ public class GocaDecoder {
|
||||
int w, h;
|
||||
int bitDepth = GocaConstants.BPP_1;
|
||||
int compression = GocaConstants.IMG_UNCOMPRESSED;
|
||||
int imult = 1;
|
||||
if (payloadLen >= 10) {
|
||||
imult = readCoord(inputData, idx + 6);
|
||||
if (imult <= 0) imult = 1;
|
||||
w = readCoord(inputData, idx + 8);
|
||||
h = readCoord(inputData, idx + 10);
|
||||
if (payloadLen >= 11) {
|
||||
@@ -1037,7 +1069,7 @@ public class GocaDecoder {
|
||||
compression = inputData[idx + 11] & 0xFF;
|
||||
}
|
||||
}
|
||||
beginImage(x, y, w, h, bitDepth, compression);
|
||||
beginImage(x, y, w, h, bitDepth, compression, imult);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
@@ -1309,10 +1341,14 @@ public class GocaDecoder {
|
||||
}
|
||||
|
||||
private void beginImage(int x, int y, int w, int h) {
|
||||
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED);
|
||||
beginImage(x, y, w, h, GocaConstants.BPP_1, GocaConstants.IMG_UNCOMPRESSED, 1);
|
||||
}
|
||||
|
||||
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression) {
|
||||
beginImage(x, y, w, h, bitDepth, compression, 1);
|
||||
}
|
||||
|
||||
private void beginImage(int x, int y, int w, int h, int bitDepth, int compression, int multiplier) {
|
||||
this.inImage = true;
|
||||
this.imgX = x;
|
||||
this.imgY = y;
|
||||
@@ -1320,6 +1356,7 @@ public class GocaDecoder {
|
||||
this.imgHeight = h;
|
||||
this.imgBitDepth = bitDepth;
|
||||
this.imgCompression = compression;
|
||||
this.imgMultiplier = (multiplier > 0) ? multiplier : 1;
|
||||
this.imgBuffer.clear();
|
||||
}
|
||||
|
||||
@@ -1335,7 +1372,7 @@ public class GocaDecoder {
|
||||
|
||||
int px = plane.mapX(imgX);
|
||||
int py = plane.mapY(imgY);
|
||||
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression);
|
||||
plane.drawImage(px, py, imgWidth, imgHeight, bytes, curColor, imgBitDepth, imgCompression, imgMultiplier);
|
||||
inImage = false;
|
||||
imgBuffer.clear();
|
||||
}
|
||||
@@ -1429,38 +1466,191 @@ public class GocaDecoder {
|
||||
|
||||
private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) {
|
||||
int pos = off;
|
||||
int end = off + len;
|
||||
|
||||
if (isFull) {
|
||||
// GFARC (0xC7) or GCFARC (0x87) Full Arc / Ellipse
|
||||
int centerX = curX;
|
||||
int centerY = curY;
|
||||
if (!fromCurPos && pos + 4 <= end) {
|
||||
centerX = readCoord(data, pos);
|
||||
centerY = readCoord(data, pos + 2);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
double multiplier = 1.0;
|
||||
if (pos + 2 <= end) {
|
||||
int intPart = data[pos];
|
||||
int fracPart = data[pos + 1] & 0xFF;
|
||||
multiplier = intPart + fracPart / 256.0;
|
||||
if (multiplier <= 0.0) multiplier = 1.0;
|
||||
pos += 2;
|
||||
}
|
||||
|
||||
double dP = arcParamP * multiplier;
|
||||
double dQ = arcParamQ * multiplier;
|
||||
double dR = arcParamR * multiplier;
|
||||
double dS = arcParamS * multiplier;
|
||||
|
||||
int[] arcX = new int[256];
|
||||
int[] arcY = new int[256];
|
||||
int nPts = HODEllipse.getHODEllipse(dP, dQ, dR, dS, arcX, arcY, centerX, centerY);
|
||||
|
||||
for (int i = 0; i < nPts; i++) {
|
||||
trackPoint(arcX[i], arcY[i]);
|
||||
}
|
||||
|
||||
if (inArea) {
|
||||
for (int i = 0; i < nPts; i++) {
|
||||
if (i == 0 && currentPolyPts == 0) {
|
||||
addAreaLineStart(arcX[i], arcY[i]);
|
||||
} else {
|
||||
addAreaPoint(arcX[i], arcY[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < nPts - 1; i++) {
|
||||
plane.drawLine(plane.mapXDouble(arcX[i]), plane.mapYDouble(arcY[i]),
|
||||
plane.mapXDouble(arcX[i + 1]), plane.mapYDouble(arcY[i + 1]),
|
||||
curColor, lineType, lineWidth);
|
||||
}
|
||||
}
|
||||
|
||||
curX = centerX;
|
||||
curY = centerY;
|
||||
return;
|
||||
}
|
||||
|
||||
// Partial Arc: GARC (0xC6) or GCARC (0x86)
|
||||
// 3-point circular arc definition per GOCA specs: start, intermediate, end
|
||||
int x0 = curX;
|
||||
int y0 = curY;
|
||||
boolean has3Point = false;
|
||||
int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
|
||||
|
||||
if (!fromCurPos && len >= 12 && pos + 12 <= end) {
|
||||
x0 = readCoord(data, pos);
|
||||
y0 = readCoord(data, pos + 2);
|
||||
x1 = readCoord(data, pos + 4);
|
||||
y1 = readCoord(data, pos + 6);
|
||||
x2 = readCoord(data, pos + 8);
|
||||
y2 = readCoord(data, pos + 10);
|
||||
has3Point = true;
|
||||
} else if (fromCurPos && len >= 8 && pos + 8 <= end) {
|
||||
x1 = readCoord(data, pos);
|
||||
y1 = readCoord(data, pos + 2);
|
||||
x2 = readCoord(data, pos + 4);
|
||||
y2 = readCoord(data, pos + 6);
|
||||
has3Point = true;
|
||||
} else if (!fromCurPos && len >= 8 && pos + 8 <= end) {
|
||||
x1 = readCoord(data, pos);
|
||||
y1 = readCoord(data, pos + 2);
|
||||
x2 = readCoord(data, pos + 4);
|
||||
y2 = readCoord(data, pos + 6);
|
||||
has3Point = true;
|
||||
}
|
||||
|
||||
if (has3Point) {
|
||||
double d = 2.0 * (x0 * (y1 - y2) + x1 * (y2 - y0) + x2 * (y0 - y1));
|
||||
if (Math.abs(d) < 1e-6) {
|
||||
// Collinear points: draw straight line segments P0 -> P1 -> P2
|
||||
trackPoint(x0, y0);
|
||||
trackPoint(x1, y1);
|
||||
trackPoint(x2, y2);
|
||||
if (inArea) {
|
||||
if (currentPolyPts == 0) addAreaLineStart(x0, y0);
|
||||
addAreaPoint(x1, y1);
|
||||
addAreaPoint(x2, y2);
|
||||
} else {
|
||||
plane.drawLine(plane.mapXDouble(x0), plane.mapYDouble(y0),
|
||||
plane.mapXDouble(x1), plane.mapYDouble(y1),
|
||||
curColor, lineType, lineWidth);
|
||||
plane.drawLine(plane.mapXDouble(x1), plane.mapYDouble(y1),
|
||||
plane.mapXDouble(x2), plane.mapYDouble(y2),
|
||||
curColor, lineType, lineWidth);
|
||||
}
|
||||
curX = x2;
|
||||
curY = y2;
|
||||
return;
|
||||
}
|
||||
|
||||
double x0sq = (double) x0 * x0 + (double) y0 * y0;
|
||||
double x1sq = (double) x1 * x1 + (double) y1 * y1;
|
||||
double x2sq = (double) x2 * x2 + (double) y2 * y2;
|
||||
double ccx = (x0sq * (y1 - y2) + x1sq * (y2 - y0) + x2sq * (y0 - y1)) / d;
|
||||
double ccy = (x0sq * (x2 - x1) + x1sq * (x0 - x2) + x2sq * (x1 - x0)) / d;
|
||||
double rad = Math.hypot(x0 - ccx, y0 - ccy);
|
||||
|
||||
double theta0 = Math.atan2(y0 - ccy, x0 - ccx);
|
||||
double theta1 = Math.atan2(y1 - ccy, x1 - ccx);
|
||||
double theta2 = Math.atan2(y2 - ccy, x2 - ccx);
|
||||
if (theta0 < 0) theta0 += 2 * Math.PI;
|
||||
if (theta1 < 0) theta1 += 2 * Math.PI;
|
||||
if (theta2 < 0) theta2 += 2 * Math.PI;
|
||||
|
||||
double d01 = theta1 - theta0;
|
||||
while (d01 < 0) d01 += 2 * Math.PI;
|
||||
double d02 = theta2 - theta0;
|
||||
while (d02 < 0) d02 += 2 * Math.PI;
|
||||
|
||||
double sweep = (d01 < d02) ? d02 : (d02 - 2 * Math.PI);
|
||||
|
||||
int steps = Math.max(8, (int) Math.round(Math.abs(Math.toDegrees(sweep)) / 5.0));
|
||||
int prevX = x0;
|
||||
int prevY = y0;
|
||||
trackPoint(x0, y0);
|
||||
if (inArea && currentPolyPts == 0) {
|
||||
addAreaLineStart(x0, y0);
|
||||
}
|
||||
|
||||
for (int s = 1; s <= steps; s++) {
|
||||
double t = (double) s / steps;
|
||||
double angle = theta0 + t * sweep;
|
||||
int px = (s == steps) ? x2 : (int) Math.round(ccx + rad * Math.cos(angle));
|
||||
int py = (s == steps) ? y2 : (int) Math.round(ccy + rad * Math.sin(angle));
|
||||
trackPoint(px, py);
|
||||
if (inArea) {
|
||||
addAreaPoint(px, py);
|
||||
} else {
|
||||
plane.drawLine(plane.mapXDouble(prevX), plane.mapYDouble(prevY),
|
||||
plane.mapXDouble(px), plane.mapYDouble(py),
|
||||
curColor, lineType, lineWidth);
|
||||
}
|
||||
prevX = px;
|
||||
prevY = py;
|
||||
}
|
||||
|
||||
curX = x2;
|
||||
curY = y2;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: sweep angle parameter decoding
|
||||
int centerX = curX;
|
||||
int centerY = curY;
|
||||
|
||||
// For absolute variants, read the center point from data
|
||||
if (!fromCurPos && pos + 4 <= off + len) {
|
||||
if (!fromCurPos && pos + 4 <= end) {
|
||||
centerX = readCoord(data, pos);
|
||||
centerY = readCoord(data, pos + 2);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
// Read the multiplier (integer.fraction format)
|
||||
double multiplier = 1.0;
|
||||
if (pos + 2 <= off + len) {
|
||||
int intPart = data[pos]; // signed byte
|
||||
if (pos + 2 <= end) {
|
||||
int intPart = data[pos];
|
||||
int fracPart = data[pos + 1] & 0xFF;
|
||||
multiplier = intPart + fracPart / 256.0;
|
||||
if (multiplier == 0.0) multiplier = 1.0;
|
||||
pos += 2;
|
||||
}
|
||||
|
||||
// Compute semi-axes from arc parameters scaled by multiplier
|
||||
// P,Q and R,S are conjugate semi-diameter vectors
|
||||
double pScaled = arcParamP * multiplier;
|
||||
double qScaled = arcParamQ * multiplier;
|
||||
double rScaled = arcParamR * multiplier;
|
||||
double sScaled = arcParamS * multiplier;
|
||||
|
||||
// Semi-axis lengths in GOCA coordinate space
|
||||
double semiAxis1 = Math.sqrt(pScaled * pScaled + qScaled * qScaled);
|
||||
double semiAxis2 = Math.sqrt(rScaled * rScaled + sScaled * sScaled);
|
||||
|
||||
// If arc parameters are at default (1,0,0,1) and curPos != center, use distance to center as radius
|
||||
double dist = Math.hypot(curX - centerX, curY - centerY);
|
||||
if (semiAxis1 <= 1.01 && dist > 1.0) {
|
||||
semiAxis1 = dist * multiplier;
|
||||
@@ -1470,7 +1660,6 @@ public class GocaDecoder {
|
||||
if (semiAxis1 < 1.0) semiAxis1 = 1.0;
|
||||
if (semiAxis2 < 1.0) semiAxis2 = 1.0;
|
||||
|
||||
// Map to pixel space with double precision
|
||||
double cx = plane.mapXDouble(centerX);
|
||||
double cy = plane.mapYDouble(centerY);
|
||||
double rx = Math.abs(plane.mapXDouble(semiAxis1) - plane.mapXDouble(0));
|
||||
@@ -1478,38 +1667,24 @@ public class GocaDecoder {
|
||||
if (rx <= 0.0) rx = Math.max(1.0, semiAxis1);
|
||||
if (ry <= 0.0) ry = Math.max(1.0, semiAxis2);
|
||||
|
||||
// For partial arcs, determine start angle and sweep angle
|
||||
double startAngleDeg = 0.0;
|
||||
double sweepAngleDeg = 360.0;
|
||||
if (!isFull) {
|
||||
if (pos + 4 <= off + len) {
|
||||
// Sweep start: integer.fraction of full revolution
|
||||
int startInt = data[pos];
|
||||
int startFrac = data[pos + 1] & 0xFF;
|
||||
startAngleDeg = (startInt + startFrac / 256.0) * 360.0;
|
||||
pos += 2;
|
||||
// Sweep angle: integer.fraction of full revolution
|
||||
int sweepInt = data[pos];
|
||||
int sweepFrac = data[pos + 1] & 0xFF;
|
||||
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
||||
} else if (pos + 2 <= off + len) {
|
||||
// Single sweep parameter
|
||||
int sweepInt = data[pos];
|
||||
int sweepFrac = data[pos + 1] & 0xFF;
|
||||
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
||||
if (curX != centerX || curY != centerY) {
|
||||
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
|
||||
startAngleDeg = Math.toDegrees(startRad);
|
||||
if (startAngleDeg < 0) startAngleDeg += 360.0;
|
||||
}
|
||||
} else {
|
||||
// No sweep data — if we have a current point, start there and sweep full circle
|
||||
if (curX != centerX || curY != centerY) {
|
||||
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
|
||||
startAngleDeg = Math.toDegrees(startRad);
|
||||
if (startAngleDeg < 0) startAngleDeg += 360.0;
|
||||
}
|
||||
sweepAngleDeg = 360.0;
|
||||
if (pos + 4 <= end) {
|
||||
int startInt = data[pos];
|
||||
int startFrac = data[pos + 1] & 0xFF;
|
||||
startAngleDeg = (startInt + startFrac / 256.0) * 360.0;
|
||||
pos += 2;
|
||||
int sweepInt = data[pos];
|
||||
int sweepFrac = data[pos + 1] & 0xFF;
|
||||
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
||||
} else if (pos + 2 <= end) {
|
||||
int sweepInt = data[pos];
|
||||
int sweepFrac = data[pos + 1] & 0xFF;
|
||||
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
||||
if (curX != centerX || curY != centerY) {
|
||||
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
|
||||
startAngleDeg = Math.toDegrees(startRad);
|
||||
if (startAngleDeg < 0) startAngleDeg += 360.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1533,7 +1708,7 @@ public class GocaDecoder {
|
||||
}
|
||||
|
||||
plane.drawArc(cx, cy, rx, ry, startAngleDeg, sweepAngleDeg,
|
||||
curColor, lineType, lineWidth, isFull);
|
||||
curColor, lineType, lineWidth, false);
|
||||
|
||||
curX = centerX;
|
||||
curY = centerY;
|
||||
@@ -1543,63 +1718,62 @@ public class GocaDecoder {
|
||||
int pos = off;
|
||||
int end = off + len;
|
||||
|
||||
List<Double> ptsX = new ArrayList<>();
|
||||
List<Double> ptsY = new ArrayList<>();
|
||||
List<Integer> gocaPtsX = new ArrayList<>();
|
||||
List<Integer> gocaPtsY = new ArrayList<>();
|
||||
|
||||
if (fromCurPos) {
|
||||
trackPoint(curX, curY);
|
||||
ptsX.add(plane.mapXDouble(curX));
|
||||
ptsY.add(plane.mapYDouble(curY));
|
||||
gocaPtsX.add(curX);
|
||||
gocaPtsY.add(curY);
|
||||
if (inArea && currentPolyPts == 0) {
|
||||
addAreaLineStart(curX, curY);
|
||||
}
|
||||
}
|
||||
|
||||
while (pos + 4 <= end) {
|
||||
int x = readCoord(data, pos);
|
||||
int y = readCoord(data, pos + 2);
|
||||
trackPoint(x, y);
|
||||
ptsX.add(plane.mapXDouble(x));
|
||||
ptsY.add(plane.mapYDouble(y));
|
||||
gocaPtsX.add(x);
|
||||
gocaPtsY.add(y);
|
||||
if (inArea && ptsX.size() == 1 && currentPolyPts == 0) {
|
||||
addAreaLineStart(x, y);
|
||||
}
|
||||
curX = x;
|
||||
curY = y;
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
if (ptsX.size() >= 2) {
|
||||
int n = ptsX.size();
|
||||
double[] px = new double[n];
|
||||
double[] py = new double[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
px[i] = ptsX.get(i);
|
||||
py[i] = ptsY.get(i);
|
||||
}
|
||||
int n = gocaPtsX.size();
|
||||
if (n < 2) return;
|
||||
|
||||
int[] ctrlX = new int[n];
|
||||
int[] ctrlY = new int[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
ctrlX[i] = gocaPtsX.get(i);
|
||||
ctrlY[i] = gocaPtsY.get(i);
|
||||
trackPoint(ctrlX[i], ctrlY[i]);
|
||||
}
|
||||
|
||||
FilletPts fillet = new FilletPts();
|
||||
int req = fillet.getPointsRequired(n);
|
||||
int[] outX = new int[Math.max(req, n * 32)];
|
||||
int[] outY = new int[Math.max(req, n * 32)];
|
||||
int nPoints = fillet.getFilletPoints(ctrlX, ctrlY, n, outX, outY);
|
||||
|
||||
if (nPoints > 0) {
|
||||
for (int i = 0; i < nPoints; i++) {
|
||||
trackPoint(outX[i], outY[i]);
|
||||
}
|
||||
if (inArea) {
|
||||
int gn = gocaPtsX.size();
|
||||
int[] gx = new int[gn];
|
||||
int[] gy = new int[gn];
|
||||
for (int i = 0; i < gn; i++) {
|
||||
gx[i] = gocaPtsX.get(i);
|
||||
gy[i] = gocaPtsY.get(i);
|
||||
}
|
||||
int[][] curve = FilletPts.calculate(gx, gy, gn, 16);
|
||||
for (int i = 1; i < curve[0].length; i++) {
|
||||
addAreaPoint(curve[0][i], curve[1][i]);
|
||||
for (int i = 0; i < nPoints; i++) {
|
||||
if (i == 0 && currentPolyPts == 0) {
|
||||
addAreaLineStart(outX[0], outY[0]);
|
||||
} else {
|
||||
addAreaPoint(outX[i], outY[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
plane.drawFillet(px, py, n, curColor, lineType, lineWidth);
|
||||
for (int i = 0; i < nPoints - 1; i++) {
|
||||
plane.drawLine(plane.mapXDouble(outX[i]), plane.mapYDouble(outY[i]),
|
||||
plane.mapXDouble(outX[i + 1]), plane.mapYDouble(outY[i + 1]),
|
||||
curColor, lineType, lineWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
curX = ctrlX[n - 1];
|
||||
curY = ctrlY[n - 1];
|
||||
}
|
||||
|
||||
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
|
||||
@@ -1824,6 +1998,15 @@ public class GocaDecoder {
|
||||
resetDefaults();
|
||||
}
|
||||
|
||||
public synchronized int getArcParamP() { return arcParamP; }
|
||||
public synchronized int getArcParamQ() { return arcParamQ; }
|
||||
public synchronized int getArcParamR() { return arcParamR; }
|
||||
public synchronized int getArcParamS() { return arcParamS; }
|
||||
public synchronized int getImageMultiplier() { return imgMultiplier; }
|
||||
public synchronized boolean hasActiveGraphics() {
|
||||
return graphicsCursorActive || (plane != null && plane.hasContent());
|
||||
}
|
||||
|
||||
private int readCoord(byte[] data, int off) {
|
||||
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
|
||||
}
|
||||
|
||||
@@ -1159,7 +1159,9 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
setPixelInFill(x, y, fill, isMixOr);
|
||||
int pixColor = (psSlot.isTriplePlane() && psSlot.getRgbPixels(fill, bg) != null)
|
||||
? psSlot.getRgbPixels(fill, bg)[pIdx] : fill;
|
||||
setPixelInFill(x, y, pixColor, isMixOr);
|
||||
} else if (isOpaqueBg) {
|
||||
setPixelInFill(x, y, bg, isMixOr);
|
||||
}
|
||||
@@ -1192,7 +1194,9 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
int pIdx = psY * psW + psX;
|
||||
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
|
||||
if (bit) {
|
||||
setPixelInFill(x, y, fill, isMixOr);
|
||||
int pixColor = (psSlot.isTriplePlane() && psSlot.getRgbPixels(fill, bg) != null)
|
||||
? psSlot.getRgbPixels(fill, bg)[pIdx] : fill;
|
||||
setPixelInFill(x, y, pixColor, isMixOr);
|
||||
} else if (isOpaqueBg) {
|
||||
setPixelInFill(x, y, bg, isMixOr);
|
||||
}
|
||||
@@ -1221,12 +1225,12 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
for (int i = 0; i < pLen - 1; i++) {
|
||||
drawLine((double) px[offset + i], (double) py[offset + i],
|
||||
(double) px[offset + i + 1], (double) py[offset + i + 1],
|
||||
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
|
||||
boundaryColorArgb, lineType, lineWidth);
|
||||
}
|
||||
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
|
||||
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
|
||||
(double) px[offset], (double) py[offset],
|
||||
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
|
||||
boundaryColorArgb, lineType, lineWidth);
|
||||
}
|
||||
}
|
||||
offset += pLen;
|
||||
@@ -1417,6 +1421,16 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, shearAngle);
|
||||
}
|
||||
|
||||
public synchronized void drawVectorString(double x, double y, String text, int colorArgb,
|
||||
double cellWidth, double cellHeight, int dir, double angle, double shearAngle) {
|
||||
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle, shearAngle);
|
||||
}
|
||||
|
||||
public synchronized void drawVectorString(int x, int y, String text, int colorArgb,
|
||||
int cellWidth, int cellHeight, int dir, double angle) {
|
||||
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle, 0.0);
|
||||
}
|
||||
|
||||
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
|
||||
drawVssChar(x, y, c, color, cw, ch, 0.0, 0.0);
|
||||
}
|
||||
@@ -1530,8 +1544,17 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
*/
|
||||
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
|
||||
int bitDepth, int compressionMode) {
|
||||
drawImage(x, y, width, height, imageData, fgColorArgb, bitDepth, compressionMode, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws bitmap image with support for bit depth, RLE decompression, and image multiplier (imult).
|
||||
*/
|
||||
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb,
|
||||
int bitDepth, int compressionMode, int multiplier) {
|
||||
if (imageData == null || width <= 0 || height <= 0) return;
|
||||
int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
|
||||
int m = (multiplier > 0) ? multiplier : 1;
|
||||
|
||||
byte[] rawData = imageData;
|
||||
if (compressionMode == GocaConstants.IMG_RLE) {
|
||||
@@ -1549,7 +1572,15 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
if (byteIdx < rawData.length) {
|
||||
boolean bit = ((rawData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
|
||||
if (bit) {
|
||||
setPixel(x + col, y + row, fgColor);
|
||||
if (m <= 1) {
|
||||
setPixel(x + col, y + row, fgColor);
|
||||
} else {
|
||||
for (int dy = 0; dy < m; dy++) {
|
||||
for (int dx = 0; dx < m; dx++) {
|
||||
setPixel(x + col * m + dx, y + row * m + dy, fgColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1572,7 +1603,15 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
case 3: pixelColor = GocaConstants.GOCA_COLORS[4]; break; // Green
|
||||
default: pixelColor = fgColor; break;
|
||||
}
|
||||
setPixel(x + col, y + row, pixelColor);
|
||||
if (m <= 1) {
|
||||
setPixel(x + col, y + row, pixelColor);
|
||||
} else {
|
||||
for (int dy = 0; dy < m; dy++) {
|
||||
for (int dx = 0; dx < m; dx++) {
|
||||
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1588,7 +1627,16 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
int shift = (1 - (col % 2)) * 4;
|
||||
int val = (rawData[byteIdx] >> shift) & 0x0F;
|
||||
if (val != 0) {
|
||||
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
|
||||
int pixelColor = GocaConstants.getGocaColorArgb(val);
|
||||
if (m <= 1) {
|
||||
setPixel(x + col, y + row, pixelColor);
|
||||
} else {
|
||||
for (int dy = 0; dy < m; dy++) {
|
||||
for (int dx = 0; dx < m; dx++) {
|
||||
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1602,7 +1650,16 @@ public class GraphicsPlane implements PixelBuffer {
|
||||
if (byteIdx < rawData.length) {
|
||||
int val = rawData[byteIdx] & 0xFF;
|
||||
if (val != 0) {
|
||||
setPixel(x + col, y + row, GocaConstants.getGocaColorArgb(val));
|
||||
int pixelColor = GocaConstants.getGocaColorArgb(val);
|
||||
if (m <= 1) {
|
||||
setPixel(x + col, y + row, pixelColor);
|
||||
} else {
|
||||
for (int dy = 0; dy < m; dy++) {
|
||||
for (int dx = 0; dx < m; dx++) {
|
||||
setPixel(x + col * m + dx, y + row * m + dy, pixelColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package haus.nightmare.lib3270j.graphics;
|
||||
|
||||
/**
|
||||
* Calculates intermediate points for IBM GOCA ellipses and arcs using conjugate semi-axes P, Q, R, S.
|
||||
* Matches IBM Host On-Demand (com.ibm.eNetwork.ECL.hostgraphics.HODEllipse and HODGraphUtil).
|
||||
*/
|
||||
public class HODEllipse {
|
||||
|
||||
protected static final double[] dXgxdq = new double[] {
|
||||
1.0, 0.999847695, 0.999390827, 0.998629534, 0.99756405, 0.996194698,
|
||||
0.994521895, 0.992546151, 0.990268068, 0.98768834, 0.984807753, 0.981627183,
|
||||
0.9781476, 0.974370064, 0.970295726, 0.965925826, 0.961261695, 0.956304756,
|
||||
0.951056516, 0.945518575, 0.93969262, 0.933580426, 0.927183854, 0.920504853,
|
||||
0.913545457, 0.906307787, 0.898794046, 0.891006524, 0.882947592, 0.874619707,
|
||||
0.866025403, 0.8571673, 0.848048096, 0.838670567, 0.829037572, 0.819152044,
|
||||
0.809016994, 0.79863551, 0.788010753, 0.777145961, 0.766044443, 0.75470958,
|
||||
0.743144825, 0.731353701, 0.7193398, 0.707106781
|
||||
};
|
||||
|
||||
protected static final double[] dXgydq = new double[] {
|
||||
0.0, 0.017452406, 0.034899496, 0.052335956, 0.069756473, 0.087155742,
|
||||
0.104528463, 0.121869343, 0.139173101, 0.156434465, 0.173648177, 0.190808995,
|
||||
0.20791169, 0.224951054, 0.241921895, 0.258819045, 0.275637355, 0.292371704,
|
||||
0.309016994, 0.325568154, 0.342020143, 0.358367949, 0.374606593, 0.390731128,
|
||||
0.406736643, 0.422618261, 0.438371146, 0.453990499, 0.469471562, 0.48480962,
|
||||
0.5, 0.515038074, 0.529919264, 0.544639035, 0.559192903, 0.573576436,
|
||||
0.587785252, 0.601815023, 0.615661475, 0.629320391, 0.642787609, 0.656059029,
|
||||
0.669130606, 0.68199836, 0.69465837, 0.707106781
|
||||
};
|
||||
|
||||
protected double dP;
|
||||
protected double dQ;
|
||||
protected double dR;
|
||||
protected double dS;
|
||||
protected int iP;
|
||||
protected int iQ;
|
||||
protected int iR;
|
||||
protected int iS;
|
||||
protected double dRad;
|
||||
protected double dCenterX;
|
||||
protected double dCenterY;
|
||||
protected double[] dXq = new double[9];
|
||||
protected double[] dYq = new double[9];
|
||||
|
||||
public HODEllipse(double p, double q, double r, double s) {
|
||||
this.dP = p;
|
||||
this.dQ = q;
|
||||
this.dR = r;
|
||||
this.dS = s;
|
||||
this.iP = (int) Math.round(p);
|
||||
this.iQ = (int) Math.round(q);
|
||||
this.iR = (int) Math.round(r);
|
||||
this.iS = (int) Math.round(s);
|
||||
}
|
||||
|
||||
public int getHODPoints(int[] outX, int[] outY, int centerX, int centerY) {
|
||||
if (outX == null || outY == null || outX.length < 128 || outY.length < 128) {
|
||||
return 0;
|
||||
}
|
||||
int ptIdx = 0;
|
||||
int n5 = Math.max(Math.abs(this.iP) + Math.abs(this.iS), Math.abs(this.iR) + Math.abs(this.iQ));
|
||||
this.dRad = n5;
|
||||
this.dCenterX = centerX;
|
||||
this.dCenterY = centerY;
|
||||
if (n5 == 0) {
|
||||
outX[0] = centerX;
|
||||
outY[0] = centerY;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int n6 = this.bres(this.dRad);
|
||||
int lastX = this.iP + centerX;
|
||||
int lastY = this.iS + centerY;
|
||||
|
||||
for (int i = 0; i <= n6; ++i) {
|
||||
this.filletHODSegment(outX, outY, ptIdx, this.dXq[i], this.dYq[i]);
|
||||
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||
lastX = outX[ptIdx];
|
||||
lastY = outY[ptIdx++];
|
||||
}
|
||||
for (int i = n6; i >= 0; --i) {
|
||||
this.filletHODSegment(outX, outY, ptIdx, this.dYq[i], this.dXq[i]);
|
||||
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||
lastX = outX[ptIdx];
|
||||
lastY = outY[ptIdx++];
|
||||
}
|
||||
for (int i = 0; i <= n6; ++i) {
|
||||
this.filletHODSegment(outX, outY, ptIdx, -this.dYq[i], this.dXq[i]);
|
||||
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||
lastX = outX[ptIdx];
|
||||
lastY = outY[ptIdx++];
|
||||
}
|
||||
for (int i = n6; i >= 0; --i) {
|
||||
this.filletHODSegment(outX, outY, ptIdx, -this.dXq[i], this.dYq[i]);
|
||||
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||
lastX = outX[ptIdx];
|
||||
lastY = outY[ptIdx++];
|
||||
}
|
||||
for (int i = 0; i <= n6; ++i) {
|
||||
this.filletHODSegment(outX, outY, ptIdx, -this.dXq[i], -this.dYq[i]);
|
||||
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||
lastX = outX[ptIdx];
|
||||
lastY = outY[ptIdx++];
|
||||
}
|
||||
for (int i = n6; i >= 0; --i) {
|
||||
this.filletHODSegment(outX, outY, ptIdx, -this.dYq[i], -this.dXq[i]);
|
||||
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||
lastX = outX[ptIdx];
|
||||
lastY = outY[ptIdx++];
|
||||
}
|
||||
for (int i = 0; i <= n6; ++i) {
|
||||
this.filletHODSegment(outX, outY, ptIdx, this.dYq[i], -this.dXq[i]);
|
||||
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||
lastX = outX[ptIdx];
|
||||
lastY = outY[ptIdx++];
|
||||
}
|
||||
for (int i = n6; i >= 0; --i) {
|
||||
this.filletHODSegment(outX, outY, ptIdx, this.dXq[i], -this.dYq[i]);
|
||||
if (lastX == outX[ptIdx] && lastY == outY[ptIdx]) continue;
|
||||
lastX = outX[ptIdx];
|
||||
lastY = outY[ptIdx++];
|
||||
}
|
||||
|
||||
outX[ptIdx] = outX[0];
|
||||
outY[ptIdx++] = outY[0];
|
||||
return ptIdx;
|
||||
}
|
||||
|
||||
protected int bres(double rad) {
|
||||
for (int i = 0; i < 9; ++i) {
|
||||
this.dXq[i] = rad * dXgxdq[i * 5 + 2];
|
||||
this.dYq[i] = rad * dXgydq[i * 5 + 2];
|
||||
}
|
||||
return 8;
|
||||
}
|
||||
|
||||
protected void filletHODSegment(int[] outX, int[] outY, int idx, double d, double d2) {
|
||||
outX[idx] = (int) Math.round(this.dP * d / this.dRad + this.dR * d2 / this.dRad + this.dCenterX);
|
||||
outY[idx] = (int) Math.round(-(this.dS * d / this.dRad + this.dQ * d2 / this.dRad) + this.dCenterY);
|
||||
}
|
||||
|
||||
protected void filletHODSegment2(int[] outX, int[] outY, int idx, double d, double d2) {
|
||||
outX[idx] = (int) Math.round(this.dP * d / this.dRad + this.dR * d2 / this.dRad + this.dCenterX);
|
||||
outY[idx] = (int) Math.round(this.dS * d / this.dRad + this.dQ * d2 / this.dRad + this.dCenterY);
|
||||
}
|
||||
|
||||
public static int getHODEllipse(double p, double q, double r, double s, int[] outX, int[] outY, int cx, int cy) {
|
||||
HODEllipse ellipse = new HODEllipse(p, q, r, s);
|
||||
return ellipse.getHODPoints(outX, outY, cx, cy);
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,12 @@ public class ProgramSymbolManager {
|
||||
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
|
||||
|
||||
private int defaultCellWidth = 9;
|
||||
private int defaultCellHeight = 12; // Standard IBM 3279 PS Slot Default Height (SDH = 0x0C = 12)
|
||||
private int defaultCellHeight = 16; // Standard IBM 9x16 Programmed Symbol cell matrix
|
||||
|
||||
|
||||
public void setDefaultCellDimensions(int width, int height) {
|
||||
this.defaultCellWidth = (width > 0) ? width : 9;
|
||||
this.defaultCellHeight = (height > 0) ? height : 12;
|
||||
this.defaultCellHeight = (height > 0) ? height : 16;
|
||||
}
|
||||
|
||||
public int getDefaultCellWidth() {
|
||||
@@ -174,40 +175,56 @@ public class ProgramSymbolManager {
|
||||
int startCodePoint = data[2] & 0xFF;
|
||||
int rws = data[3] & 0xFF;
|
||||
|
||||
int normRws = rws;
|
||||
if (normRws >= 0x40 && normRws <= 0x49) {
|
||||
normRws = normRws - 0x40 + 2; // 0x40 -> 2 (PSA), 0x41 -> 3 (PSB), ..., 0x45 -> 7 (PSF)
|
||||
} else if ((normRws < 2 || normRws > 11) && (lcid >= 0x40 && lcid <= 0x49)) {
|
||||
normRws = lcid - 0x40 + 2;
|
||||
}
|
||||
|
||||
int setIndex;
|
||||
switch (rws) {
|
||||
case 2: setIndex = 0; break;
|
||||
case 3: setIndex = 1; break;
|
||||
case 4: setIndex = 2; break;
|
||||
case 5: setIndex = 3; break;
|
||||
case 6: setIndex = 4; break;
|
||||
case 7: setIndex = 5; break;
|
||||
switch (normRws) {
|
||||
case 2: setIndex = 0; break; // PSA (0x40)
|
||||
case 3: setIndex = 1; break; // PSB (0x41)
|
||||
case 4: setIndex = 2; break; // PSC (0x42)
|
||||
case 5: setIndex = 3; break; // PSD (0x43)
|
||||
case 6: setIndex = 4; break; // PSE (0x44)
|
||||
case 7: setIndex = 5; break; // PSF (0x45)
|
||||
case 8: setIndex = 6; break;
|
||||
case 9: setIndex = 7; break;
|
||||
case 10: setIndex = 8; break;
|
||||
case 11: setIndex = 9; break;
|
||||
default:
|
||||
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
|
||||
return;
|
||||
if (lcid >= 0x40 && lcid < 256) {
|
||||
setIndex = (lcid - 0x40) % NUMBER_SYMBOL_SETS;
|
||||
} else {
|
||||
logger.warning("LOADPS: Invalid RWS slot 0x" + Integer.toHexString(rws));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
||||
ProgramSymbolSet set = sets[setIndex];
|
||||
boolean isTriplePlane = set.isTriplePlane() || (normRws >= 4 && normRws <= 7);
|
||||
|
||||
if (clearAll) {
|
||||
set.clear();
|
||||
}
|
||||
|
||||
int extHeaderLen = 0;
|
||||
int cellWidth = defaultCellWidth;
|
||||
int cellHeight = defaultCellHeight;
|
||||
int cellWidth = (loadFormat == 1) ? 9 : (defaultCellWidth > 0 ? defaultCellWidth : 9);
|
||||
int cellHeight = (loadFormat == 1) ? 16 : (defaultCellHeight > 0 ? defaultCellHeight : 16);
|
||||
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
|
||||
|
||||
if (hasExtHeader && data.length > 4) {
|
||||
extHeaderLen = data[4] & 0xFF;
|
||||
if (extHeaderLen > 3 && data.length > 6) {
|
||||
int lw = data[6] & 0xFF;
|
||||
if (lw > 0) cellWidth = lw;
|
||||
if (lw > 0 && loadFormat != 1) cellWidth = lw;
|
||||
}
|
||||
if (extHeaderLen > 4 && data.length > 7) {
|
||||
int lh = data[7] & 0xFF;
|
||||
if (lh > 0) cellHeight = lh;
|
||||
if (lh > 0 && loadFormat != 1) cellHeight = lh;
|
||||
}
|
||||
if (extHeaderLen >= 6 && data.length > 9) {
|
||||
colorPlane = data[9] & 0xFF;
|
||||
@@ -223,55 +240,34 @@ public class ProgramSymbolManager {
|
||||
int remaining = data.length - offset;
|
||||
int codeIndex = (startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint;
|
||||
|
||||
int sliceBytes = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
|
||||
if (sliceBytes <= 0) sliceBytes = 18;
|
||||
|
||||
int bytesPerSymbol;
|
||||
if (isTriplePlane && colorPlane == 0 && remaining >= sliceBytes * 3) {
|
||||
bytesPerSymbol = sliceBytes * 3;
|
||||
} else {
|
||||
bytesPerSymbol = sliceBytes;
|
||||
}
|
||||
int bytesPerSymbol = (loadFormat == 1) ? 18 : (cellWidth * cellHeight + 7) / 8;
|
||||
if (bytesPerSymbol <= 0) bytesPerSymbol = 18;
|
||||
|
||||
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
||||
byte[] pixelData = new byte[cellWidth * cellHeight];
|
||||
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
|
||||
if (!clearAll && existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
||||
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
|
||||
|
||||
// If loading a specific color plane (colorPlane != 0), merge with existing ONLY IF existing was not all-planes
|
||||
if (isTriplePlane && colorPlane != 0 && existing != null && !existing.isAllPlanesLoaded()
|
||||
&& existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
||||
System.arraycopy(existing.getPixelData(), 0, pixelData, 0,
|
||||
Math.min(existing.getPixelData().length, pixelData.length));
|
||||
}
|
||||
|
||||
if (isTriplePlane && colorPlane == 0 && bytesPerSymbol == sliceBytes * 3) {
|
||||
// 3 consecutive slices: Red (plane 1), Green (plane 2), Blue (plane 4)
|
||||
if (loadFormat == 1) {
|
||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, true, 1);
|
||||
unpackFormat1(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
|
||||
unpackFormat1(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
|
||||
} else {
|
||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, true, 1);
|
||||
unpackFormat3(data, offset + sliceBytes, pixelData, cellWidth, cellHeight, true, 2);
|
||||
unpackFormat3(data, offset + sliceBytes * 2, pixelData, cellWidth, cellHeight, true, 4);
|
||||
}
|
||||
if (loadFormat == 1) {
|
||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
} else {
|
||||
if (loadFormat == 1) {
|
||||
unpackFormat1(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
} else {
|
||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
}
|
||||
unpackFormat3(data, offset, pixelData, cellWidth, cellHeight, isTriplePlane, colorPlane);
|
||||
}
|
||||
|
||||
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane));
|
||||
boolean allPlanes = (colorPlane == 0);
|
||||
set.setSlot(codeIndex, new ProgramSymbolSet.SymbolSlot(cellWidth, cellHeight, pixelData, isTriplePlane, allPlanes));
|
||||
|
||||
codeIndex++;
|
||||
offset += bytesPerSymbol;
|
||||
remaining -= bytesPerSymbol;
|
||||
}
|
||||
|
||||
if (clearAll) {
|
||||
for (int i = codeIndex; i < ProgramSymbolSet.NUM_SLOTS; i++) {
|
||||
set.clearSlot(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isLoggable(Level.FINE)) {
|
||||
logger.fine(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
|
||||
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
|
||||
|
||||
@@ -62,6 +62,7 @@ public class ProgramSymbolSet {
|
||||
private final int height;
|
||||
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
|
||||
private final boolean isTriplePlane;
|
||||
private final boolean allPlanesLoaded;
|
||||
private int[] cachedRgbArray;
|
||||
private int cachedFgRgb = -1;
|
||||
private int cachedBgRgb = -1;
|
||||
@@ -73,10 +74,19 @@ public class ProgramSymbolSet {
|
||||
private int cachedScaledBgRgb = -1;
|
||||
|
||||
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
|
||||
this(width, height, pixelData, isTriplePlane, false);
|
||||
}
|
||||
|
||||
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane, boolean allPlanesLoaded) {
|
||||
this.width = width > 0 ? width : 9;
|
||||
this.height = height > 0 ? height : 16;
|
||||
this.pixelData = pixelData;
|
||||
this.isTriplePlane = isTriplePlane;
|
||||
this.allPlanesLoaded = allPlanesLoaded;
|
||||
}
|
||||
|
||||
public boolean isAllPlanesLoaded() {
|
||||
return allPlanesLoaded;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
@@ -183,18 +193,18 @@ public class ProgramSymbolSet {
|
||||
} else if (!isTriplePlane) {
|
||||
rgbArray[idx] = fgArgb;
|
||||
} else {
|
||||
// Triple-Plane RGB composite aligned with IBM Host On-Demand 16-color GOCA palette:
|
||||
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
|
||||
// Triple-Plane RGB composite aligned with IBM 3179G / HoD GOCA palette:
|
||||
// val bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
|
||||
int colorMask = val & 0x07;
|
||||
int colorArgb;
|
||||
switch (colorMask) {
|
||||
case 1: colorArgb = GocaConstants.GOCA_COLORS[2]; break; // Red
|
||||
case 2: colorArgb = GocaConstants.GOCA_COLORS[4]; break; // Green
|
||||
case 3: colorArgb = GocaConstants.GOCA_COLORS[6]; break; // Yellow
|
||||
case 4: colorArgb = GocaConstants.GOCA_COLORS[1]; break; // Blue (CUSTOMBLUE)
|
||||
case 5: colorArgb = GocaConstants.GOCA_COLORS[3]; break; // Pink
|
||||
case 6: colorArgb = GocaConstants.GOCA_COLORS[5]; break; // Turquoise
|
||||
case 7: colorArgb = GocaConstants.GOCA_COLORS[7]; break; // White
|
||||
case 3: colorArgb = GocaConstants.GOCA_COLORS[6]; break; // Yellow (Red + Green)
|
||||
case 4: colorArgb = GocaConstants.GOCA_COLORS[1]; break; // Blue
|
||||
case 5: colorArgb = GocaConstants.GOCA_COLORS[3]; break; // Pink (Red + Blue)
|
||||
case 6: colorArgb = GocaConstants.GOCA_COLORS[5]; break; // Turquoise (Green + Blue)
|
||||
case 7: colorArgb = GocaConstants.GOCA_COLORS[7]; break; // White (Red + Green + Blue)
|
||||
default: colorArgb = bgArgb; break;
|
||||
}
|
||||
rgbArray[idx] = colorArgb;
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
package haus.nightmare.lib3270j.input;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* IBM Host On-Demand (HoD) .kmp keyboard map profile parser and serializer.
|
||||
* Translates HoD hashKey codes (e.g. A67 -> Alt+C, B35 -> End, A82 -> Alt+R)
|
||||
* and duplicate/numpad key representations.
|
||||
*
|
||||
* Implemented in pure Java standard library for lib3270j.
|
||||
*/
|
||||
public class HodKeyMap {
|
||||
|
||||
public static final int MOD_SHIFT = 1;
|
||||
public static final int MOD_CTRL = 2;
|
||||
public static final int MOD_META = 4;
|
||||
public static final int MOD_ALT = 8;
|
||||
|
||||
public static final int LOCATION_STANDARD = 1;
|
||||
public static final int LOCATION_LEFT = 2;
|
||||
public static final int LOCATION_RIGHT = 3;
|
||||
public static final int LOCATION_NUMPAD = 4;
|
||||
|
||||
public static final int KEY_LOC_LEFT = 0x1000000; // 16777216
|
||||
public static final int KEY_LOC_NUMPAD = 0x2000000; // 33554432
|
||||
|
||||
// HoD duplicate keycodes for numpad arrows
|
||||
public static final int NUMPAD_UP_CODE = 224;
|
||||
public static final int NUMPAD_DOWN_CODE = 225;
|
||||
public static final int NUMPAD_LEFT_CODE = 226;
|
||||
public static final int NUMPAD_RIGHT_CODE = 227;
|
||||
|
||||
// Standard AWT VK codes
|
||||
public static final int VK_ENTER = 10;
|
||||
public static final int VK_CLEAR = 12;
|
||||
public static final int VK_SHIFT = 16;
|
||||
public static final int VK_CONTROL = 17;
|
||||
public static final int VK_ALT = 18;
|
||||
public static final int VK_PAGE_UP = 33;
|
||||
public static final int VK_PAGE_DOWN = 34;
|
||||
public static final int VK_END = 35;
|
||||
public static final int VK_HOME = 36;
|
||||
public static final int VK_LEFT = 37;
|
||||
public static final int VK_UP = 38;
|
||||
public static final int VK_RIGHT = 39;
|
||||
public static final int VK_DOWN = 40;
|
||||
public static final int VK_DELETE = 127;
|
||||
public static final int VK_INSERT = 155;
|
||||
public static final int VK_META = 157;
|
||||
|
||||
private static final Set<Integer> NUMPAD_DUP_KEYS = new HashSet<>(Arrays.asList(
|
||||
VK_ENTER, VK_CLEAR, VK_PAGE_UP, VK_PAGE_DOWN, VK_END, VK_HOME, VK_DELETE, VK_INSERT
|
||||
));
|
||||
|
||||
private static final Set<Integer> LEFT_DUP_KEYS = new HashSet<>(Arrays.asList(
|
||||
VK_SHIFT, VK_CONTROL, VK_ALT, VK_META
|
||||
));
|
||||
|
||||
// Common keycode to standard name mapping
|
||||
private static final Map<Integer, String> KEYCODE_NAMES = new HashMap<>();
|
||||
private static final Map<String, Integer> NAME_KEYCODES = new HashMap<>();
|
||||
|
||||
static {
|
||||
// Alphanumeric A-Z (65-90)
|
||||
for (char c = 'A'; c <= 'Z'; c++) {
|
||||
KEYCODE_NAMES.put((int) c, String.valueOf(c));
|
||||
NAME_KEYCODES.put(String.valueOf(c), (int) c);
|
||||
}
|
||||
// Digits 0-9 (48-57)
|
||||
for (char c = '0'; c <= '9'; c++) {
|
||||
KEYCODE_NAMES.put((int) c, String.valueOf(c));
|
||||
NAME_KEYCODES.put(String.valueOf(c), (int) c);
|
||||
}
|
||||
// Function keys F1-F24
|
||||
for (int i = 1; i <= 24; i++) {
|
||||
int vk = (i <= 12) ? (111 + i) : (111 + i); // F1 is 112
|
||||
KEYCODE_NAMES.put(111 + i, "F" + i);
|
||||
NAME_KEYCODES.put("F" + i, 111 + i);
|
||||
}
|
||||
// Specific keys
|
||||
registerKey(VK_ENTER, "ENTER");
|
||||
registerKey(13, "ENTER");
|
||||
registerKey(9, "TAB");
|
||||
registerKey(8, "BACK_SPACE");
|
||||
registerKey(27, "ESCAPE");
|
||||
registerKey(32, "SPACE");
|
||||
registerKey(VK_PAGE_UP, "PAGE_UP");
|
||||
registerKey(VK_PAGE_DOWN, "PAGE_DOWN");
|
||||
registerKey(VK_END, "END");
|
||||
registerKey(VK_HOME, "HOME");
|
||||
registerKey(VK_LEFT, "LEFT");
|
||||
registerKey(VK_UP, "UP");
|
||||
registerKey(VK_RIGHT, "RIGHT");
|
||||
registerKey(VK_DOWN, "DOWN");
|
||||
registerKey(VK_INSERT, "INSERT");
|
||||
registerKey(VK_DELETE, "DELETE");
|
||||
registerKey(VK_CLEAR, "CLEAR");
|
||||
registerKey(144, "NUM_LOCK");
|
||||
registerKey(145, "SCROLL_LOCK");
|
||||
registerKey(19, "PAUSE");
|
||||
registerKey(44, "PRINTSCREEN");
|
||||
|
||||
// Symbols
|
||||
registerKey(59, "SEMICOLON");
|
||||
registerKey(61, "EQUALS");
|
||||
registerKey(44, "COMMA");
|
||||
registerKey(45, "MINUS");
|
||||
registerKey(46, "PERIOD");
|
||||
registerKey(47, "SLASH");
|
||||
registerKey(192, "BACK_QUOTE");
|
||||
registerKey(91, "OPEN_BRACKET");
|
||||
registerKey(92, "BACK_SLASH");
|
||||
registerKey(93, "CLOSE_BRACKET");
|
||||
registerKey(222, "QUOTE");
|
||||
}
|
||||
|
||||
private static void registerKey(int code, String name) {
|
||||
KEYCODE_NAMES.put(code, name);
|
||||
NAME_KEYCODES.put(name.toUpperCase(), code);
|
||||
}
|
||||
|
||||
public static class KeyEntry {
|
||||
private final int keyCode;
|
||||
private final int modifiers;
|
||||
private final int keyLocation;
|
||||
private final String value;
|
||||
|
||||
public KeyEntry(int keyCode, int modifiers, int keyLocation, String value) {
|
||||
this.keyCode = keyCode;
|
||||
this.modifiers = modifiers;
|
||||
this.keyLocation = keyLocation;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getKeyCode() { return keyCode; }
|
||||
public int getModifiers() { return modifiers; }
|
||||
public int getKeyLocation() { return keyLocation; }
|
||||
public String getValue() { return value; }
|
||||
|
||||
public boolean isShift() { return (modifiers & MOD_SHIFT) != 0; }
|
||||
public boolean isCtrl() { return (modifiers & MOD_CTRL) != 0; }
|
||||
public boolean isMeta() { return (modifiers & MOD_META) != 0; }
|
||||
public boolean isAlt() { return (modifiers & MOD_ALT) != 0; }
|
||||
|
||||
public String toKeyStrokeString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (isCtrl()) sb.append("ctrl ");
|
||||
if (isAlt()) sb.append("alt ");
|
||||
if (isShift()) sb.append("shift ");
|
||||
if (isMeta()) sb.append("meta ");
|
||||
|
||||
String name = KEYCODE_NAMES.get(keyCode);
|
||||
if (name != null) {
|
||||
sb.append(name);
|
||||
} else {
|
||||
sb.append("KEY_").append(keyCode);
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
public String toHashKey() {
|
||||
return HodKeyMap.hashKey(keyCode, modifiers, keyLocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toHashKey() + "=" + value + " (" + toKeyStrokeString() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute HoD modifier prefix character.
|
||||
*/
|
||||
public static char getModifierPrefix(int modifiers) {
|
||||
int m = 0;
|
||||
if ((modifiers & MOD_SHIFT) != 0) m |= 1;
|
||||
if ((modifiers & MOD_CTRL) != 0) m |= 2;
|
||||
if ((modifiers & MOD_META) != 0) m |= 4;
|
||||
if ((modifiers & MOD_ALT) != 0) m |= 8;
|
||||
switch (m) {
|
||||
case 1: return 'S';
|
||||
case 2: return 'C';
|
||||
case 3: return 'D';
|
||||
case 4: return 'M';
|
||||
case 5: return 'E';
|
||||
case 6: return 'F';
|
||||
case 7: return 'G';
|
||||
case 8: return 'A';
|
||||
case 9: return 'H';
|
||||
case 10: return 'I';
|
||||
case 11: return 'J';
|
||||
case 12: return 'K';
|
||||
case 13: return 'L';
|
||||
case 14: return 'N';
|
||||
case 15: return 'O';
|
||||
default: return 'B';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode HoD modifier prefix character into modifier bitmask.
|
||||
*/
|
||||
public static int getModifiersFromPrefix(char c) {
|
||||
switch (Character.toUpperCase(c)) {
|
||||
case 'S': return MOD_SHIFT;
|
||||
case 'C': return MOD_CTRL;
|
||||
case 'D': return MOD_SHIFT | MOD_CTRL;
|
||||
case 'M': return MOD_META;
|
||||
case 'E': return MOD_SHIFT | MOD_META;
|
||||
case 'F': return MOD_CTRL | MOD_META;
|
||||
case 'G': return MOD_SHIFT | MOD_CTRL | MOD_META;
|
||||
case 'A': return MOD_ALT;
|
||||
case 'H': return MOD_SHIFT | MOD_ALT;
|
||||
case 'I': return MOD_CTRL | MOD_ALT;
|
||||
case 'J': return MOD_SHIFT | MOD_CTRL | MOD_ALT;
|
||||
case 'K': return MOD_META | MOD_ALT;
|
||||
case 'L': return MOD_SHIFT | MOD_META | MOD_ALT;
|
||||
case 'N': return MOD_CTRL | MOD_META | MOD_ALT;
|
||||
case 'O': return MOD_SHIFT | MOD_CTRL | MOD_META | MOD_ALT;
|
||||
default: return 0; // 'B' = base/none
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a HoD hashKey string for a given keycode, modifier mask, and key location.
|
||||
*/
|
||||
public static String hashKey(int keyCode, int modifiers, int keyLocation) {
|
||||
char prefix = getModifierPrefix(modifiers);
|
||||
String codeStr;
|
||||
|
||||
if (keyLocation == LOCATION_NUMPAD) {
|
||||
if (keyCode == VK_UP) {
|
||||
codeStr = String.valueOf(NUMPAD_UP_CODE);
|
||||
} else if (keyCode == VK_DOWN) {
|
||||
codeStr = String.valueOf(NUMPAD_DOWN_CODE);
|
||||
} else if (keyCode == VK_LEFT) {
|
||||
codeStr = String.valueOf(NUMPAD_LEFT_CODE);
|
||||
} else if (keyCode == VK_RIGHT) {
|
||||
codeStr = String.valueOf(NUMPAD_RIGHT_CODE);
|
||||
} else if (NUMPAD_DUP_KEYS.contains(keyCode)) {
|
||||
codeStr = String.valueOf(KEY_LOC_NUMPAD + keyCode);
|
||||
} else {
|
||||
codeStr = String.valueOf(keyCode);
|
||||
}
|
||||
} else if (keyLocation == LOCATION_LEFT && LEFT_DUP_KEYS.contains(keyCode)) {
|
||||
codeStr = String.valueOf(KEY_LOC_LEFT + keyCode);
|
||||
} else {
|
||||
codeStr = String.valueOf(keyCode);
|
||||
}
|
||||
|
||||
return prefix + codeStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a HoD hashKey and optional value.
|
||||
*/
|
||||
public static KeyEntry parseHashKey(String hashKey, String value) {
|
||||
if (hashKey == null || hashKey.length() < 2) {
|
||||
return null;
|
||||
}
|
||||
char prefix = hashKey.charAt(0);
|
||||
int mods = getModifiersFromPrefix(prefix);
|
||||
int rawCode;
|
||||
try {
|
||||
rawCode = Integer.parseInt(hashKey.substring(1));
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int keyCode = rawCode;
|
||||
int location = LOCATION_STANDARD;
|
||||
|
||||
if (rawCode == NUMPAD_UP_CODE) {
|
||||
keyCode = VK_UP;
|
||||
location = LOCATION_NUMPAD;
|
||||
} else if (rawCode == NUMPAD_DOWN_CODE) {
|
||||
keyCode = VK_DOWN;
|
||||
location = LOCATION_NUMPAD;
|
||||
} else if (rawCode == NUMPAD_LEFT_CODE) {
|
||||
keyCode = VK_LEFT;
|
||||
location = LOCATION_NUMPAD;
|
||||
} else if (rawCode == NUMPAD_RIGHT_CODE) {
|
||||
keyCode = VK_RIGHT;
|
||||
location = LOCATION_NUMPAD;
|
||||
} else if (rawCode >= KEY_LOC_NUMPAD) {
|
||||
keyCode = rawCode - KEY_LOC_NUMPAD;
|
||||
location = LOCATION_NUMPAD;
|
||||
} else if (rawCode >= KEY_LOC_LEFT) {
|
||||
keyCode = rawCode - KEY_LOC_LEFT;
|
||||
location = LOCATION_LEFT;
|
||||
}
|
||||
|
||||
return new KeyEntry(keyCode, mods, location, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a human-readable keystroke expression (e.g. "alt C", "ctrl shift ENTER")
|
||||
* into a KeyEntry.
|
||||
*/
|
||||
public static KeyEntry keyStrokeToEntry(String stroke, String value) {
|
||||
if (stroke == null || stroke.trim().isEmpty()) return null;
|
||||
String s = stroke.replace("+", " ").trim();
|
||||
String[] tokens = s.split("\\s+");
|
||||
|
||||
int mods = 0;
|
||||
int location = LOCATION_STANDARD;
|
||||
Integer keyCode = null;
|
||||
|
||||
for (String t : tokens) {
|
||||
String lower = t.toLowerCase();
|
||||
if ("ctrl".equals(lower) || "control".equals(lower)) {
|
||||
mods |= MOD_CTRL;
|
||||
} else if ("shift".equals(lower)) {
|
||||
mods |= MOD_SHIFT;
|
||||
} else if ("alt".equals(lower)) {
|
||||
mods |= MOD_ALT;
|
||||
} else if ("meta".equals(lower) || "cmd".equals(lower)) {
|
||||
mods |= MOD_META;
|
||||
} else if ("numpad".equals(lower)) {
|
||||
location = LOCATION_NUMPAD;
|
||||
} else {
|
||||
String upper = t.toUpperCase();
|
||||
if (NAME_KEYCODES.containsKey(upper)) {
|
||||
keyCode = NAME_KEYCODES.get(upper);
|
||||
} else if (upper.startsWith("NUMPAD") && upper.length() > 6) {
|
||||
location = LOCATION_NUMPAD;
|
||||
String rest = upper.substring(6);
|
||||
if (NAME_KEYCODES.containsKey(rest)) {
|
||||
keyCode = NAME_KEYCODES.get(rest);
|
||||
}
|
||||
} else if (upper.startsWith("KEY_")) {
|
||||
try {
|
||||
keyCode = Integer.parseInt(upper.substring(4));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (keyCode == null) return null;
|
||||
return new KeyEntry(keyCode, mods, location, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an IBM HoD .kmp profile.
|
||||
* Returns a map of j3270 action names to normalized key strokes.
|
||||
*/
|
||||
public static Map<String, String> parseHodKmp(Reader reader) throws IOException {
|
||||
BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);
|
||||
Map<String, List<String>> actionToBindings = new LinkedHashMap<>();
|
||||
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
line = line.trim();
|
||||
// Comments
|
||||
int cIdx = line.indexOf(';');
|
||||
if (cIdx >= 0) line = line.substring(0, cIdx).trim();
|
||||
cIdx = line.indexOf('#');
|
||||
if (cIdx >= 0) line = line.substring(0, cIdx).trim();
|
||||
|
||||
if (line.isEmpty() || line.startsWith("[")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int eq = line.indexOf('=');
|
||||
if (eq <= 0) continue;
|
||||
|
||||
String left = line.substring(0, eq).trim();
|
||||
String right = line.substring(eq + 1).trim();
|
||||
|
||||
// Ignore configuration/session properties
|
||||
if (left.equalsIgnoreCase("sessionType") ||
|
||||
left.equalsIgnoreCase("codePage") ||
|
||||
left.equalsIgnoreCase("autoApply") ||
|
||||
left.equalsIgnoreCase("keyVersion") ||
|
||||
left.equalsIgnoreCase("disableKeyBuffer") ||
|
||||
left.equalsIgnoreCase("id") ||
|
||||
left.equalsIgnoreCase("Description") ||
|
||||
left.equalsIgnoreCase("Version")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extended custom function mapping
|
||||
if (left.startsWith("XF:")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// HoD hashKey assignment: e.g. A67=[clear]
|
||||
if (left.matches("^[A-Za-z][0-9]+$")) {
|
||||
KeyEntry entry = parseHashKey(left, right);
|
||||
if (entry != null) {
|
||||
String stroke = entry.toKeyStrokeString();
|
||||
String action = resolveMnemonicToAction(right);
|
||||
if (action != null && stroke != null && !stroke.isEmpty()) {
|
||||
actionToBindings.computeIfAbsent(action, k -> new ArrayList<>()).add(stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : actionToBindings.entrySet()) {
|
||||
result.put(entry.getKey(), String.join(", ", entry.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports key bindings into standard IBM HoD .kmp file format.
|
||||
*/
|
||||
public static void exportHodKmp(Writer writer, Map<String, String> keyBindings) throws IOException {
|
||||
writer.write("[KeyRemap]\n");
|
||||
writer.write("keyVersion=2\n");
|
||||
writer.write("sessionType=1\n");
|
||||
writer.write("codePage=037\n");
|
||||
writer.write("autoApply=false\n");
|
||||
writer.write("disableKeyBuffer=false\n");
|
||||
|
||||
for (Map.Entry<String, String> entry : keyBindings.entrySet()) {
|
||||
String action = entry.getKey();
|
||||
String strokeList = entry.getValue();
|
||||
if (strokeList == null || strokeList.trim().isEmpty() || "UNBOUND".equalsIgnoreCase(strokeList.trim())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String mnemonic = actionToMnemonic(action);
|
||||
String[] strokes = strokeList.split(",");
|
||||
for (String stroke : strokes) {
|
||||
KeyEntry ke = keyStrokeToEntry(stroke.trim(), mnemonic);
|
||||
if (ke != null) {
|
||||
writer.write(ke.toHashKey() + "=" + mnemonic + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map HoD mnemonic string (with or without brackets) to standard j3270 action name.
|
||||
*/
|
||||
public static String resolveMnemonicToAction(String token) {
|
||||
if (token == null) return null;
|
||||
String clean = token.trim();
|
||||
if (clean.startsWith("[") && clean.endsWith("]")) {
|
||||
clean = clean.substring(1, clean.length() - 1);
|
||||
}
|
||||
clean = clean.toLowerCase();
|
||||
|
||||
switch (clean) {
|
||||
case "enter":
|
||||
case "enterreset":
|
||||
return "ENTER";
|
||||
case "newline":
|
||||
return "NEWLINE";
|
||||
case "tab":
|
||||
return "TAB";
|
||||
case "backtab":
|
||||
return "shift TAB";
|
||||
case "reset":
|
||||
return "ESCAPE";
|
||||
case "clear":
|
||||
return "CLEAR";
|
||||
case "eraseeof":
|
||||
case "eof":
|
||||
return "END";
|
||||
case "erasefld":
|
||||
case "erinp":
|
||||
case "eraseinput":
|
||||
return "ERASE_INPUT";
|
||||
case "dup":
|
||||
return "DUP";
|
||||
case "fieldmark":
|
||||
case "fldmark":
|
||||
return "FIELD_MARK";
|
||||
case "fldext":
|
||||
case "field-exit":
|
||||
return "NEWLINE";
|
||||
case "attn":
|
||||
return "ATTN";
|
||||
case "sysreq":
|
||||
return "SYSREQ";
|
||||
case "cursel":
|
||||
return "CURSEL";
|
||||
case "copy":
|
||||
return "COPY";
|
||||
case "paste":
|
||||
return "PASTE";
|
||||
case "selectall":
|
||||
return "SELECTALL";
|
||||
case "insert":
|
||||
return "INSERT";
|
||||
case "delete":
|
||||
return "DELETE";
|
||||
case "backspace":
|
||||
return "BACK_SPACE";
|
||||
case "home":
|
||||
return "HOME";
|
||||
case "end":
|
||||
return "END";
|
||||
case "up":
|
||||
return "UP";
|
||||
case "down":
|
||||
return "DOWN";
|
||||
case "left":
|
||||
return "LEFT";
|
||||
case "right":
|
||||
return "RIGHT";
|
||||
case "pageup":
|
||||
return "PAGE_UP";
|
||||
case "pagedn":
|
||||
case "pagedown":
|
||||
return "PAGE_DOWN";
|
||||
case "docmode":
|
||||
return "DOCMODE";
|
||||
case "wordwrap":
|
||||
return "WORDWRAP";
|
||||
case "apl":
|
||||
return "APL";
|
||||
case "rule":
|
||||
return "CROSSHAIR_RULER";
|
||||
case "statusbar":
|
||||
return "STATUS_BAR";
|
||||
default:
|
||||
if (clean.matches("pf[0-9]+")) {
|
||||
return clean.toUpperCase();
|
||||
}
|
||||
if (clean.matches("pa[1-3]")) {
|
||||
return clean.toUpperCase();
|
||||
}
|
||||
if (clean.matches("spf[0-9]+")) {
|
||||
int n = Integer.parseInt(clean.substring(3));
|
||||
return "PF" + (n + 12);
|
||||
}
|
||||
return clean.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map j3270 action name to HoD mnemonic token with brackets.
|
||||
*/
|
||||
public static String actionToMnemonic(String action) {
|
||||
if (action == null) return "[]";
|
||||
String u = action.toUpperCase().trim();
|
||||
switch (u) {
|
||||
case "ENTER": return "[enter]";
|
||||
case "NEWLINE": return "[newline]";
|
||||
case "TAB": return "[tab]";
|
||||
case "BACKTAB":
|
||||
case "SHIFT TAB": return "[backtab]";
|
||||
case "ESCAPE":
|
||||
case "RESET": return "[reset]";
|
||||
case "CLEAR": return "[clear]";
|
||||
case "END":
|
||||
case "ERASEEOF": return "[eraseeof]";
|
||||
case "ERASE_INPUT": return "[erasefld]";
|
||||
case "DUP": return "[dup]";
|
||||
case "FIELD_MARK": return "[fieldmark]";
|
||||
case "ATTN": return "[attn]";
|
||||
case "SYSREQ": return "[sysreq]";
|
||||
case "CURSEL": return "[cursel]";
|
||||
case "COPY": return "[copy]";
|
||||
case "PASTE": return "[paste]";
|
||||
case "SELECTALL": return "[selectall]";
|
||||
case "INSERT": return "[insert]";
|
||||
case "DELETE": return "[delete]";
|
||||
case "BACK_SPACE": return "[backspace]";
|
||||
case "HOME": return "[home]";
|
||||
case "UP": return "[up]";
|
||||
case "DOWN": return "[down]";
|
||||
case "LEFT": return "[left]";
|
||||
case "RIGHT": return "[right]";
|
||||
case "PAGE_UP": return "[pageup]";
|
||||
case "PAGE_DOWN": return "[pagedn]";
|
||||
case "DOCMODE": return "[docmode]";
|
||||
case "WORDWRAP": return "[wordwrap]";
|
||||
case "APL": return "[apl]";
|
||||
case "CROSSHAIR_RULER": return "[rule]";
|
||||
case "STATUS_BAR": return "[statusbar]";
|
||||
default:
|
||||
if (u.matches("PF[0-9]+")) return "[" + u.toLowerCase() + "]";
|
||||
if (u.matches("PA[1-3]")) return "[" + u.toLowerCase() + "]";
|
||||
return "[" + u.toLowerCase() + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,14 @@ public class InputProcessor {
|
||||
if (fsm == null || fsm.getConnectionState() == null || !fsm.getConnectionState().isConnected()) {
|
||||
return OiaStatus.NOT_CONNECTED;
|
||||
}
|
||||
if (oia != null) {
|
||||
if (oia.isXProt()) {
|
||||
return OiaStatus.X_PROT;
|
||||
}
|
||||
if (oia.isXSystem()) {
|
||||
return OiaStatus.X_SYSTEM;
|
||||
}
|
||||
}
|
||||
if (keyboardLocked) {
|
||||
return OiaStatus.X_SYSTEM;
|
||||
}
|
||||
@@ -112,6 +120,11 @@ public class InputProcessor {
|
||||
public void setKeyboardLocked(boolean locked) {
|
||||
if (this.keyboardLocked != locked) {
|
||||
this.keyboardLocked = locked;
|
||||
if (!locked && oia != null) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_NOT_INHIBITED);
|
||||
} else if (locked && oia != null && oia.getInputInhibited() == ECLConstants.INHIBIT_NOT_INHIBITED) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
|
||||
}
|
||||
if (lockStateListener != null) {
|
||||
lockStateListener.onLockStateChanged(locked);
|
||||
}
|
||||
@@ -334,11 +347,17 @@ public class InputProcessor {
|
||||
}
|
||||
|
||||
// Set MDT on field attribute
|
||||
boolean isLastFieldChar = false;
|
||||
ExtendedAttribute currentFaEa = null;
|
||||
if (screen.isFormatted()) {
|
||||
int faAddr = screen.findFieldAttribute(baddr);
|
||||
if (faAddr >= 0) {
|
||||
ExtendedAttribute faEa = screen.getCell(faAddr);
|
||||
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
|
||||
currentFaEa = screen.getCell(faAddr);
|
||||
currentFaEa.fa = (byte) (currentFaEa.fa | FA_MODIFY);
|
||||
}
|
||||
int nextAddr = screen.incrementAddress(baddr);
|
||||
if (screen.getCell(nextAddr).isFieldAttribute()) {
|
||||
isLastFieldChar = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +410,11 @@ public class InputProcessor {
|
||||
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
|
||||
// Field Validation: Trigger field (0x04) — automatically send AID upon filling the last character position
|
||||
if (isLastFieldChar && currentFaEa != null && (currentFaEa.vl & ExtendedAttribute.VL_TRIGGER) != 0) {
|
||||
sendAid(AID_ENTER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -443,14 +467,15 @@ public class InputProcessor {
|
||||
* - '\n' or '\r\n' triggers a NewLine action advancing to the first unprotected field of the next line.
|
||||
* When pasteStopAtProtectedLine is true:
|
||||
* - Halts or truncates paste if cursor reaches a protected line/boundary or if the current field is full.
|
||||
*
|
||||
* @param text text to paste
|
||||
* @param enablePasteFromExcel whether to parse tabs as field advances and newlines as row advances
|
||||
* @param pasteStopAtProtectedLine whether to halt paste when encountering protected boundaries
|
||||
* @return number of characters pasted
|
||||
*/
|
||||
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) {
|
||||
return pasteText(text, enablePasteFromExcel, pasteStopAtProtectedLine, this.blockPaste);
|
||||
private boolean pasteWordBreak = true;
|
||||
|
||||
public boolean isPasteWordBreak() {
|
||||
return pasteWordBreak;
|
||||
}
|
||||
|
||||
public void setPasteWordBreak(boolean pasteWordBreak) {
|
||||
this.pasteWordBreak = pasteWordBreak;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -459,10 +484,27 @@ public class InputProcessor {
|
||||
* @param text text to paste
|
||||
* @param enablePasteFromExcel whether to parse tabs as field advances and newlines as row advances
|
||||
* @param pasteStopAtProtectedLine whether to halt paste when encountering protected boundaries
|
||||
* @param blockPaste whether newlines align each line with the initial starting cursor column
|
||||
* @return number of characters pasted
|
||||
*/
|
||||
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) {
|
||||
return pasteText(text, enablePasteFromExcel, pasteStopAtProtectedLine, this.blockPaste, this.pasteWordBreak);
|
||||
}
|
||||
|
||||
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine, boolean blockPaste) {
|
||||
return pasteText(text, enablePasteFromExcel, pasteStopAtProtectedLine, blockPaste, this.pasteWordBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes text onto the screen with tabular, boundary, block alignment, and word break controls.
|
||||
*
|
||||
* @param text text to paste
|
||||
* @param enablePasteFromExcel whether to parse tabs as field advances and newlines as row advances
|
||||
* @param pasteStopAtProtectedLine whether to halt paste when encountering protected boundaries
|
||||
* @param blockPaste whether newlines align each line with the initial starting cursor column
|
||||
* @param pasteWordBreak whether to prevent splitting words across field boundaries
|
||||
* @return number of characters pasted
|
||||
*/
|
||||
public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine, boolean blockPaste, boolean pasteWordBreak) {
|
||||
if (text == null || text.isEmpty() || screen == null || keyboardLocked) {
|
||||
return 0;
|
||||
}
|
||||
@@ -549,9 +591,10 @@ public class InputProcessor {
|
||||
if (ch >= 0x20 && ch != 0x7F) {
|
||||
if (screen.isFormatted()) {
|
||||
int baddr = screen.getCursorAddress();
|
||||
int totalScreenSize = screen.getRows() * screen.getCols();
|
||||
ExtendedAttribute ea = screen.getCell(baddr);
|
||||
if (ea.isFieldAttribute()) {
|
||||
baddr = (baddr + 1) % (screen.getRows() * screen.getCols());
|
||||
baddr = (baddr + 1) % totalScreenSize;
|
||||
}
|
||||
byte faVal = screen.getFieldAttributeAt(baddr);
|
||||
if (faIsProtected(faVal & 0xFF)) {
|
||||
@@ -565,6 +608,56 @@ public class InputProcessor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pasteWordBreak) {
|
||||
int remaining = screen.getFieldRemainingLength(baddr);
|
||||
int faAddr = screen.findFieldAttribute(baddr);
|
||||
int startOfField = (faAddr >= 0) ? (faAddr + 1) % totalScreenSize : 0;
|
||||
|
||||
if (ch == ' ') {
|
||||
// If separating space occurs when current field is full, advance to next field
|
||||
if (remaining <= 0) {
|
||||
tab();
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
// If cursor just transitioned to start of next field after completing a word,
|
||||
// don't insert a leading delimiter space into the new field
|
||||
if (baddr == startOfField && i > 0 && !Character.isWhitespace(text.charAt(i - 1))) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Check if at the start of a word
|
||||
boolean isWordStart = (i == 0 || Character.isWhitespace(text.charAt(i - 1)));
|
||||
if (isWordStart) {
|
||||
int wordEnd = i;
|
||||
while (wordEnd < len) {
|
||||
char wc = text.charAt(wordEnd);
|
||||
if (Character.isWhitespace(wc) || wc == '\t' || wc == '\r' || wc == '\n') {
|
||||
break;
|
||||
}
|
||||
wordEnd++;
|
||||
}
|
||||
int wordLen = wordEnd - i;
|
||||
// If the word cannot fit in remaining space and we're not at start of field,
|
||||
// advance to next field to prevent splitting the word
|
||||
if (wordLen > remaining && baddr != startOfField) {
|
||||
if (pasteStopAtProtectedLine && isLineProtected(screen.getCursorRow())) {
|
||||
break;
|
||||
}
|
||||
tab();
|
||||
baddr = screen.getCursorAddress();
|
||||
if (screen.getCell(baddr).isFieldAttribute()) {
|
||||
baddr = (baddr + 1) % totalScreenSize;
|
||||
}
|
||||
if (faIsProtected(screen.getFieldAttributeAt(baddr) & 0xFF)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typeCharacter(ch);
|
||||
@@ -846,6 +939,11 @@ public class InputProcessor {
|
||||
if (graphicsPlane != null) {
|
||||
graphicsPlane.clear();
|
||||
}
|
||||
lastAid = aidCode;
|
||||
setKeyboardLocked(true);
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
|
||||
}
|
||||
byte[] data = new byte[] { (byte) aidCode };
|
||||
sendAidResponse(data);
|
||||
return;
|
||||
@@ -856,12 +954,22 @@ public class InputProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aidCode != AID_CLEAR && aidCode != AID_PA1 && aidCode != AID_PA2 && aidCode != AID_PA3 && aidCode != AID_SYSREQ && aidCode != AID_SELECT) {
|
||||
if (!validateInput()) {
|
||||
log.fine("Input validation failed for AID 0x" + Integer.toHexString(aidCode));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (insertOffOnAid && insertMode) {
|
||||
setInsertMode(false);
|
||||
}
|
||||
|
||||
lastAid = aidCode;
|
||||
setKeyboardLocked(true);
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
|
||||
}
|
||||
|
||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
|
||||
// SSCP-LU Mode: send raw EBCDIC line data without AID or 3270 cursor address
|
||||
@@ -948,6 +1056,15 @@ public class InputProcessor {
|
||||
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G and DS3270.
|
||||
*/
|
||||
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
|
||||
boolean hasGraphics = gocaDecoder != null && (gocaDecoder.isGraphicsCursorActive() ||
|
||||
(gocaDecoder.getGraphicsPlane() != null && gocaDecoder.getGraphicsPlane().hasContent()));
|
||||
if (hasGraphics) {
|
||||
int gx = gocaDecoder.getGraphicCursorX();
|
||||
int gy = gocaDecoder.getGraphicCursorY();
|
||||
sendGraphicMouseAid(gx, gy, aidCode, button, isShift, isCtrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
|
||||
return;
|
||||
}
|
||||
@@ -955,47 +1072,198 @@ public class InputProcessor {
|
||||
return;
|
||||
}
|
||||
setKeyboardLocked(true);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
|
||||
int gx = gocaDecoder.getGraphicCursorX();
|
||||
int gy = gocaDecoder.getGraphicCursorY();
|
||||
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
|
||||
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 24;
|
||||
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
|
||||
int row = cursorAddr / cols;
|
||||
int col = cursorAddr % cols;
|
||||
|
||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
gx, gy, row, col, button, true, isShift, isCtrl
|
||||
);
|
||||
|
||||
StringBuilder sfHex = new StringBuilder();
|
||||
for (byte b : sf) {
|
||||
sfHex.append(String.format("%02X ", b & 0xFF));
|
||||
}
|
||||
log.info(String.format(
|
||||
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d cursorAddr=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
|
||||
gx, gy, row, col, cursorAddr, button, isShift, isCtrl, sfHex.toString().trim()
|
||||
));
|
||||
|
||||
// Structured Field AID (0x88) + 56-byte Graphic Input SF
|
||||
out.write(AID_SF);
|
||||
try {
|
||||
out.write(sf);
|
||||
byte[] rma = buildReadModifiedInboundData(aidCode, false);
|
||||
out.write(rma);
|
||||
} catch (java.io.IOException ignored) {}
|
||||
} else {
|
||||
byte[] rma = buildReadModifiedInboundData(aidCode, false);
|
||||
try {
|
||||
out.write(rma);
|
||||
} catch (java.io.IOException ignored) {}
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] rma = buildReadModifiedInboundData(aidCode, false);
|
||||
try {
|
||||
out.write(rma);
|
||||
} catch (java.io.IOException ignored) {}
|
||||
sendAidResponse(out.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits a mouse / light-pen graphic input structured field AID with explicit GOCA coordinates.
|
||||
*/
|
||||
public void sendGraphicMouseAid(int gx, int gy, int aidCode, int button, boolean isShift, boolean isCtrl) {
|
||||
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
|
||||
return;
|
||||
}
|
||||
if (isKeyboardLocked()) {
|
||||
return;
|
||||
}
|
||||
setKeyboardLocked(true);
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_SYSTEM_LOCK);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
|
||||
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
|
||||
int row = cursorAddr / cols;
|
||||
int col = cursorAddr % cols;
|
||||
|
||||
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
gx, gy, row, col, button, true, isShift, isCtrl
|
||||
);
|
||||
|
||||
StringBuilder sfHex = new StringBuilder();
|
||||
for (byte b : sf) {
|
||||
sfHex.append(String.format("%02X ", b & 0xFF));
|
||||
}
|
||||
log.info(String.format(
|
||||
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d cursorAddr=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
|
||||
gx, gy, row, col, cursorAddr, button, isShift, isCtrl, sfHex.toString().trim()
|
||||
));
|
||||
|
||||
// Structured Field AID (0x88) + 56-byte Graphic Input SF
|
||||
out.write(AID_SF);
|
||||
try {
|
||||
out.write(sf);
|
||||
byte[] rma = buildReadModifiedInboundData(aidCode, false);
|
||||
out.write(rma);
|
||||
} catch (java.io.IOException ignored) {}
|
||||
|
||||
sendAidResponse(out.toByteArray());
|
||||
}
|
||||
|
||||
// ========== 3270 EDS Field Validation (0xC1 / TY_FVALIDATE) ==========
|
||||
|
||||
/**
|
||||
* Enforce 3270 Extended Data Stream field validation attributes (0xC1 / TY_FVALIDATE)
|
||||
* prior to transmitting AID keys (Mandatory Fill: 0x01, Mandatory Entry: 0x02).
|
||||
*
|
||||
* @return true if all fields pass validation, false if validation failed and input inhibited
|
||||
*/
|
||||
public boolean validateInput() {
|
||||
if (screen == null || !screen.isFormatted()) {
|
||||
return true;
|
||||
}
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return true;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
ExtendedAttribute faCell = screen.getCell(i);
|
||||
if (!faCell.isFieldAttribute()) continue;
|
||||
|
||||
int vl = faCell.vl & 0xFF;
|
||||
if (vl == 0) continue;
|
||||
|
||||
// Only validate unprotected input fields
|
||||
if (faIsProtected(faCell.fa & 0xFF)) continue;
|
||||
|
||||
int fieldStart = (i + 1) % size;
|
||||
int totalChars = 0;
|
||||
int filledChars = 0;
|
||||
int firstEmptyPos = -1;
|
||||
|
||||
int p = fieldStart;
|
||||
while (p != i && !screen.getCell(p).isFieldAttribute()) {
|
||||
ExtendedAttribute cell = screen.getCell(p);
|
||||
totalChars++;
|
||||
int ec = cell.ec & 0xFF;
|
||||
char ucs = cell.ucs4;
|
||||
boolean isFilled = (ec != 0x00 && ec != 0x40 && ucs != 0 && ucs != ' ' && ucs != '\u3000');
|
||||
if (isFilled) {
|
||||
filledChars++;
|
||||
} else if (firstEmptyPos < 0) {
|
||||
firstEmptyPos = p;
|
||||
}
|
||||
p = (p + 1) % size;
|
||||
}
|
||||
|
||||
if (totalChars == 0) continue;
|
||||
|
||||
boolean isModified = faIsModified(faCell.fa & 0xFF);
|
||||
boolean mandatoryFill = (vl & ExtendedAttribute.VL_MANDATORY_FILL) != 0;
|
||||
boolean mandatoryEntry = (vl & ExtendedAttribute.VL_MANDATORY_ENTRY) != 0;
|
||||
|
||||
// Mandatory Entry (0x02): Field must contain at least one user-entered character
|
||||
if (mandatoryEntry && filledChars == 0) {
|
||||
failValidation(fieldStart);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mandatory Fill (0x01): Field must be completely filled with characters (no nulls/blanks).
|
||||
// If the field is modified, has characters entered, or has Mandatory Entry also set:
|
||||
if (mandatoryFill) {
|
||||
if (mandatoryEntry || isModified || filledChars > 0) {
|
||||
if (filledChars < totalChars) {
|
||||
failValidation(firstEmptyPos >= 0 ? firstEmptyPos : fieldStart);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the field at the current cursor position when the user attempts to tab away.
|
||||
* If the field has Mandatory Fill (0x01) and is partially filled, inhibits keyboard with input error.
|
||||
*/
|
||||
public boolean validateCurrentFieldOnExit() {
|
||||
if (screen == null || !screen.isFormatted()) {
|
||||
return true;
|
||||
}
|
||||
int curAddr = screen.getCursorAddress();
|
||||
int faPos = screen.findFieldAttribute(curAddr);
|
||||
if (faPos < 0) return true;
|
||||
|
||||
ExtendedAttribute faCell = screen.getCell(faPos);
|
||||
int vl = faCell.vl & 0xFF;
|
||||
if ((vl & ExtendedAttribute.VL_MANDATORY_FILL) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (faIsProtected(faCell.fa & 0xFF)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
int fieldStart = (faPos + 1) % size;
|
||||
int totalChars = 0;
|
||||
int filledChars = 0;
|
||||
int firstEmptyPos = -1;
|
||||
|
||||
int p = fieldStart;
|
||||
while (p != faPos && !screen.getCell(p).isFieldAttribute()) {
|
||||
ExtendedAttribute cell = screen.getCell(p);
|
||||
totalChars++;
|
||||
int ec = cell.ec & 0xFF;
|
||||
char ucs = cell.ucs4;
|
||||
boolean isFilled = (ec != 0x00 && ec != 0x40 && ucs != 0 && ucs != ' ' && ucs != '\u3000');
|
||||
if (isFilled) {
|
||||
filledChars++;
|
||||
} else if (firstEmptyPos < 0) {
|
||||
firstEmptyPos = p;
|
||||
}
|
||||
p = (p + 1) % size;
|
||||
}
|
||||
|
||||
// If partially filled (incomplete):
|
||||
if (filledChars > 0 && filledChars < totalChars) {
|
||||
failValidation(firstEmptyPos >= 0 ? firstEmptyPos : curAddr);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void failValidation(int errorCursorPos) {
|
||||
if (errorCursorPos >= 0 && screen != null) {
|
||||
screen.setCursorAddress(errorCursorPos);
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
if (oia != null) {
|
||||
oia.setInputInhibited(ECLConstants.INHIBIT_OPERATOR_DUE);
|
||||
}
|
||||
setKeyboardLocked(true);
|
||||
if (bellListener != null) {
|
||||
bellListener.onBell();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Cursor movement ==========
|
||||
|
||||
/**
|
||||
@@ -1030,7 +1298,12 @@ public class InputProcessor {
|
||||
screen.setCursorAddress(address);
|
||||
|
||||
// Immediate Selection (Space or Null) -> AID_SELECT (0x7E)
|
||||
// Per IBM 3270 Reference (GA23-0059), light-pen selection has no effect
|
||||
// on protected fields with a space or null designator character.
|
||||
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || ascii == '\u3000') {
|
||||
if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsProtected(fa)) {
|
||||
return false;
|
||||
}
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
|
||||
return true;
|
||||
@@ -1118,6 +1391,9 @@ public class InputProcessor {
|
||||
screen.processWordTab(true);
|
||||
return;
|
||||
}
|
||||
if (!validateCurrentFieldOnExit()) {
|
||||
return;
|
||||
}
|
||||
int addr = screen.findNextUnprotected(screen.getCursorAddress());
|
||||
screen.setCursorAddress(addr);
|
||||
screen.updateDisplaySnapshot();
|
||||
@@ -1137,6 +1413,9 @@ public class InputProcessor {
|
||||
return;
|
||||
}
|
||||
if (!screen.isFormatted()) return;
|
||||
if (!validateCurrentFieldOnExit()) {
|
||||
return;
|
||||
}
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
if (size <= 0) return;
|
||||
int addr = screen.getCursorAddress();
|
||||
@@ -1318,14 +1597,13 @@ public class InputProcessor {
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
/** Attention key (sends Telnet Interrupt Process + IAC EOR break signal). */
|
||||
/** Attention key (sends Telnet Interrupt Process or PA1/BREAK signal). */
|
||||
public void processAttn() {
|
||||
if (insertOffOnAid && insertMode) {
|
||||
setInsertMode(false);
|
||||
}
|
||||
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isConnected()) {
|
||||
byte[] ipEor = new byte[] {
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.IP,
|
||||
(byte) TelnetConstants.IAC, (byte) TelnetConstants.EOR
|
||||
};
|
||||
fsm.sendRecord(ipEor);
|
||||
fsm.handleAttn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1550,11 +1828,110 @@ public class InputProcessor {
|
||||
public void processBackspace() { backspace(); }
|
||||
public void processEraseEOF() { eraseEof(); }
|
||||
public void processEraseInput() { eraseInput(); }
|
||||
public void processEraseField() { eraseField(); }
|
||||
public void processDup() { dup(); }
|
||||
public void processFieldMark() { fieldMark(); }
|
||||
public void processToggleInsert() { setInsertMode(!isInsertMode()); }
|
||||
public boolean processCurSel() { return cursorSelect(); }
|
||||
public boolean processCursorSelect() { return cursorSelect(); }
|
||||
public boolean processSelFld() { return selectField(); }
|
||||
public boolean processDeSelFld() { return deselectField(); }
|
||||
|
||||
public void processFieldStart() {
|
||||
if (screen == null) return;
|
||||
int cur = screen.getCursorAddress();
|
||||
if (screen.isFormatted()) {
|
||||
int faPos = screen.findFieldAttribute(cur);
|
||||
if (faPos >= 0) {
|
||||
int fieldStart = screen.incrementAddress(faPos);
|
||||
screen.setCursorAddress(fieldStart);
|
||||
}
|
||||
} else {
|
||||
int cols = screen.getCols();
|
||||
int row = cur / cols;
|
||||
screen.setCursorAddress(row * cols);
|
||||
}
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public void eraseField() {
|
||||
if (screen == null) return;
|
||||
if (!screen.isFormatted()) {
|
||||
eraseEof();
|
||||
return;
|
||||
}
|
||||
int cur = screen.getCursorAddress();
|
||||
int faPos = screen.findFieldAttribute(cur);
|
||||
if (faPos < 0) return;
|
||||
byte faVal = screen.getCell(faPos).fa;
|
||||
if (faIsProtected(faVal & 0xFF)) return;
|
||||
|
||||
int fieldStart = screen.incrementAddress(faPos);
|
||||
int pos = fieldStart;
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
int count = 0;
|
||||
while (!screen.getCell(pos).isFieldAttribute() && count < size) {
|
||||
ExtendedAttribute ea = screen.getCell(pos);
|
||||
ea.ec = 0;
|
||||
ea.ucs4 = 0;
|
||||
pos = screen.incrementAddress(pos);
|
||||
count++;
|
||||
}
|
||||
screen.getCell(faPos).fa = (byte) (screen.getCell(faPos).fa | FA_MODIFY);
|
||||
screen.setCursorAddress(fieldStart);
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
public boolean selectField() {
|
||||
if (screen == null || !screen.isFormatted()) return false;
|
||||
int cur = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
int faPos = screen.findFieldAttribute(cur);
|
||||
if (faPos < 0) return false;
|
||||
ExtendedAttribute faCell = screen.getCell(faPos);
|
||||
int fa = faCell.fa & 0xFF;
|
||||
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) return false;
|
||||
|
||||
int desPos = (faPos + 1) % size;
|
||||
ExtendedAttribute desCell = screen.getCell(desPos);
|
||||
int ebcdic = desCell.ec & 0xFF;
|
||||
char ascii = (char) desCell.ucs4;
|
||||
if (ebcdic == 0x6F || ascii == '?' || ascii == '\uFF1F') {
|
||||
desCell.ec = (byte) 0x6E;
|
||||
desCell.ucs4 = '>';
|
||||
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean deselectField() {
|
||||
if (screen == null || !screen.isFormatted()) return false;
|
||||
int cur = screen.getCursorAddress();
|
||||
int size = screen.getRows() * screen.getCols();
|
||||
int faPos = screen.findFieldAttribute(cur);
|
||||
if (faPos < 0) return false;
|
||||
ExtendedAttribute faCell = screen.getCell(faPos);
|
||||
int fa = faCell.fa & 0xFF;
|
||||
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) return false;
|
||||
|
||||
int desPos = (faPos + 1) % size;
|
||||
ExtendedAttribute desCell = screen.getCell(desPos);
|
||||
int ebcdic = desCell.ec & 0xFF;
|
||||
char ascii = (char) desCell.ucs4;
|
||||
if (ebcdic == 0x6E || ascii == '>' || ascii == '\uFF1E') {
|
||||
desCell.ec = (byte) 0x6F;
|
||||
desCell.ucs4 = '?';
|
||||
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ========== File Transfer Support ==========
|
||||
|
||||
@@ -1667,34 +2044,46 @@ public class InputProcessor {
|
||||
|
||||
/**
|
||||
* Parse and execute IBM ECL standard mnemonic keystrokes (e.g. "USER[tab]PASS[enter]").
|
||||
* Fully compatible with IBM Host On-Demand SendKeyTokenizer and 120+ standard mnemonics.
|
||||
*/
|
||||
public void sendKeys(String keys) {
|
||||
if (keys == null || keys.isEmpty()) return;
|
||||
|
||||
int len = keys.length();
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
char c = keys.charAt(i);
|
||||
if (c == '[') {
|
||||
int end = keys.indexOf(']', i);
|
||||
if (end > i) {
|
||||
String token = keys.substring(i + 1, end).trim().toLowerCase();
|
||||
try {
|
||||
haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer tokenizer =
|
||||
new haus.nightmare.lib3270j.ecl.util.SendKeyTokenizer(keys);
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
String token = tokenizer.nextToken();
|
||||
if (token == null) break;
|
||||
|
||||
if (token.length() == 1) {
|
||||
typeCharacter(token.charAt(0));
|
||||
} else if (token.startsWith("[") && token.endsWith("]")) {
|
||||
executeMnemonicToken(token);
|
||||
i = end + 1;
|
||||
continue;
|
||||
} else {
|
||||
for (int i = 0; i < token.length(); i++) {
|
||||
typeCharacter(token.charAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
typeCharacter(c);
|
||||
i++;
|
||||
} catch (haus.nightmare.lib3270j.ecl.ECLErr e) {
|
||||
log.warning("Syntax error in sendKeys: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void executeMnemonicToken(String token) {
|
||||
public void executeMnemonicToken(String rawToken) {
|
||||
if (rawToken == null || rawToken.isEmpty()) return;
|
||||
String token = rawToken.trim().toLowerCase();
|
||||
if (token.startsWith("[") && token.endsWith("]") && token.length() >= 2) {
|
||||
token = token.substring(1, token.length() - 1).trim();
|
||||
}
|
||||
|
||||
if (isNvtMode()) {
|
||||
try {
|
||||
switch (token) {
|
||||
case "enter":
|
||||
case "return":
|
||||
case "keypadenter":
|
||||
fsm.sendNVTString("\r\n");
|
||||
break;
|
||||
case "clear":
|
||||
@@ -1715,6 +2104,8 @@ public class InputProcessor {
|
||||
fsm.sendNVTString("\u001B[H");
|
||||
break;
|
||||
case "end":
|
||||
case "eof":
|
||||
case "fieldend":
|
||||
fsm.sendNVTString("\u001B[F");
|
||||
break;
|
||||
case "up":
|
||||
@@ -1737,6 +2128,7 @@ public class InputProcessor {
|
||||
case "pgup":
|
||||
fsm.sendNVTString("\u001B[5~");
|
||||
break;
|
||||
case "pagedn":
|
||||
case "pagedown":
|
||||
case "pgdn":
|
||||
fsm.sendNVTString("\u001B[6~");
|
||||
@@ -1751,6 +2143,7 @@ public class InputProcessor {
|
||||
break;
|
||||
case "attn":
|
||||
case "break":
|
||||
case "telnetbreak":
|
||||
fsm.sendNVTChar('\u0003');
|
||||
break;
|
||||
case "sysreq":
|
||||
@@ -1761,10 +2154,36 @@ public class InputProcessor {
|
||||
case "reset":
|
||||
setKeyboardLocked(false);
|
||||
break;
|
||||
case "enterreset":
|
||||
if (keyboardLocked) {
|
||||
setKeyboardLocked(false);
|
||||
} else {
|
||||
fsm.sendNVTString("\r\n");
|
||||
}
|
||||
break;
|
||||
case "keypad0": fsm.sendNVTChar('0'); break;
|
||||
case "keypad1": fsm.sendNVTChar('1'); break;
|
||||
case "keypad2": fsm.sendNVTChar('2'); break;
|
||||
case "keypad3": fsm.sendNVTChar('3'); break;
|
||||
case "keypad4": fsm.sendNVTChar('4'); break;
|
||||
case "keypad5": fsm.sendNVTChar('5'); break;
|
||||
case "keypad6": fsm.sendNVTChar('6'); break;
|
||||
case "keypad7": fsm.sendNVTChar('7'); break;
|
||||
case "keypad8": fsm.sendNVTChar('8'); break;
|
||||
case "keypad9": fsm.sendNVTChar('9'); break;
|
||||
case "keypad.": fsm.sendNVTChar('.'); break;
|
||||
case "keypad,": fsm.sendNVTChar(','); break;
|
||||
case "keypad-": fsm.sendNVTChar('-'); break;
|
||||
default:
|
||||
if (token.startsWith("pf") || token.startsWith("f")) {
|
||||
if (token.length() == 1 && Character.isDigit(token.charAt(0))) {
|
||||
fsm.sendNVTChar(token.charAt(0));
|
||||
} else if (token.startsWith("pf") || token.startsWith("f") || token.startsWith("vt-pf") || token.startsWith("spf")) {
|
||||
try {
|
||||
String numStr = token.startsWith("pf") ? token.substring(2) : token.substring(1);
|
||||
String numStr = token;
|
||||
if (numStr.startsWith("vt-pf")) numStr = numStr.substring(5);
|
||||
else if (numStr.startsWith("spf")) numStr = numStr.substring(3);
|
||||
else if (numStr.startsWith("pf")) numStr = numStr.substring(2);
|
||||
else if (numStr.startsWith("f")) numStr = numStr.substring(1);
|
||||
int fn = Integer.parseInt(numStr);
|
||||
sendNvtFunctionKey(fn);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
@@ -1815,11 +2234,36 @@ public class InputProcessor {
|
||||
case "curright":
|
||||
cursorRight();
|
||||
break;
|
||||
case "pagedn":
|
||||
case "pagedown":
|
||||
case "pgdn":
|
||||
sendAid(AID_PF8);
|
||||
break;
|
||||
case "pageup":
|
||||
case "pgup":
|
||||
sendAid(AID_PF7);
|
||||
break;
|
||||
case "eof":
|
||||
case "fieldend":
|
||||
case "end":
|
||||
processFieldEnd();
|
||||
break;
|
||||
case "bof":
|
||||
case "fieldstart":
|
||||
processFieldStart();
|
||||
break;
|
||||
case "eraseeof":
|
||||
case "erase_eof":
|
||||
eraseEof();
|
||||
break;
|
||||
case "erasefld":
|
||||
case "erasefield":
|
||||
case "erase_field":
|
||||
eraseField();
|
||||
break;
|
||||
case "erinp":
|
||||
case "eraseinp":
|
||||
case "eraseinput":
|
||||
case "erase_input":
|
||||
eraseInput();
|
||||
break;
|
||||
@@ -1830,7 +2274,24 @@ public class InputProcessor {
|
||||
case "fieldmark":
|
||||
fieldMark();
|
||||
break;
|
||||
case "fldext":
|
||||
case "fieldexit":
|
||||
case "field+":
|
||||
case "field-":
|
||||
tab();
|
||||
break;
|
||||
case "+cr":
|
||||
if (graphicsPlane != null) {
|
||||
if (graphicsPlane.isGraphicCursorAttached()) {
|
||||
graphicsPlane.detachGraphicCursor();
|
||||
} else {
|
||||
graphicsPlane.attachGraphicCursor(graphicsPlane.getGraphicCursorX(), graphicsPlane.getGraphicCursorY());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "attn":
|
||||
case "break":
|
||||
case "telnetbreak":
|
||||
attn();
|
||||
break;
|
||||
case "sysreq":
|
||||
@@ -1839,10 +2300,18 @@ public class InputProcessor {
|
||||
case "reset":
|
||||
reset();
|
||||
break;
|
||||
case "enterreset":
|
||||
if (keyboardLocked) {
|
||||
reset();
|
||||
} else {
|
||||
sendAid(AID_ENTER);
|
||||
}
|
||||
break;
|
||||
case "insert":
|
||||
setInsertMode(!isInsertMode());
|
||||
break;
|
||||
case "delete":
|
||||
case "del":
|
||||
deleteChar();
|
||||
break;
|
||||
case "backspace":
|
||||
@@ -1850,9 +2319,13 @@ public class InputProcessor {
|
||||
backspace();
|
||||
break;
|
||||
case "wordtab":
|
||||
case "tabword":
|
||||
case "nextword":
|
||||
processWordTab(true);
|
||||
break;
|
||||
case "wordbacktab":
|
||||
case "backtabword":
|
||||
case "previousword":
|
||||
processWordTab(false);
|
||||
break;
|
||||
case "wordleft":
|
||||
@@ -1861,10 +2334,6 @@ public class InputProcessor {
|
||||
case "wordright":
|
||||
processWordRight();
|
||||
break;
|
||||
case "fieldend":
|
||||
case "end":
|
||||
processFieldEnd();
|
||||
break;
|
||||
case "deleteword":
|
||||
processDeleteWord();
|
||||
break;
|
||||
@@ -1873,6 +2342,12 @@ public class InputProcessor {
|
||||
case "select":
|
||||
cursorSelect();
|
||||
break;
|
||||
case "selfld":
|
||||
selectField();
|
||||
break;
|
||||
case "deselfld":
|
||||
deselectField();
|
||||
break;
|
||||
case "lightpen":
|
||||
processLightPen();
|
||||
break;
|
||||
@@ -1892,8 +2367,37 @@ public class InputProcessor {
|
||||
case "aplmode":
|
||||
toggleAplKeyboardMode();
|
||||
break;
|
||||
case "altview":
|
||||
if (screen != null) {
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
break;
|
||||
case "dspsosi":
|
||||
if (screen != null) {
|
||||
screen.balanceSOSI();
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
break;
|
||||
case "keypad0": typeCharacter('0'); break;
|
||||
case "keypad1": typeCharacter('1'); break;
|
||||
case "keypad2": typeCharacter('2'); break;
|
||||
case "keypad3": typeCharacter('3'); break;
|
||||
case "keypad4": typeCharacter('4'); break;
|
||||
case "keypad5": typeCharacter('5'); break;
|
||||
case "keypad6": typeCharacter('6'); break;
|
||||
case "keypad7": typeCharacter('7'); break;
|
||||
case "keypad8": typeCharacter('8'); break;
|
||||
case "keypad9": typeCharacter('9'); break;
|
||||
case "keypad.": typeCharacter('.'); break;
|
||||
case "keypad,": typeCharacter(','); break;
|
||||
case "keypad-": typeCharacter('-'); break;
|
||||
case "keypadenter": sendAid(AID_ENTER); break;
|
||||
default:
|
||||
if (token.startsWith("pf")) {
|
||||
if (token.length() == 1 && Character.isDigit(token.charAt(0))) {
|
||||
typeCharacter(token.charAt(0));
|
||||
} else if (token.startsWith("pf")) {
|
||||
try {
|
||||
int pfNum = Integer.parseInt(token.substring(2));
|
||||
if (pfNum >= 1 && pfNum <= 24) {
|
||||
@@ -1907,6 +2411,24 @@ public class InputProcessor {
|
||||
sendAid(AID_PA1 + (paNum - 1));
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} else if (token.startsWith("spf")) {
|
||||
try {
|
||||
int spfNum = Integer.parseInt(token.substring(3));
|
||||
if (spfNum >= 1 && spfNum <= 24) {
|
||||
sendAid(AID_PF1 + (spfNum - 1));
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} else if (token.startsWith("vt-pf")) {
|
||||
try {
|
||||
int vpfNum = Integer.parseInt(token.substring(5));
|
||||
if (vpfNum >= 1 && vpfNum <= 24) {
|
||||
sendAid(AID_PF1 + (vpfNum - 1));
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} else {
|
||||
// Recognized HoD Bidi / Layer / Print / GUI event tokens:
|
||||
// log or no-op safely without throwing unknown errors
|
||||
log.fine("Mnemonic token received: " + token);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package haus.nightmare.lib3270j.macro;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import haus.nightmare.lib3270j.Telnet3270Client;
|
||||
import haus.nightmare.lib3270j.ecl.ECLSession;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroAction;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroActionPrompt;
|
||||
import haus.nightmare.lib3270j.macro.variable.MacroVariables;
|
||||
|
||||
/**
|
||||
* High-level Macro engine matching IBM Host On-Demand Macro.
|
||||
* Coordinates loading, variable scopes, screen state transitions, and background PlayThread execution.
|
||||
*/
|
||||
public class Macro implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final int STATE_STOPPED = 1;
|
||||
public static final int STATE_PLAYING = 2;
|
||||
public static final int STATE_PAUSED = 4;
|
||||
public static final int STATE_ERROR = 8;
|
||||
|
||||
public interface MacroListener {
|
||||
void macroStateChanged(Macro macro, int oldState, int newState);
|
||||
void macroScreenMatching(MacroScreen screen);
|
||||
void macroActionExecuting(MacroAction action);
|
||||
void macroMessage(String title, String message);
|
||||
void macroError(String error);
|
||||
}
|
||||
|
||||
public interface MacroPromptHandler {
|
||||
String prompt(MacroActionPrompt prompt);
|
||||
}
|
||||
|
||||
private MacroScreens macScreens = new MacroScreens();
|
||||
private ECLSession eclSess;
|
||||
private Telnet3270Client client;
|
||||
private PlayThread playThread;
|
||||
private int state = STATE_STOPPED;
|
||||
private MacroPromptHandler promptHandler;
|
||||
private final List<MacroListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private final Map<String, Macro> chainedMacros = new HashMap<>();
|
||||
|
||||
public Macro() {}
|
||||
|
||||
public Macro(MacroScreens screens) {
|
||||
setScreens(screens);
|
||||
}
|
||||
|
||||
public Macro(ECLSession session) {
|
||||
this.eclSess = session;
|
||||
}
|
||||
|
||||
public Macro(Telnet3270Client client) {
|
||||
setClient(client);
|
||||
}
|
||||
|
||||
public void setScreens(MacroScreens screens) {
|
||||
this.macScreens = (screens != null) ? screens : new MacroScreens();
|
||||
}
|
||||
|
||||
public MacroScreens getScreens() {
|
||||
return macScreens;
|
||||
}
|
||||
|
||||
public void setSession(ECLSession session) {
|
||||
this.eclSess = session;
|
||||
}
|
||||
|
||||
public ECLSession getECLSession() {
|
||||
return eclSess;
|
||||
}
|
||||
|
||||
public void setClient(Telnet3270Client client) {
|
||||
this.client = client;
|
||||
if (client != null && this.eclSess == null) {
|
||||
this.eclSess = new ECLSession(client);
|
||||
}
|
||||
}
|
||||
|
||||
public Telnet3270Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public MacroVariables getVariables() {
|
||||
return (macScreens != null) ? macScreens.getVariables() : null;
|
||||
}
|
||||
|
||||
public boolean isUseVars() {
|
||||
return macScreens != null && macScreens.isUseVars();
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public synchronized void setState(int newState) {
|
||||
if (this.state != newState) {
|
||||
int old = this.state;
|
||||
this.state = newState;
|
||||
for (MacroListener l : listeners) {
|
||||
try {
|
||||
l.macroStateChanged(this, old, newState);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setPromptHandler(MacroPromptHandler handler) {
|
||||
this.promptHandler = handler;
|
||||
}
|
||||
|
||||
public MacroPromptHandler getPromptHandler() {
|
||||
return promptHandler;
|
||||
}
|
||||
|
||||
public void addMacroListener(MacroListener listener) {
|
||||
if (listener != null && !listeners.contains(listener)) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeMacroListener(MacroListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public void load(String xml) {
|
||||
setScreens(MacroXmlParser.parse(xml));
|
||||
}
|
||||
|
||||
public synchronized void play() {
|
||||
play(null);
|
||||
}
|
||||
|
||||
public synchronized void play(String screenName) {
|
||||
if (state == STATE_PLAYING) {
|
||||
return;
|
||||
}
|
||||
if (state == STATE_PAUSED && playThread != null) {
|
||||
resume();
|
||||
return;
|
||||
}
|
||||
|
||||
stop();
|
||||
|
||||
if (eclSess == null && client != null) {
|
||||
eclSess = new ECLSession(client);
|
||||
}
|
||||
|
||||
if (macScreens.getVariables() != null) {
|
||||
macScreens.getVariables().reset();
|
||||
}
|
||||
|
||||
playThread = new PlayThread(this, macScreens, eclSess, screenName);
|
||||
playThread.start();
|
||||
}
|
||||
|
||||
public synchronized void pause() {
|
||||
if (state == STATE_PLAYING && playThread != null) {
|
||||
playThread.pausePlay();
|
||||
setState(STATE_PAUSED);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void resume() {
|
||||
if (state == STATE_PAUSED && playThread != null) {
|
||||
playThread.resumePlay();
|
||||
setState(STATE_PLAYING);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
if (playThread != null) {
|
||||
playThread.halt();
|
||||
playThread = null;
|
||||
}
|
||||
setState(STATE_STOPPED);
|
||||
}
|
||||
|
||||
public void halt() {
|
||||
stop();
|
||||
}
|
||||
|
||||
public void playChainedMacro(String name, String startScreen) {
|
||||
Macro chained = chainedMacros.get(name);
|
||||
if (chained != null) {
|
||||
chained.setSession(this.eclSess);
|
||||
chained.setClient(this.client);
|
||||
chained.setPromptHandler(this.promptHandler);
|
||||
chained.play(startScreen);
|
||||
} else {
|
||||
logError("Chained macro not found: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerChainedMacro(String name, Macro macro) {
|
||||
if (name != null && macro != null) {
|
||||
chainedMacros.put(name, macro);
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyScreenMatching(MacroScreen screen) {
|
||||
for (MacroListener l : listeners) {
|
||||
try {
|
||||
l.macroScreenMatching(screen);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyActionExecuting(MacroAction action) {
|
||||
for (MacroListener l : listeners) {
|
||||
try {
|
||||
l.macroActionExecuting(action);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyMessage(String title, String message) {
|
||||
for (MacroListener l : listeners) {
|
||||
try {
|
||||
l.macroMessage(title, message);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void logError(String error) {
|
||||
for (MacroListener l : listeners) {
|
||||
try {
|
||||
l.macroError(error);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package haus.nightmare.lib3270j.macro;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import haus.nightmare.lib3270j.macro.action.MacroAction;
|
||||
|
||||
/**
|
||||
* Collection of MacroAction items matching IBM Host On-Demand MacroActions.
|
||||
*/
|
||||
public class MacroActions implements Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final List<MacroAction> actions = new ArrayList<>();
|
||||
private boolean promptAll = false;
|
||||
|
||||
public MacroActions() {}
|
||||
|
||||
public MacroActions(List<MacroAction> actions) {
|
||||
if (actions != null) {
|
||||
this.actions.addAll(actions);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(MacroAction action) {
|
||||
if (action != null) {
|
||||
actions.add(action);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(int index, MacroAction action) {
|
||||
if (action != null) {
|
||||
actions.add(index, action);
|
||||
}
|
||||
}
|
||||
|
||||
public MacroAction get(int index) {
|
||||
return actions.get(index);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return actions.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return actions.isEmpty();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
actions.clear();
|
||||
}
|
||||
|
||||
public boolean remove(MacroAction action) {
|
||||
return actions.remove(action);
|
||||
}
|
||||
|
||||
public MacroAction remove(int index) {
|
||||
return actions.remove(index);
|
||||
}
|
||||
|
||||
public List<MacroAction> getActions() {
|
||||
return Collections.unmodifiableList(actions);
|
||||
}
|
||||
|
||||
public boolean isPromptAll() {
|
||||
return promptAll;
|
||||
}
|
||||
|
||||
public void setPromptAll(boolean promptAll) {
|
||||
this.promptAll = promptAll;
|
||||
}
|
||||
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String ind = "";
|
||||
for (int i = 0; i < indent; i++) ind += " ";
|
||||
|
||||
sb.append(ind).append("<actions");
|
||||
if (promptAll) sb.append(" promptall=\"true\"");
|
||||
sb.append(">\n");
|
||||
|
||||
for (MacroAction act : actions) {
|
||||
sb.append(act.format(indent + 1)).append("\n");
|
||||
}
|
||||
|
||||
sb.append(ind).append("</actions>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroActions clone() {
|
||||
MacroActions clone = new MacroActions();
|
||||
clone.promptAll = this.promptAll;
|
||||
for (MacroAction act : actions) {
|
||||
clone.add(act.clone());
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package haus.nightmare.lib3270j.macro;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Next screen references for state transitions matching IBM HoD MacroNextScreens.
|
||||
*/
|
||||
public class MacroNextScreens implements Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final List<String> nextScreens = new ArrayList<>();
|
||||
private int timeout = 0;
|
||||
|
||||
public MacroNextScreens() {}
|
||||
|
||||
public MacroNextScreens(int timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public void addNextScreen(String name) {
|
||||
if (name != null && !name.trim().isEmpty() && !nextScreens.contains(name.trim())) {
|
||||
nextScreens.add(name.trim());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsName(String name) {
|
||||
return name != null && nextScreens.contains(name.trim());
|
||||
}
|
||||
|
||||
public List<String> getNextScreenNames() {
|
||||
return Collections.unmodifiableList(nextScreens);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return nextScreens.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return nextScreens.isEmpty();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
nextScreens.clear();
|
||||
}
|
||||
|
||||
public int getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(int timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public String format(int indent) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String ind = "";
|
||||
for (int i = 0; i < indent; i++) ind += " ";
|
||||
|
||||
sb.append(ind).append("<nextscreens");
|
||||
if (timeout > 0) sb.append(" timeout=\"").append(timeout).append("\"");
|
||||
sb.append(">\n");
|
||||
|
||||
for (String name : nextScreens) {
|
||||
sb.append(ind).append(" <nextscreen name=\"").append(escapeXml(name)).append("\" />\n");
|
||||
}
|
||||
|
||||
sb.append(ind).append("</nextscreens>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String escapeXml(String text) {
|
||||
if (text == null) return "";
|
||||
return text.replace("&", "&")
|
||||
.replace("\"", """)
|
||||
.replace("<", "<")
|
||||
.replace(">", ">");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MacroNextScreens clone() {
|
||||
MacroNextScreens clone = new MacroNextScreens(timeout);
|
||||
for (String s : nextScreens) {
|
||||
clone.addNextScreen(s);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user