Cleaning the UI and organizing
Build and Test j3270 / Build JAR & Run Tests (Java 21) (push) Successful in 43s
Build and Test j3270 / Build JAR & Run Tests (Java 11) (push) Successful in 59s
Build and Test j3270 / Build JAR & Run Tests (Java 17) (push) Successful in 1m30s

This commit is contained in:
2026-10-02 12:26:58 +00:00
parent 03d5ec0fae
commit dde5918da8
7 changed files with 558 additions and 191 deletions
@@ -6,6 +6,7 @@ import haus.nightmare.lib3270j.graphics.GraphicsMode;
import haus.nightmare.lib3270j.listener.ConnectionListener; import haus.nightmare.lib3270j.listener.ConnectionListener;
import haus.nightmare.lib3270j.listener.ScreenUpdateListener; import haus.nightmare.lib3270j.listener.ScreenUpdateListener;
import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.lib3270j.screen.ScreenBuffer;
import haus.nightmare.lib3270j.charset.CodePageRegistry;
import haus.nightmare.j3270.ui.*; import haus.nightmare.j3270.ui.*;
import haus.nightmare.j3270.ft.FileTransfer; import haus.nightmare.j3270.ft.FileTransfer;
import haus.nightmare.j3270.ft.FileTransferDialog; import haus.nightmare.j3270.ft.FileTransferDialog;
@@ -78,11 +79,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private JCheckBoxMenuItem aplModeItem; private JCheckBoxMenuItem aplModeItem;
private JCheckBoxMenuItem insertModeItem; private JCheckBoxMenuItem insertModeItem;
private JCheckBoxMenuItem fourColorItem; private JCheckBoxMenuItem fourColorItem;
private JCheckBoxMenuItem numLockItem;
private JCheckBoxMenuItem autoSkipItem;
private JCheckBoxMenuItem insertOffAidItem;
private JCheckBoxMenuItem statusBarItem; private JCheckBoxMenuItem statusBarItem;
// Connection menu items
private JMenuItem disconnectMenuItem;
private JMenu hostTypeMenu;
private ButtonGroup hostTypeGroup;
private JMenu codePageMenu;
private ButtonGroup codePageGroup;
private JMenu graphicsModeMenu;
private ButtonGroup graphicsModeGroup;
private JMenu savedHostsMenu;
public J3270App() { public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator"); super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
@@ -105,6 +113,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (client != null) { if (client != null) {
statusBar.updateStatus(); statusBar.updateStatus();
syncModeMenuItems(); syncModeMenuItems();
syncConnectionMenuItems();
if (!isModalDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) { if (!isModalDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) {
if (!terminalPanel.hasFocus()) { if (!terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow(); terminalPanel.requestFocusInWindow();
@@ -197,9 +206,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
if (aplModeItem != null) aplModeItem.setSelected(terminalPanel.isAplMode()); if (aplModeItem != null) aplModeItem.setSelected(terminalPanel.isAplMode());
if (insertModeItem != null) insertModeItem.setSelected(terminalPanel.isInsertMode()); if (insertModeItem != null) insertModeItem.setSelected(terminalPanel.isInsertMode());
if (fourColorItem != null) fourColorItem.setSelected(Settings.getFourColorOverride()); if (fourColorItem != null) fourColorItem.setSelected(Settings.getFourColorOverride());
if (numLockItem != null) numLockItem.setSelected(Settings.getNumericFieldLock());
if (autoSkipItem != null) autoSkipItem.setSelected(Settings.getAutoSkipEnabled());
if (insertOffAidItem != null) insertOffAidItem.setSelected(Settings.getInsertOffOnAid());
} }
private void onThemeChanged(UITheme theme) { private void onThemeChanged(UITheme theme) {
@@ -217,9 +223,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// 1. File menu // 1. File menu
JMenu fileMenu = createMenu("File"); JMenu fileMenu = createMenu("File");
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Close Window", KeyEvent.VK_W, this::closeWindow)); fileMenu.add(createMenuItem("Close Window", KeyEvent.VK_W, this::closeWindow));
fileMenu.addSeparator(); fileMenu.addSeparator();
fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel))); fileMenu.add(createMenuItem("Save Screen As...", -1, () -> ScreenExporter.showExportDialog(this, client, terminalPanel)));
@@ -240,12 +243,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// 2. Connection menu // 2. Connection menu
JMenu connectionMenu = createMenu("Connection"); JMenu connectionMenu = createMenu("Connection");
connectionMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog)); connectionMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
connectionMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect)); disconnectMenuItem = createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect);
connectionMenu.add(disconnectMenuItem);
connectionMenu.addSeparator(); connectionMenu.addSeparator();
// Host Type Submenu (CMS/TSO/CICS) // Host Type Submenu (CMS/TSO/CICS)
JMenu hostTypeMenu = createMenu("Host Type"); hostTypeMenu = createMenu("Host Type");
ButtonGroup hostTypeGroup = new ButtonGroup(); hostTypeGroup = new ButtonGroup();
for (haus.nightmare.lib3270j.ft.FTConfig.HostType ht : haus.nightmare.lib3270j.ft.FTConfig.HostType.values()) { for (haus.nightmare.lib3270j.ft.FTConfig.HostType ht : haus.nightmare.lib3270j.ft.FTConfig.HostType.values()) {
JRadioButtonMenuItem htItem = new JRadioButtonMenuItem(ht.name()); JRadioButtonMenuItem htItem = new JRadioButtonMenuItem(ht.name());
ThemeManager.styleMenuItem(htItem); ThemeManager.styleMenuItem(htItem);
@@ -254,16 +258,44 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
hostTypeMenu.add(htItem); hostTypeMenu.add(htItem);
} }
connectionMenu.add(hostTypeMenu); connectionMenu.add(hostTypeMenu);
// Code Page Submenu
codePageMenu = createMenu("Code Page");
codePageGroup = new ButtonGroup();
for (String cpItem : CodePageRegistry.COMMON_CODE_PAGES) {
String cpId = cpItem.split(" -")[0].trim();
JRadioButtonMenuItem cpMi = new JRadioButtonMenuItem(cpItem);
cpMi.putClientProperty("cpId", cpId);
ThemeManager.styleMenuItem(cpMi);
codePageGroup.add(cpMi);
cpMi.addActionListener(e -> changeCodePage(cpId));
codePageMenu.add(cpMi);
}
connectionMenu.add(codePageMenu);
// Graphics Mode Submenu
graphicsModeMenu = createMenu("Graphics Mode");
graphicsModeGroup = new ButtonGroup();
for (GraphicsMode gm : GraphicsMode.values()) {
JRadioButtonMenuItem gmMi = new JRadioButtonMenuItem(gm.getDescription());
gmMi.putClientProperty("mode", gm);
ThemeManager.styleMenuItem(gmMi);
graphicsModeGroup.add(gmMi);
gmMi.addActionListener(e -> changeGraphicsMode(gm));
graphicsModeMenu.add(gmMi);
}
connectionMenu.add(graphicsModeMenu);
connectionMenu.addSeparator(); connectionMenu.addSeparator();
// Saved Connections Submenu // Saved Connections Submenu
JMenu savedHostsMenu = createMenu("Saved Connections"); savedHostsMenu = createMenu("Saved Connections");
connectionMenu.add(savedHostsMenu); connectionMenu.add(savedHostsMenu);
connectionMenu.addMenuListener(new javax.swing.event.MenuListener() { connectionMenu.addMenuListener(new javax.swing.event.MenuListener() {
@Override @Override
public void menuSelected(javax.swing.event.MenuEvent e) { public void menuSelected(javax.swing.event.MenuEvent e) {
updateConnectionMenu(hostTypeMenu, hostTypeGroup, savedHostsMenu); updateConnectionMenu();
} }
@Override public void menuDeselected(javax.swing.event.MenuEvent e) {} @Override public void menuDeselected(javax.swing.event.MenuEvent e) {}
@Override public void menuCanceled(javax.swing.event.MenuEvent e) {} @Override public void menuCanceled(javax.swing.event.MenuEvent e) {}
@@ -286,7 +318,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
editMenu.add(createMenuItem("Find Previous", KeyEvent.VK_G, this::findPrevious, true)); editMenu.add(createMenuItem("Find Previous", KeyEvent.VK_G, this::findPrevious, true));
menuBar.add(editMenu); menuBar.add(editMenu);
// 3. View menu // 4. View menu
JMenu viewMenu = createMenu("View"); JMenu viewMenu = createMenu("View");
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2), true)); viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2), true));
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2), true)); viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2), true));
@@ -319,6 +351,14 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
themeMenu.add(lightThemeItem); themeMenu.add(lightThemeItem);
viewMenu.add(themeMenu); viewMenu.add(themeMenu);
fourColorItem = new JCheckBoxMenuItem("Base 4-Color Override", Settings.getFourColorOverride());
ThemeManager.styleMenuItem(fourColorItem);
fourColorItem.addActionListener(e -> {
Settings.setFourColorOverride(fourColorItem.isSelected());
terminalPanel.repaint();
});
viewMenu.add(fourColorItem);
viewMenu.addSeparator(); viewMenu.addSeparator();
statusBarItem = new JCheckBoxMenuItem("Status Bar", statusBar != null ? statusBar.isVisible() : Settings.getStatusBarVisible()); statusBarItem = new JCheckBoxMenuItem("Status Bar", statusBar != null ? statusBar.isVisible() : Settings.getStatusBarVisible());
@@ -359,57 +399,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
viewMenu.add(cursorMenu); viewMenu.add(cursorMenu);
viewMenu.addSeparator(); viewMenu.addSeparator();
// CodePage Submenu
JMenu cpMenu = createMenu("Code Page");
String[] codePages = {
"037 - US / Canada / Brazil",
"1047 - IBM Open Systems / z/OS Unix",
"500 - International Latin-1",
"273 - Germany / Austria",
"277 - Denmark / Norway",
"278 - Sweden / Finland",
"280 - Italy",
"284 - Spain / Latin America",
"285 - United Kingdom",
"297 - France",
"870 - Eastern Europe / Latin-2",
"871 - Iceland",
"875 - Greece (Greek)",
"1026 - Turkey (Turkish)",
"1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)",
"1148 - International (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS",
"933 - Korean Mixed DBCS"
};
for (String cpItem : codePages) {
String cpId = cpItem.split(" -")[0].trim();
JMenuItem cpMi = new JMenuItem(cpItem);
ThemeManager.styleMenuItem(cpMi);
cpMi.addActionListener(e -> changeCodePage(cpId));
cpMenu.add(cpMi);
}
viewMenu.add(cpMenu);
// Graphics Mode Submenu
JMenu gfxMenu = createMenu("Graphics Mode");
for (GraphicsMode gm : GraphicsMode.values()) {
JMenuItem gmMi = new JMenuItem(gm.name());
ThemeManager.styleMenuItem(gmMi);
gmMi.addActionListener(e -> changeGraphicsMode(gm));
gfxMenu.add(gmMi);
}
viewMenu.add(gfxMenu);
viewMenu.addSeparator();
viewMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true)); viewMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog)); viewMenu.add(createMenuItem("Field Inspector...", -1, this::showFieldInspectorDialog));
menuBar.add(viewMenu); menuBar.add(viewMenu);
// 4. Modes menu // 5. Modes menu
JMenu modesMenu = createMenu("Modes"); JMenu modesMenu = createMenu("Modes");
docModeItem = new JCheckBoxMenuItem("Document Mode (DOC)", terminalPanel.isDocMode()); docModeItem = new JCheckBoxMenuItem("Document Mode (DOC)", terminalPanel.isDocMode());
ThemeManager.styleMenuItem(docModeItem); ThemeManager.styleMenuItem(docModeItem);
@@ -435,40 +429,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
insertModeItem.addActionListener(e -> terminalPanel.toggleInsertMode()); insertModeItem.addActionListener(e -> terminalPanel.toggleInsertMode());
modesMenu.add(insertModeItem); modesMenu.add(insertModeItem);
modesMenu.addSeparator();
fourColorItem = new JCheckBoxMenuItem("Base 4-Color Override", Settings.getFourColorOverride());
ThemeManager.styleMenuItem(fourColorItem);
fourColorItem.addActionListener(e -> {
Settings.setFourColorOverride(fourColorItem.isSelected());
terminalPanel.repaint();
});
modesMenu.add(fourColorItem);
numLockItem = new JCheckBoxMenuItem("Numeric Field Lock", Settings.getNumericFieldLock());
ThemeManager.styleMenuItem(numLockItem);
numLockItem.addActionListener(e -> {
Settings.setNumericFieldLock(numLockItem.isSelected());
terminalPanel.applyModeSettings();
});
modesMenu.add(numLockItem);
autoSkipItem = new JCheckBoxMenuItem("Auto-Skip Across Fields", Settings.getAutoSkipEnabled());
ThemeManager.styleMenuItem(autoSkipItem);
autoSkipItem.addActionListener(e -> {
Settings.setAutoSkipEnabled(autoSkipItem.isSelected());
terminalPanel.applyModeSettings();
});
modesMenu.add(autoSkipItem);
insertOffAidItem = new JCheckBoxMenuItem("Reset Insert on AID Key", Settings.getInsertOffOnAid());
ThemeManager.styleMenuItem(insertOffAidItem);
insertOffAidItem.addActionListener(e -> {
Settings.setInsertOffOnAid(insertOffAidItem.isSelected());
terminalPanel.applyModeSettings();
});
modesMenu.add(insertOffAidItem);
modesMenu.addMenuListener(new javax.swing.event.MenuListener() { modesMenu.addMenuListener(new javax.swing.event.MenuListener() {
@Override @Override
public void menuSelected(javax.swing.event.MenuEvent e) { public void menuSelected(javax.swing.event.MenuEvent e) {
@@ -479,7 +439,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}); });
menuBar.add(modesMenu); menuBar.add(modesMenu);
// 5. Actions menu // 6. Actions menu
JMenu actionsMenu = createMenu("Actions"); JMenu actionsMenu = createMenu("Actions");
actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> { actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
@@ -525,11 +485,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
actionsMenu.addSeparator(); actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog)); actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
actionsMenu.add(createMenuItem("Macro Runtime (<HASCRIPT>)...", KeyEvent.VK_M, this::showMacroDialog, true)); actionsMenu.add(createMenuItem("Macro Runtime (<HASCRIPT>)...", KeyEvent.VK_M, this::showMacroDialog, true));
actionsMenu.add(createMenuItem("Screen History...", KeyEvent.VK_H, this::showScreenHistoryDialog, true));
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true)); actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
menuBar.add(actionsMenu); menuBar.add(actionsMenu);
// 5. Help menu // 7. Help menu
JMenu helpMenu = createMenu("Help"); JMenu helpMenu = createMenu("Help");
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings)); helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
helpMenu.add(createMenuItem("Mnemonic Keystroke Reference", -1, this::showMnemonicHelp)); helpMenu.add(createMenuItem("Mnemonic Keystroke Reference", -1, this::showMnemonicHelp));
@@ -537,6 +496,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
helpMenu.add(createMenuItem("About", -1, this::showAbout)); helpMenu.add(createMenuItem("About", -1, this::showAbout));
menuBar.add(helpMenu); menuBar.add(helpMenu);
syncConnectionMenuItems();
setJMenuBar(menuBar); setJMenuBar(menuBar);
} }
@@ -708,6 +668,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
private void changeCodePage(String cpId) { private void changeCodePage(String cpId) {
if (client != null) { if (client != null) {
client.setCodePage(cpId); client.setCodePage(cpId);
if (client.getConfig() != null) {
client.getConfig().setCodePage(cpId);
}
client.getScreenBuffer().translateToUnicode(); client.getScreenBuffer().translateToUnicode();
terminalPanel.refreshScreen(); terminalPanel.refreshScreen();
statusBar.updateStatus(); statusBar.updateStatus();
@@ -716,6 +679,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
private void changeGraphicsMode(GraphicsMode mode) { private void changeGraphicsMode(GraphicsMode mode) {
if (isConnected()) {
return;
}
if (client != null && client.getConfig() != null) {
client.getConfig().setGraphicsMode(mode);
}
if (client != null && client.getDataStreamProcessor() != null) { if (client != null && client.getDataStreamProcessor() != null) {
client.getDataStreamProcessor().getQueryReplyBuilder().setGraphicsMode(mode); client.getDataStreamProcessor().getQueryReplyBuilder().setGraphicsMode(mode);
terminalPanel.repaint(); terminalPanel.repaint();
@@ -857,7 +826,31 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
} }
private void updateConnectionMenu(JMenu hostTypeMenu, ButtonGroup hostTypeGroup, JMenu savedHostsMenu) { public void syncConnectionMenuItems() {
boolean connected = isConnected();
if (disconnectMenuItem != null) {
disconnectMenuItem.setEnabled(connected);
}
if (graphicsModeMenu != null) {
graphicsModeMenu.setEnabled(!connected);
graphicsModeMenu.setToolTipText(connected
? "Graphics Mode is negotiated at connection time and cannot be changed while connected."
: null);
}
}
public JMenu getGraphicsModeMenu() {
return graphicsModeMenu;
}
public JMenu getCodePageMenu() {
return codePageMenu;
}
private void updateConnectionMenu() {
syncConnectionMenuItems();
if (hostTypeMenu != null) {
haus.nightmare.lib3270j.ft.FTConfig.HostType currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO; haus.nightmare.lib3270j.ft.FTConfig.HostType currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO;
if (client != null && client.getConfig() != null) { if (client != null && client.getConfig() != null) {
currentHt = client.getConfig().getFtHostType(); currentHt = client.getConfig().getFtHostType();
@@ -877,7 +870,40 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
} }
} }
}
if (codePageMenu != null) {
String currentCp = (client != null && client.getConfig() != null && client.getConfig().getCodePage() != null)
? client.getConfig().getCodePage()
: Settings.getCodePage();
for (int i = 0; i < codePageMenu.getItemCount(); i++) {
JMenuItem item = codePageMenu.getItem(i);
if (item instanceof JRadioButtonMenuItem) {
JRadioButtonMenuItem rb = (JRadioButtonMenuItem) item;
Object id = rb.getClientProperty("cpId");
if (id != null && id.toString().equalsIgnoreCase(currentCp)) {
rb.setSelected(true);
}
}
}
}
if (graphicsModeMenu != null) {
GraphicsMode currentGfx = (client != null && client.getConfig() != null && client.getConfig().getGraphicsMode() != null)
? client.getConfig().getGraphicsMode()
: Settings.getGraphicsMode();
for (int i = 0; i < graphicsModeMenu.getItemCount(); i++) {
JMenuItem item = graphicsModeMenu.getItem(i);
if (item instanceof JRadioButtonMenuItem) {
JRadioButtonMenuItem rb = (JRadioButtonMenuItem) item;
if (rb.getClientProperty("mode") == currentGfx) {
rb.setSelected(true);
}
}
}
}
if (savedHostsMenu != null) {
savedHostsMenu.removeAll(); savedHostsMenu.removeAll();
java.util.List<haus.nightmare.j3270.storage.SavedHost> savedList = haus.nightmare.j3270.storage.HostStorage.getSavedHosts(); java.util.List<haus.nightmare.j3270.storage.SavedHost> savedList = haus.nightmare.j3270.storage.HostStorage.getSavedHosts();
if (savedList.isEmpty()) { if (savedList.isEmpty()) {
@@ -899,6 +925,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
} }
} }
}
private void showSettingsDialog() { private void showSettingsDialog() {
haus.nightmare.j3270.ui.SettingsDialog dialog = new haus.nightmare.j3270.ui.SettingsDialog(this); haus.nightmare.j3270.ui.SettingsDialog dialog = new haus.nightmare.j3270.ui.SettingsDialog(this);
@@ -1166,6 +1193,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
terminalPanel.setClient(null); terminalPanel.setClient(null);
statusBar.setClient(null, terminalPanel); statusBar.setClient(null, terminalPanel);
syncModeMenuItems(); syncModeMenuItems();
syncConnectionMenuItems();
terminalPanel.repaint(); terminalPanel.repaint();
setTitle("j3270 — Java TN3270 Terminal Emulator"); setTitle("j3270 — Java TN3270 Terminal Emulator");
}; };
@@ -1212,6 +1240,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) { public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
log.info("Connection state: " + oldState + " -> " + newState); log.info("Connection state: " + oldState + " -> " + newState);
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
syncConnectionMenuItems();
statusBar.updateStatus(); statusBar.updateStatus();
terminalPanel.repaint(); terminalPanel.repaint();
@@ -1378,20 +1407,32 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE); JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE);
} }
private void showAbout() { /**
JOptionPane.showMessageDialog(this, * Generates the descriptive text for the About dialog and version reporting.
"j3270 — Java TN3270 Terminal Emulator\n\n" + */
"A pure Java reimplementation of x3270 & IBM Host On-Demand ECL.\n" + public static String getAboutText() {
"lib3270j v0.2.0\n\n" + return "j3270 v" + Version.getVersion() + " — Java TN3270 Terminal Emulator\n" +
"lib3270j v" + haus.nightmare.lib3270j.Version.getVersion() + "\n\n" +
"A pure Java reimplementation of x3270 & IBM Host On-Demand ECL.\n\n" +
"Features:\n" + "Features:\n" +
"• TN3270 & TN3270E (RFC 2355, SSCP-LU, NVT)\n" + "• TN3270 & TN3270E (RFC 2355, SSCP-LU, NVT)\n" +
"• Models 2-5 & Dynamic Geometry\n" + "• Models 2-5 & Dynamic Geometry\n" +
"• IBM 3287/3286 Printer Session Emulation (SCS & LU3)\n" + "• IBM 3287/3286 Printer Session Emulation (SCS & LU3)\n" +
"• IND$FILE CUT & DFT File Transfer + Host Catalog Query\n" + "• IND$FILE CUT & DFT File Transfer + Host Catalog Query\n" +
"• GDDM/GOCA Vector Graphics & Programmed Symbols\n" + "• GDDM/GOCA Vector Graphics & Programmed Symbols\n" +
"• 22+ Pluggable EBCDIC Codepages & DBCS Support\n" + "• 40+ Pluggable EBCDIC Codepages & DBCS Support\n" +
"• ECL Presentation Space Search, Inspector & Scripting", "• ECL Presentation Space Search, Inspector & Scripting";
"About j3270", JOptionPane.INFORMATION_MESSAGE); }
private void showAbout() {
String aboutText = getAboutText();
Image iconImg = AppIcon.getDefaultIcon();
if (iconImg != null) {
ImageIcon icon = new ImageIcon(iconImg.getScaledInstance(64, 64, Image.SCALE_SMOOTH));
JOptionPane.showMessageDialog(this, aboutText, "About j3270", JOptionPane.INFORMATION_MESSAGE, icon);
} else {
JOptionPane.showMessageDialog(this, aboutText, "About j3270", JOptionPane.INFORMATION_MESSAGE);
}
} }
/** /**
@@ -1497,6 +1538,30 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
} }
} }
private static void printHelp() {
System.out.println("j3270 " + Version.getVersion() + " (lib3270j " + haus.nightmare.lib3270j.Version.getVersion() + ")");
System.out.println("Usage: java -jar j3270.jar [options] [host [port [model]]]");
System.out.println();
System.out.println("Options:");
System.out.println(" -v, --version Print version information and exit");
System.out.println(" -h, --help Print this help message and exit");
System.out.println(" -d, --debug Enable verbose debug logging to j3270.log");
System.out.println(" -s, --tls, --ssl Enable TLS/SSL encryption");
System.out.println(" -k, --insecure,");
System.out.println(" --no-verify-cert Disable TLS certificate verification");
System.out.println(" -p, -P, --no-tn3270e Disable TN3270E extended protocol negotiation");
System.out.println(" --tn3270e Enforce TN3270E negotiation");
System.out.println(" --auto-sys-unlock Auto unlock keyboard on host EOR");
System.out.println(" --no-auto-sys-unlock Require explicit Host RESET/unlock");
System.out.println(" -ht, --host-type <type> Host type: TSO, VM, CMS, CICS");
System.out.println(" -g, --graphics <mode> Graphics mode: NONE, VECTOR, PROGRAMMED_SYMBOLS, FULL");
System.out.println(" -c, --config <file> Load INI configuration file");
System.out.println(" --proxy <url> Proxy URL (e.g. socks5://host:1080, http://host:8080)");
System.out.println(" --profile <name> Connect using saved profile name or ID");
System.out.println();
System.out.println("Terminal Models: 2 (24x80), 3 (32x80), 4 (43x80, default), 5 (27x132), dynamic (0)");
}
// ========== Main ========== // ========== Main ==========
public static void main(String[] args) { public static void main(String[] args) {
@@ -1514,7 +1579,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
java.util.List<String> remainingArgs = new java.util.ArrayList<>(); java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {
String arg = args[i]; String arg = args[i];
if ("-d".equals(arg) || "--debug".equals(arg)) { if ("-v".equals(arg) || "--version".equals(arg)) {
System.out.println("j3270 " + Version.getVersion() + " (lib3270j " + haus.nightmare.lib3270j.Version.getVersion() + ")");
System.exit(0);
} else if ("-h".equals(arg) || "--help".equals(arg)) {
printHelp();
System.exit(0);
} else if ("-d".equals(arg) || "--debug".equals(arg)) {
debug = true; debug = true;
} else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) { } else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) {
cliTls = true; cliTls = true;
@@ -0,0 +1,37 @@
package haus.nightmare.j3270;
/**
* Version information for the j3270 application.
*/
public final class Version {
/**
* Hardcoded fallback release version matching build.gradle and pom coordinates.
*/
public static final String VERSION = "1.2.4";
/**
* Canonical application name.
*/
public static final String NAME = "j3270";
private Version() {}
/**
* Retrieves the runtime version string of the j3270 application.
* Inspects the package implementation version first (populated when loaded from a JAR),
* and falls back to the compile-time {@link #VERSION} constant if null or blank.
*
* @return the version string (e.g. "1.2.4")
*/
public static String getVersion() {
Package pkg = Version.class.getPackage();
if (pkg != null) {
String implVer = pkg.getImplementationVersion();
if (implVer != null && !implVer.isBlank()) {
return implVer;
}
}
return VERSION;
}
}
@@ -4,6 +4,7 @@ import haus.nightmare.j3270.storage.HostStorage;
import haus.nightmare.j3270.storage.SavedHost; import haus.nightmare.j3270.storage.SavedHost;
import haus.nightmare.lib3270j.ConnectionConfig; import haus.nightmare.lib3270j.ConnectionConfig;
import haus.nightmare.lib3270j.TerminalModel; import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.CodePageRegistry;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder; import javax.swing.border.EmptyBorder;
@@ -278,51 +279,7 @@ public class ConnectDialog extends JDialog {
contentPanel.add(cpLabel, gbc); contentPanel.add(cpLabel, gbc);
gbc.gridx = 1; gbc.gridx = 1;
gbc.weightx = 1.0; gbc.weightx = 1.0;
String[] commonCodePages = { codePageCombo = new JComboBox<>(CodePageRegistry.COMMON_CODE_PAGES);
"037 - US / Canada / Brazil",
"1047 - IBM Open Systems / z/OS Unix",
"500 - International Latin-1",
"273 - Germany / Austria",
"277 - Denmark / Norway",
"278 - Sweden / Finland",
"280 - Italy",
"284 - Spain / Latin America",
"285 - United Kingdom",
"297 - France",
"870 - Eastern Europe / Latin-2",
"871 - Iceland",
"875 - Greece (Greek)",
"1026 - Turkey (Turkish Latin-5)",
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
"905 - Turkey (Turkish Latin-3)",
"420 - Arabic Bilingual",
"424 - Hebrew (with Latin)",
"803 - Hebrew Character Set",
"838 - Thai Extended",
"1160 - Thai Euro \u20AC",
"1025 - Cyrillic Multilingual",
"1123 - Cyrillic Ukraine",
"1154 - Cyrillic Euro \u20AC",
"880 - Cyrillic (Russian)",
"1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)",
"1142 - Denmark / Norway (Euro \u20AC)",
"1143 - Sweden / Finland (Euro \u20AC)",
"1144 - Italy (Euro \u20AC)",
"1145 - Spain / Latin America (Euro \u20AC)",
"1146 - United Kingdom (Euro \u20AC)",
"1147 - France (Euro \u20AC)",
"1148 - International (Euro \u20AC)",
"1149 - Iceland (Euro \u20AC)",
"930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS",
"1388 - Simplified Chinese Extended DBCS",
"1371 - Traditional Chinese Extended DBCS",
"933 - Korean Mixed DBCS"
};
codePageCombo = new JComboBox<>(commonCodePages);
codePageCombo.setFont(ThemeManager.getMonospacedUiFont()); codePageCombo.setFont(ThemeManager.getMonospacedUiFont());
ThemeManager.styleComboBox(codePageCombo); ThemeManager.styleComboBox(codePageCombo);
setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage()); setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage());
@@ -1998,7 +1998,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
Container top = getTopLevelAncestor(); Container top = getTopLevelAncestor();
if (top instanceof JFrame) { if (top instanceof JFrame) {
((JFrame) top).setTitle(title != null && !title.isEmpty() ? ("j3270 - " + title) : "j3270"); ((JFrame) top).setTitle(title != null && !title.isEmpty() ? ("j3270 — " + title) : "j3270 — Java TN3270 Terminal Emulator");
} }
}); });
}); });
@@ -0,0 +1,213 @@
package haus.nightmare.j3270;
import haus.nightmare.lib3270j.charset.CodePage;
import haus.nightmare.lib3270j.charset.CodePageRegistry;
import haus.nightmare.lib3270j.graphics.GraphicsMode;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.HeadlessException;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 31:
* Help > About Dialogue Version Reporting and j3270 Interface Inconsistency Fixes (v1.2.4).
*/
public class Phase31AboutAndInterfaceTest {
@Test
@DisplayName("Phase 31.1: Dedicated Version classes report v1.2.4 single source of truth")
public void testVersionClasses() {
// lib3270j
assertEquals("1.2.4", haus.nightmare.lib3270j.Version.VERSION);
assertEquals("lib3270j", haus.nightmare.lib3270j.Version.NAME);
assertEquals("1.2.4", haus.nightmare.lib3270j.Version.getVersion());
// j3270
assertEquals("1.2.4", haus.nightmare.j3270.Version.VERSION);
assertEquals("j3270", haus.nightmare.j3270.Version.NAME);
assertEquals("1.2.4", haus.nightmare.j3270.Version.getVersion());
}
@Test
@DisplayName("Phase 31.2: Help > About dialogue text properly reports j3270 v1.2.4 and lib3270j v1.2.4")
public void testAboutTextContent() {
String aboutText = J3270App.getAboutText();
assertNotNull(aboutText, "About text must not be null");
// Verify version reports
assertTrue(aboutText.contains("j3270 v1.2.4"), "About text must report j3270 v1.2.4");
assertTrue(aboutText.contains("lib3270j v1.2.4"), "About text must report lib3270j v1.2.4");
// Verify stale pre-release versions are eradicated
assertFalse(aboutText.contains("0.2.0"), "About text must not contain stale version 0.2.0");
assertFalse(aboutText.contains("0.1.0"), "About text must not contain stale version 0.1.0");
// Verify 40+ codepages reporting
assertTrue(aboutText.contains("40+ Pluggable EBCDIC Codepages & DBCS Support"),
"About text must report 40+ Pluggable EBCDIC Codepages");
assertFalse(aboutText.contains("22+"), "About text must not report stale 22+ codepages");
}
@Test
@DisplayName("Phase 31.3: CodePageRegistry.COMMON_CODE_PAGES defines 40+ supported codepages that all resolve")
public void testCommonCodePagesRegistration() {
String[] common = CodePageRegistry.COMMON_CODE_PAGES;
assertNotNull(common, "COMMON_CODE_PAGES must not be null");
assertTrue(common.length >= 40, "COMMON_CODE_PAGES must define at least 40 entries, found: " + common.length);
for (String entry : common) {
String cpId = entry.split(" -")[0].trim();
CodePage cp = CodePageRegistry.resolveCodePage(cpId);
assertNotNull(cp, "Codepage identifier '" + cpId + "' from entry '" + entry + "' must resolve in CodePageRegistry");
}
}
@Test
@DisplayName("Phase 31.4: Menu bar consistency: Code Page & Graphics Mode in Connection menu, Screen History in View")
public void testMenuBarConsistency() {
J3270App app;
try {
app = new J3270App();
} catch (HeadlessException e) {
// Headless environment without X11/display
return;
}
try {
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb, "JMenuBar must exist");
JMenu viewMenu = null;
JMenu actionsMenu = null;
JMenu fileMenu = null;
JMenu connectionMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null) {
if ("View".equals(m.getText())) viewMenu = m;
else if ("Actions".equals(m.getText())) actionsMenu = m;
else if ("File".equals(m.getText())) fileMenu = m;
else if ("Connection".equals(m.getText())) connectionMenu = m;
}
}
assertNotNull(viewMenu, "View menu must exist");
assertNotNull(actionsMenu, "Actions menu must exist");
assertNotNull(fileMenu, "File menu must exist");
assertNotNull(connectionMenu, "Connection menu must exist");
// Verify Connect... and Disconnect are in Connection menu and NOT in File menu
boolean connectInConnection = false;
boolean disconnectInConnection = false;
for (int i = 0; i < connectionMenu.getItemCount(); i++) {
JMenuItem item = connectionMenu.getItem(i);
if (item != null) {
if ("Connect...".equals(item.getText())) connectInConnection = true;
if ("Disconnect".equals(item.getText())) disconnectInConnection = true;
}
}
assertTrue(connectInConnection, "Connect... must be in Connection menu");
assertTrue(disconnectInConnection, "Disconnect must be in Connection menu");
boolean connectInFile = false;
boolean disconnectInFile = false;
for (int i = 0; i < fileMenu.getItemCount(); i++) {
JMenuItem item = fileMenu.getItem(i);
if (item != null) {
if ("Connect...".equals(item.getText())) connectInFile = true;
if ("Disconnect".equals(item.getText())) disconnectInFile = true;
}
}
assertFalse(connectInFile, "Connect... must NOT be in File menu");
assertFalse(disconnectInFile, "Disconnect must NOT be in File menu");
// Verify Screen History is in View menu
boolean screenHistoryInView = false;
for (int i = 0; i < viewMenu.getItemCount(); i++) {
JMenuItem item = viewMenu.getItem(i);
if (item != null && "Screen History...".equals(item.getText())) {
screenHistoryInView = true;
}
}
assertTrue(screenHistoryInView, "Screen History... must be present in View menu");
// Verify Screen History is NOT duplicated in Actions menu
boolean screenHistoryInActions = false;
for (int i = 0; i < actionsMenu.getItemCount(); i++) {
JMenuItem item = actionsMenu.getItem(i);
if (item != null && "Screen History...".equals(item.getText())) {
screenHistoryInActions = true;
}
}
assertFalse(screenHistoryInActions, "Screen History... must NOT be duplicated in Actions menu");
// Verify Code Page and Graphics Mode are in Connection menu, NOT View menu
JMenu codePageSubmenu = null;
JMenu graphicsModeSubmenu = null;
for (int i = 0; i < connectionMenu.getItemCount(); i++) {
JMenuItem item = connectionMenu.getItem(i);
if (item != null) {
if ("Code Page".equals(item.getText()) && item instanceof JMenu) {
codePageSubmenu = (JMenu) item;
}
if ("Graphics Mode".equals(item.getText()) && item instanceof JMenu) {
graphicsModeSubmenu = (JMenu) item;
}
}
}
assertNotNull(codePageSubmenu, "Connection > Code Page submenu must exist");
assertNotNull(graphicsModeSubmenu, "Connection > Graphics Mode submenu must exist");
assertEquals(CodePageRegistry.COMMON_CODE_PAGES.length, codePageSubmenu.getItemCount(),
"Connection > Code Page submenu must contain all " + CodePageRegistry.COMMON_CODE_PAGES.length + " common codepages");
assertEquals(GraphicsMode.values().length, graphicsModeSubmenu.getItemCount(),
"Connection > Graphics Mode submenu must contain all " + GraphicsMode.values().length + " graphics modes");
// Graphics Mode must be enabled when disconnected
assertFalse(app.isConnected(), "App should not be connected by default");
assertTrue(graphicsModeSubmenu.isEnabled(), "Graphics Mode submenu should be enabled when disconnected");
// Verify Code Page and Graphics Mode are NOT in View menu
for (int i = 0; i < viewMenu.getItemCount(); i++) {
JMenuItem item = viewMenu.getItem(i);
if (item != null) {
assertNotEquals("Code Page", item.getText(), "Code Page must NOT be in View menu");
assertNotEquals("Graphics Mode", item.getText(), "Graphics Mode must NOT be in View menu");
}
}
// Verify Base 4-Color Override is in View menu
boolean base4InView = false;
for (int i = 0; i < viewMenu.getItemCount(); i++) {
JMenuItem item = viewMenu.getItem(i);
if (item != null && "Base 4-Color Override".equals(item.getText())) {
base4InView = true;
}
}
assertTrue(base4InView, "Base 4-Color Override must be present in View menu");
// Verify Modes menu contains strictly the 4 interactive operator modes
JMenu modesMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null && "Modes".equals(m.getText())) {
modesMenu = m;
break;
}
}
assertNotNull(modesMenu, "Modes menu must exist");
assertEquals(4, modesMenu.getItemCount(), "Modes menu should contain exactly 4 interactive operator modes");
assertEquals("Document Mode (DOC)", modesMenu.getItem(0).getText());
assertEquals("Word Wrap Mode", modesMenu.getItem(1).getText());
assertEquals("APL Keyboard Mode", modesMenu.getItem(2).getText());
assertEquals("Insert Mode", modesMenu.getItem(3).getText());
} finally {
app.dispose();
}
}
}
@@ -0,0 +1,37 @@
package haus.nightmare.lib3270j;
/**
* Version information for lib3270j.
*/
public final class Version {
/**
* Hardcoded fallback release version matching build.gradle and pom coordinates.
*/
public static final String VERSION = "1.2.4";
/**
* Canonical library name.
*/
public static final String NAME = "lib3270j";
private Version() {}
/**
* Retrieves the runtime version string of lib3270j.
* Inspects the package implementation version first (populated when loaded from a JAR),
* and falls back to the compile-time {@link #VERSION} constant if null or blank.
*
* @return the version string (e.g. "1.2.4")
*/
public static String getVersion() {
Package pkg = Version.class.getPackage();
if (pkg != null) {
String implVer = pkg.getImplementationVersion();
if (implVer != null && !implVer.isBlank()) {
return implVer;
}
}
return VERSION;
}
}
@@ -15,6 +15,58 @@ public class CodePageRegistry {
private static final Map<String, CodePage> CODE_PAGES = new LinkedHashMap<>(); private static final Map<String, CodePage> CODE_PAGES = new LinkedHashMap<>();
private static final Map<String, String> ALIASES = new HashMap<>(); private static final Map<String, String> ALIASES = new HashMap<>();
/**
* Standard human-readable codepage option labels for UI dropdowns and menus.
* Covers core SBCS, all 10 Euro variants (1140-1149), Extended SBCS (Arabic,
* Hebrew, Thai, Cyrillic, Turkish), and Mixed/Extended DBCS.
*/
public static final String[] COMMON_CODE_PAGES = {
"037 - US / Canada / Brazil",
"1047 - IBM Open Systems / z/OS Unix",
"500 - International Latin-1",
"273 - Germany / Austria",
"277 - Denmark / Norway",
"278 - Sweden / Finland",
"280 - Italy",
"284 - Spain / Latin America",
"285 - United Kingdom",
"297 - France",
"870 - Eastern Europe / Latin-2",
"871 - Iceland",
"875 - Greece (Greek)",
"1026 - Turkey (Turkish Latin-5)",
"1155 - Turkey (Turkish Latin-5 Euro \u20AC)",
"905 - Turkey (Turkish Latin-3)",
"420 - Arabic Bilingual",
"424 - Hebrew (with Latin)",
"803 - Hebrew Character Set",
"838 - Thai Extended",
"1160 - Thai Euro \u20AC",
"1025 - Cyrillic Multilingual",
"1123 - Cyrillic Ukraine",
"1154 - Cyrillic Euro \u20AC",
"880 - Cyrillic (Russian)",
"1140 - US / Canada (Euro \u20AC)",
"1141 - Germany / Austria (Euro \u20AC)",
"1142 - Denmark / Norway (Euro \u20AC)",
"1143 - Sweden / Finland (Euro \u20AC)",
"1144 - Italy (Euro \u20AC)",
"1145 - Spain / Latin America (Euro \u20AC)",
"1146 - United Kingdom (Euro \u20AC)",
"1147 - France (Euro \u20AC)",
"1148 - International (Euro \u20AC)",
"1149 - Iceland (Euro \u20AC)",
"290 - Japanese Katakana SBCS",
"1027 - Japanese Latin SBCS",
"930 - Japanese Katakana Mixed DBCS",
"939 - Japanese Latin Mixed DBCS",
"935 - Simplified Chinese Mixed DBCS",
"937 - Traditional Chinese Mixed DBCS",
"1388 - Simplified Chinese Extended DBCS",
"1371 - Traditional Chinese Extended DBCS",
"933 - Korean Mixed DBCS"
};
static { static {
// Register core SBCS codepages // Register core SBCS codepages
register(new Cp037()); register(new Cp037());