diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index 30bf7af..e15401f 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -6,6 +6,7 @@ import haus.nightmare.lib3270j.graphics.GraphicsMode; import haus.nightmare.lib3270j.listener.ConnectionListener; import haus.nightmare.lib3270j.listener.ScreenUpdateListener; import haus.nightmare.lib3270j.screen.ScreenBuffer; +import haus.nightmare.lib3270j.charset.CodePageRegistry; import haus.nightmare.j3270.ui.*; import haus.nightmare.j3270.ft.FileTransfer; import haus.nightmare.j3270.ft.FileTransferDialog; @@ -78,11 +79,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate private JCheckBoxMenuItem aplModeItem; private JCheckBoxMenuItem insertModeItem; private JCheckBoxMenuItem fourColorItem; - private JCheckBoxMenuItem numLockItem; - private JCheckBoxMenuItem autoSkipItem; - private JCheckBoxMenuItem insertOffAidItem; 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() { super("j3270 — Java TN3270 Terminal Emulator"); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); @@ -105,6 +113,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (client != null) { statusBar.updateStatus(); syncModeMenuItems(); + syncConnectionMenuItems(); if (!isModalDialogActive() && KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow() == this) { if (!terminalPanel.hasFocus()) { terminalPanel.requestFocusInWindow(); @@ -197,9 +206,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (aplModeItem != null) aplModeItem.setSelected(terminalPanel.isAplMode()); if (insertModeItem != null) insertModeItem.setSelected(terminalPanel.isInsertMode()); 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) { @@ -217,9 +223,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate // 1. File menu 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.addSeparator(); 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 JMenu connectionMenu = createMenu("Connection"); 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(); // Host Type Submenu (CMS/TSO/CICS) - JMenu hostTypeMenu = createMenu("Host Type"); - ButtonGroup hostTypeGroup = new ButtonGroup(); + hostTypeMenu = createMenu("Host Type"); + hostTypeGroup = new ButtonGroup(); for (haus.nightmare.lib3270j.ft.FTConfig.HostType ht : haus.nightmare.lib3270j.ft.FTConfig.HostType.values()) { JRadioButtonMenuItem htItem = new JRadioButtonMenuItem(ht.name()); ThemeManager.styleMenuItem(htItem); @@ -254,16 +258,44 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate hostTypeMenu.add(htItem); } 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(); // Saved Connections Submenu - JMenu savedHostsMenu = createMenu("Saved Connections"); + savedHostsMenu = createMenu("Saved Connections"); connectionMenu.add(savedHostsMenu); connectionMenu.addMenuListener(new javax.swing.event.MenuListener() { @Override public void menuSelected(javax.swing.event.MenuEvent e) { - updateConnectionMenu(hostTypeMenu, hostTypeGroup, savedHostsMenu); + updateConnectionMenu(); } @Override public void menuDeselected(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)); menuBar.add(editMenu); - // 3. View menu + // 4. View menu JMenu viewMenu = createMenu("View"); viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> 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); 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(); 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.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("Field Inspector...", -1, this::showFieldInspectorDialog)); menuBar.add(viewMenu); - // 4. Modes menu + // 5. Modes menu JMenu modesMenu = createMenu("Modes"); docModeItem = new JCheckBoxMenuItem("Document Mode (DOC)", terminalPanel.isDocMode()); ThemeManager.styleMenuItem(docModeItem); @@ -435,40 +429,6 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate insertModeItem.addActionListener(e -> terminalPanel.toggleInsertMode()); 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() { @Override public void menuSelected(javax.swing.event.MenuEvent e) { @@ -479,7 +439,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate }); menuBar.add(modesMenu); - // 5. Actions menu + // 6. Actions menu JMenu actionsMenu = createMenu("Actions"); actionsMenu.add(createMenuItem("Send Enter", KeyEvent.VK_ENTER, () -> { if (client != null && client.getConnectionState().isFullSession()) { @@ -525,11 +485,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate actionsMenu.addSeparator(); actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog)); actionsMenu.add(createMenuItem("Macro Runtime ()...", 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); - // 5. Help menu + // 7. Help menu JMenu helpMenu = createMenu("Help"); helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings)); 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)); menuBar.add(helpMenu); + syncConnectionMenuItems(); setJMenuBar(menuBar); } @@ -708,6 +668,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate private void changeCodePage(String cpId) { if (client != null) { client.setCodePage(cpId); + if (client.getConfig() != null) { + client.getConfig().setCodePage(cpId); + } client.getScreenBuffer().translateToUnicode(); terminalPanel.refreshScreen(); statusBar.updateStatus(); @@ -716,6 +679,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate } private void changeGraphicsMode(GraphicsMode mode) { + if (isConnected()) { + return; + } + if (client != null && client.getConfig() != null) { + client.getConfig().setGraphicsMode(mode); + } if (client != null && client.getDataStreamProcessor() != null) { client.getDataStreamProcessor().getQueryReplyBuilder().setGraphicsMode(mode); terminalPanel.repaint(); @@ -857,45 +826,103 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate } } - private void updateConnectionMenu(JMenu hostTypeMenu, ButtonGroup hostTypeGroup, JMenu savedHostsMenu) { - haus.nightmare.lib3270j.ft.FTConfig.HostType currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO; - if (client != null && client.getConfig() != null) { - currentHt = client.getConfig().getFtHostType(); - } else { - String def = Settings.getDefaultHostType(); - try { - if (def != null) currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(def.trim().toUpperCase()); - } catch (Exception ignored) {} + 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); + } + } - for (int i = 0; i < hostTypeMenu.getItemCount(); i++) { - JMenuItem item = hostTypeMenu.getItem(i); - if (item instanceof JRadioButtonMenuItem) { - JRadioButtonMenuItem rb = (JRadioButtonMenuItem) item; - if (rb.getText().equalsIgnoreCase(currentHt.name())) { - rb.setSelected(true); + 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; + if (client != null && client.getConfig() != null) { + currentHt = client.getConfig().getFtHostType(); + } else { + String def = Settings.getDefaultHostType(); + try { + if (def != null) currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(def.trim().toUpperCase()); + } catch (Exception ignored) {} + } + + for (int i = 0; i < hostTypeMenu.getItemCount(); i++) { + JMenuItem item = hostTypeMenu.getItem(i); + if (item instanceof JRadioButtonMenuItem) { + JRadioButtonMenuItem rb = (JRadioButtonMenuItem) item; + if (rb.getText().equalsIgnoreCase(currentHt.name())) { + rb.setSelected(true); + } } } } - savedHostsMenu.removeAll(); - java.util.List savedList = haus.nightmare.j3270.storage.HostStorage.getSavedHosts(); - if (savedList.isEmpty()) { - JMenuItem noneItem = new JMenuItem("(No Saved Connections)"); - noneItem.setEnabled(false); - ThemeManager.styleMenuItem(noneItem); - savedHostsMenu.add(noneItem); - } else { - for (haus.nightmare.j3270.storage.SavedHost sh : savedList) { - String label = sh.getName(); - if (label == null || label.trim().isEmpty()) { - label = sh.getHost() + ":" + sh.getPort(); + 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(); + java.util.List savedList = haus.nightmare.j3270.storage.HostStorage.getSavedHosts(); + if (savedList.isEmpty()) { + JMenuItem noneItem = new JMenuItem("(No Saved Connections)"); + noneItem.setEnabled(false); + ThemeManager.styleMenuItem(noneItem); + savedHostsMenu.add(noneItem); + } else { + for (haus.nightmare.j3270.storage.SavedHost sh : savedList) { + String label = sh.getName(); + if (label == null || label.trim().isEmpty()) { + label = sh.getHost() + ":" + sh.getPort(); + } + String ht = (sh.getHostType() != null && !sh.getHostType().trim().isEmpty()) ? sh.getHostType().trim() : "TSO"; + JMenuItem hostItem = new JMenuItem(label + " [" + ht + "]"); + ThemeManager.styleMenuItem(hostItem); + hostItem.addActionListener(ev -> connect(sh.toConnectionConfig(), true)); + savedHostsMenu.add(hostItem); } - String ht = (sh.getHostType() != null && !sh.getHostType().trim().isEmpty()) ? sh.getHostType().trim() : "TSO"; - JMenuItem hostItem = new JMenuItem(label + " [" + ht + "]"); - ThemeManager.styleMenuItem(hostItem); - hostItem.addActionListener(ev -> connect(sh.toConnectionConfig(), true)); - savedHostsMenu.add(hostItem); } } } @@ -1166,6 +1193,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate terminalPanel.setClient(null); statusBar.setClient(null, terminalPanel); syncModeMenuItems(); + syncConnectionMenuItems(); terminalPanel.repaint(); 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) { log.info("Connection state: " + oldState + " -> " + newState); SwingUtilities.invokeLater(() -> { + syncConnectionMenuItems(); statusBar.updateStatus(); terminalPanel.repaint(); @@ -1378,20 +1407,32 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate JOptionPane.showMessageDialog(this, sp, "Mnemonic Keystroke Reference", JOptionPane.INFORMATION_MESSAGE); } + /** + * Generates the descriptive text for the About dialog and version reporting. + */ + public static String getAboutText() { + 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" + + "• TN3270 & TN3270E (RFC 2355, SSCP-LU, NVT)\n" + + "• Models 2-5 & Dynamic Geometry\n" + + "• IBM 3287/3286 Printer Session Emulation (SCS & LU3)\n" + + "• IND$FILE CUT & DFT File Transfer + Host Catalog Query\n" + + "• GDDM/GOCA Vector Graphics & Programmed Symbols\n" + + "• 40+ Pluggable EBCDIC Codepages & DBCS Support\n" + + "• ECL Presentation Space Search, Inspector & Scripting"; + } + private void showAbout() { - JOptionPane.showMessageDialog(this, - "j3270 — Java TN3270 Terminal Emulator\n\n" + - "A pure Java reimplementation of x3270 & IBM Host On-Demand ECL.\n" + - "lib3270j v0.2.0\n\n" + - "Features:\n" + - "• TN3270 & TN3270E (RFC 2355, SSCP-LU, NVT)\n" + - "• Models 2-5 & Dynamic Geometry\n" + - "• IBM 3287/3286 Printer Session Emulation (SCS & LU3)\n" + - "• IND$FILE CUT & DFT File Transfer + Host Catalog Query\n" + - "• GDDM/GOCA Vector Graphics & Programmed Symbols\n" + - "• 22+ Pluggable EBCDIC Codepages & DBCS Support\n" + - "• ECL Presentation Space Search, Inspector & Scripting", - "About j3270", JOptionPane.INFORMATION_MESSAGE); + 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 Host type: TSO, VM, CMS, CICS"); + System.out.println(" -g, --graphics Graphics mode: NONE, VECTOR, PROGRAMMED_SYMBOLS, FULL"); + System.out.println(" -c, --config Load INI configuration file"); + System.out.println(" --proxy Proxy URL (e.g. socks5://host:1080, http://host:8080)"); + System.out.println(" --profile 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 ========== public static void main(String[] args) { @@ -1514,7 +1579,13 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate java.util.List remainingArgs = new java.util.ArrayList<>(); for (int i = 0; i < args.length; 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; } else if ("-s".equals(arg) || "--tls".equals(arg) || "--ssl".equals(arg)) { cliTls = true; diff --git a/j3270/src/main/java/haus/nightmare/j3270/Version.java b/j3270/src/main/java/haus/nightmare/j3270/Version.java new file mode 100644 index 0000000..63ef4f2 --- /dev/null +++ b/j3270/src/main/java/haus/nightmare/j3270/Version.java @@ -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; + } +} diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java index cc2c840..ca01104 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java @@ -4,6 +4,7 @@ import haus.nightmare.j3270.storage.HostStorage; import haus.nightmare.j3270.storage.SavedHost; import haus.nightmare.lib3270j.ConnectionConfig; import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.CodePageRegistry; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -278,51 +279,7 @@ public class ConnectDialog extends JDialog { contentPanel.add(cpLabel, gbc); gbc.gridx = 1; gbc.weightx = 1.0; - String[] commonCodePages = { - "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 = new JComboBox<>(CodePageRegistry.COMMON_CODE_PAGES); codePageCombo.setFont(ThemeManager.getMonospacedUiFont()); ThemeManager.styleComboBox(codePageCombo); setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage()); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java index 9aa4163..a3707d5 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -1998,7 +1998,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I SwingUtilities.invokeLater(() -> { Container top = getTopLevelAncestor(); 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"); } }); }); diff --git a/j3270/src/test/java/haus/nightmare/j3270/Phase31AboutAndInterfaceTest.java b/j3270/src/test/java/haus/nightmare/j3270/Phase31AboutAndInterfaceTest.java new file mode 100644 index 0000000..7670ee2 --- /dev/null +++ b/j3270/src/test/java/haus/nightmare/j3270/Phase31AboutAndInterfaceTest.java @@ -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(); + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/Version.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/Version.java new file mode 100644 index 0000000..eab9e1a --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/Version.java @@ -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; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java index 73090aa..2530c70 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/charset/CodePageRegistry.java @@ -15,6 +15,58 @@ public class CodePageRegistry { private static final Map CODE_PAGES = new LinkedHashMap<>(); private static final Map 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 { // Register core SBCS codepages register(new Cp037());