From 6331e0108badf9b1a71dd0ed2c9b4d6c1ef65334 Mon Sep 17 00:00:00 2001 From: Rudi Date: Sun, 6 Sep 2026 09:53:43 -0400 Subject: [PATCH] Update tests and Status/UI --- .../java/haus/nightmare/j3270/J3270App.java | 94 +++-- .../haus/nightmare/j3270/config/Settings.java | 77 +++- .../nightmare/j3270/ui/SettingsDialog.java | 21 +- .../haus/nightmare/j3270/ui/StatusBar.java | 28 +- .../nightmare/j3270/ui/TerminalPanel.java | 129 ++++++- .../nightmare/j3270/ui/KeyBindingsTest.java | 337 ++++++++++++++++++ .../j3270/ui/MenuBarShortcutsTest.java | 1 + 7 files changed, 619 insertions(+), 68 deletions(-) create mode 100644 j3270/src/test/java/haus/nightmare/j3270/ui/KeyBindingsTest.java diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index 97c404e..0e665d8 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -45,6 +45,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate private FieldInspectorDialog fieldInspectorDialog; private PrinterSessionDialog printerSessionDialog; + // Modes menu items + private JCheckBoxMenuItem docModeItem; + private JCheckBoxMenuItem wordWrapItem; + private JCheckBoxMenuItem aplModeItem; + private JCheckBoxMenuItem insertModeItem; + private JCheckBoxMenuItem fourColorItem; + private JCheckBoxMenuItem numLockItem; + private JCheckBoxMenuItem autoSkipItem; + private JCheckBoxMenuItem insertOffAidItem; + private JCheckBoxMenuItem statusBarItem; + public J3270App() { super("j3270 — Java TN3270 Terminal Emulator"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); @@ -63,6 +74,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate refreshTimer = new Timer(100, e -> { if (client != null) { statusBar.updateStatus(); + syncModeMenuItems(); if (isActive() && !terminalPanel.hasFocus()) { terminalPanel.requestFocusInWindow(); } @@ -93,6 +105,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate terminalPanel = new TerminalPanel(); statusBar = new StatusBar(); terminalPanel.setStatusBar(statusBar); + terminalPanel.setStatusBarToggleCallback(this::toggleStatusBar); + terminalPanel.addModeChangeListener(this::syncModeMenuItems); + + statusBar.setVisible(haus.nightmare.j3270.config.Settings.getStatusBarVisible()); getContentPane().setLayout(new BorderLayout()); getContentPane().setBackground(Color.BLACK); @@ -100,6 +116,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate getContentPane().add(statusBar, BorderLayout.SOUTH); } + private void syncModeMenuItems() { + if (docModeItem != null) docModeItem.setSelected(terminalPanel.isDocMode()); + if (wordWrapItem != null) wordWrapItem.setSelected(terminalPanel.isWordWrap()); + if (aplModeItem != null) aplModeItem.setSelected(terminalPanel.isAplMode()); + if (insertModeItem != null) insertModeItem.setSelected(terminalPanel.isInsertMode()); + if (fourColorItem != null) fourColorItem.setSelected(haus.nightmare.j3270.config.Settings.getFourColorOverride()); + if (numLockItem != null) numLockItem.setSelected(haus.nightmare.j3270.config.Settings.getNumericFieldLock()); + if (autoSkipItem != null) autoSkipItem.setSelected(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); + if (insertOffAidItem != null) insertOffAidItem.setSelected(haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); + } + private void onThemeChanged(UITheme theme) { buildMenuBar(); statusBar.applyTheme(theme); @@ -179,6 +206,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate viewMenu.add(themeMenu); viewMenu.addSeparator(); + statusBarItem = new JCheckBoxMenuItem("Status Bar", statusBar != null ? statusBar.isVisible() : haus.nightmare.j3270.config.Settings.getStatusBarVisible()); + ThemeManager.styleMenuItem(statusBarItem); + statusBarItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.ALT_DOWN_MASK)); + statusBarItem.addActionListener(e -> setStatusBarVisible(statusBarItem.isSelected())); + viewMenu.add(statusBarItem); + JCheckBoxMenuItem rulerItem = new JCheckBoxMenuItem("Crosshair Ruler", terminalPanel.isCrosshairRulerEnabled()); ThemeManager.styleMenuItem(rulerItem); rulerItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); @@ -262,38 +295,33 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate // 4. Modes menu JMenu modesMenu = createMenu("Modes"); - JCheckBoxMenuItem docModeItem = new JCheckBoxMenuItem("Document Mode (DOC)", terminalPanel.isDocMode()); + docModeItem = new JCheckBoxMenuItem("Document Mode (DOC)", terminalPanel.isDocMode()); ThemeManager.styleMenuItem(docModeItem); docModeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, InputEvent.ALT_DOWN_MASK)); docModeItem.addActionListener(e -> terminalPanel.toggleDocMode()); modesMenu.add(docModeItem); - JCheckBoxMenuItem wordWrapItem = new JCheckBoxMenuItem("Word Wrap Mode", terminalPanel.isWordWrap()); + wordWrapItem = new JCheckBoxMenuItem("Word Wrap Mode", terminalPanel.isWordWrap()); ThemeManager.styleMenuItem(wordWrapItem); wordWrapItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, InputEvent.ALT_DOWN_MASK)); wordWrapItem.addActionListener(e -> terminalPanel.toggleWordWrap()); modesMenu.add(wordWrapItem); - JCheckBoxMenuItem aplModeItem = new JCheckBoxMenuItem("APL Keyboard Mode", terminalPanel.isAplMode()); + aplModeItem = new JCheckBoxMenuItem("APL Keyboard Mode", terminalPanel.isAplMode()); ThemeManager.styleMenuItem(aplModeItem); aplModeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.ALT_DOWN_MASK)); aplModeItem.addActionListener(e -> terminalPanel.toggleAplMode()); modesMenu.add(aplModeItem); - JCheckBoxMenuItem insertModeItem = new JCheckBoxMenuItem("Insert Mode", client != null && client.isInsertMode()); + insertModeItem = new JCheckBoxMenuItem("Insert Mode", terminalPanel.isInsertMode()); ThemeManager.styleMenuItem(insertModeItem); insertModeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0)); - insertModeItem.addActionListener(e -> { - if (client != null) { - client.toggleInsert(); - terminalPanel.refreshScreen(); - } - }); + insertModeItem.addActionListener(e -> terminalPanel.toggleInsertMode()); modesMenu.add(insertModeItem); modesMenu.addSeparator(); - JCheckBoxMenuItem fourColorItem = new JCheckBoxMenuItem("Base 4-Color Override", haus.nightmare.j3270.config.Settings.getFourColorOverride()); + fourColorItem = new JCheckBoxMenuItem("Base 4-Color Override", haus.nightmare.j3270.config.Settings.getFourColorOverride()); ThemeManager.styleMenuItem(fourColorItem); fourColorItem.addActionListener(e -> { haus.nightmare.j3270.config.Settings.setFourColorOverride(fourColorItem.isSelected()); @@ -301,7 +329,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate }); modesMenu.add(fourColorItem); - JCheckBoxMenuItem numLockItem = new JCheckBoxMenuItem("Numeric Field Lock", haus.nightmare.j3270.config.Settings.getNumericFieldLock()); + numLockItem = new JCheckBoxMenuItem("Numeric Field Lock", haus.nightmare.j3270.config.Settings.getNumericFieldLock()); ThemeManager.styleMenuItem(numLockItem); numLockItem.addActionListener(e -> { haus.nightmare.j3270.config.Settings.setNumericFieldLock(numLockItem.isSelected()); @@ -309,7 +337,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate }); modesMenu.add(numLockItem); - JCheckBoxMenuItem autoSkipItem = new JCheckBoxMenuItem("Auto-Skip Across Fields", haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); + autoSkipItem = new JCheckBoxMenuItem("Auto-Skip Across Fields", haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); ThemeManager.styleMenuItem(autoSkipItem); autoSkipItem.addActionListener(e -> { haus.nightmare.j3270.config.Settings.setAutoSkipEnabled(autoSkipItem.isSelected()); @@ -317,7 +345,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate }); modesMenu.add(autoSkipItem); - JCheckBoxMenuItem insertOffAidItem = new JCheckBoxMenuItem("Reset Insert on AID Key", haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); + insertOffAidItem = new JCheckBoxMenuItem("Reset Insert on AID Key", haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); ThemeManager.styleMenuItem(insertOffAidItem); insertOffAidItem.addActionListener(e -> { haus.nightmare.j3270.config.Settings.setInsertOffOnAid(insertOffAidItem.isSelected()); @@ -328,14 +356,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate modesMenu.addMenuListener(new javax.swing.event.MenuListener() { @Override public void menuSelected(javax.swing.event.MenuEvent e) { - docModeItem.setSelected(terminalPanel.isDocMode()); - wordWrapItem.setSelected(terminalPanel.isWordWrap()); - aplModeItem.setSelected(terminalPanel.isAplMode()); - insertModeItem.setSelected(client != null && client.isInsertMode()); - fourColorItem.setSelected(haus.nightmare.j3270.config.Settings.getFourColorOverride()); - numLockItem.setSelected(haus.nightmare.j3270.config.Settings.getNumericFieldLock()); - autoSkipItem.setSelected(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); - insertOffAidItem.setSelected(haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); + syncModeMenuItems(); } @Override public void menuDeselected(javax.swing.event.MenuEvent e) {} @Override public void menuCanceled(javax.swing.event.MenuEvent e) {} @@ -726,6 +747,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate client = null; terminalPanel.setClient(null); statusBar.setClient(null, terminalPanel); + syncModeMenuItems(); terminalPanel.repaint(); setTitle("j3270 — Java TN3270 Terminal Emulator"); } @@ -738,6 +760,27 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate terminalPanel.guardedPack(); } + public void toggleStatusBar() { + if (statusBar != null) { + setStatusBarVisible(!statusBar.isVisible()); + } + } + + public void setStatusBarVisible(boolean visible) { + if (statusBar != null) { + statusBar.setVisible(visible); + } + haus.nightmare.j3270.config.Settings.setStatusBarVisible(visible); + if (statusBarItem != null) { + statusBarItem.setSelected(visible); + } + getContentPane().revalidate(); + getContentPane().repaint(); + if ((getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) { + terminalPanel.guardedPack(); + } + } + // ========== ConnectionListener ========== @Override @@ -786,6 +829,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate } terminalPanel.repaint(); statusBar.updateStatus(); + syncModeMenuItems(); }); } } @@ -818,7 +862,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate "End — Erase to end of field\n" + "Delete — Delete character\n" + "Backspace — Backspace\n" + - "Insert — Toggle insert mode\n" + + "Insert / Help / Alt+I — Toggle insert mode\n" + "Escape / Alt+R — Reset\n" + "PageUp/Down — PF7/PF8\n" + "Alt+C / Alt+K — Clear\n" + @@ -829,7 +873,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate "Alt+L — Toggle Light Pen\n" + "Alt+T — File Transfer\n" + "Alt+Shift+R — Crosshair Ruler\n" + + "Alt+B — Toggle Status Bar\n" + "Alt+=/-/0 — Font size +/-/reset\n" + + "Cmd/Ctrl+C / Ctrl+Ins — Copy\n" + + "Cmd/Ctrl+V / Shift+Ins — Paste\n" + + "Cmd/Ctrl+A — Select All\n" + "Cmd/Ctrl+F — Find on Screen\n" + "Cmd/Ctrl+G / F3— Find Next\n" + "Cmd/Ctrl+D — Disconnect\n" + diff --git a/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java b/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java index 7192983..de76ba6 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java +++ b/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java @@ -164,8 +164,61 @@ public class Settings { prefs.put("mono_" + key, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue())); } + public static boolean isMac() { + String os = System.getProperty("os.name"); + return os != null && os.toLowerCase().contains("mac"); + } + + public static String getDefaultBinding(String action) { + boolean mac = isMac(); + switch (action) { + case "COPY": + return mac ? "meta C, ctrl C, ctrl INSERT, ctrl shift C" : "ctrl C, ctrl INSERT, ctrl shift C"; + case "PASTE": + return mac ? "meta V, ctrl V, shift INSERT, ctrl shift V" : "ctrl V, shift INSERT, ctrl shift V"; + case "SELECTALL": + return mac ? "meta A, ctrl A" : "ctrl A"; + case "INSERT": + return mac ? "INSERT, HELP, alt I" : "INSERT, ctrl I"; + case "ERASE_INPUT": + return "alt E"; + case "NEWLINE": + return "shift ENTER"; + case "DUP": + return "alt D"; + case "FIELD_MARK": + return "alt M"; + case "ATTN": + return "alt A"; + case "SYSREQ": + return "alt S"; + case "CURSEL": + return "alt Q"; + case "CLEAR": + return "alt C"; + case "STATUS_BAR": + return "alt B"; + default: + return action; + } + } + public static String getKeyBinding(String action, String defaultBinding) { - return prefs.get("key_" + action, defaultBinding); + String val = prefs.get("key_" + action, null); + if (val == null) { + return defaultBinding; + } + // If stored binding is the legacy single-key "INSERT" default, migrate it to the current platform default + if ("INSERT".equals(action) && "INSERT".equals(val)) { + prefs.put("key_INSERT", defaultBinding); + return defaultBinding; + } + // If stored binding is the legacy or corrupted "alt STE" for PASTE, migrate it to the current platform default + if ("PASTE".equals(action) && ("alt STE".equals(val) || "PASTE".equals(val))) { + prefs.put("key_PASTE", defaultBinding); + return defaultBinding; + } + return val; } public static void setKeyBinding(String action, String binding) { @@ -268,6 +321,16 @@ public class Settings { prefs.putBoolean("crosshairRuler", enabled); } + // ========== Status Bar Visibility ========== + + public static boolean getStatusBarVisible() { + return prefs.getBoolean("statusBarVisible", true); + } + + public static void setStatusBarVisible(boolean visible) { + prefs.putBoolean("statusBarVisible", visible); + } + // ========== Cursor Style ========== public static String getCursorStyle() { @@ -388,6 +451,12 @@ public class Settings { case "ruler": setCrosshairRuler(Boolean.parseBoolean(value)); break; + case "statusBarVisible": + case "statusbar": + case "statusBar": + case "showStatusBar": + setStatusBarVisible(Boolean.parseBoolean(value)); + break; case "cursorStyle": setCursorStyle(value); break; @@ -545,6 +614,7 @@ public class Settings { w.println("fontFamily = " + getFontFamily()); w.println("fontSize = " + getFontSize()); w.println("crosshairRuler = " + getCrosshairRuler()); + w.println("statusBarVisible = " + getStatusBarVisible()); w.println("cursorStyle = " + getCursorStyle()); w.println(); @@ -610,9 +680,10 @@ public class Settings { // [keybindings] w.println("[keybindings]"); - // Navigation keys + // Navigation & clipboard keys String[] navActions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT", - "HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR"}; + "HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR", + "STATUS_BAR", "COPY", "PASTE", "SELECTALL"}; for (String act : navActions) { String val = prefs.get("key_" + act, null); if (val != null) { diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java index 1e2f62e..0203eba 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java @@ -502,7 +502,9 @@ public class SettingsDialog extends JDialog { String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT", "HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR", - "ERASE_INPUT", "NEWLINE", "DUP", "FIELD_MARK", "ATTN", "SYSREQ", "CURSEL"}; + "STATUS_BAR", + "ERASE_INPUT", "NEWLINE", "DUP", "FIELD_MARK", "ATTN", "SYSREQ", "CURSEL", + "COPY", "PASTE", "SELECTALL"}; keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) { @Override @@ -513,15 +515,7 @@ public class SettingsDialog extends JDialog { // Populate table from Settings or Defaults for(String act : actions) { - String def = act; - if (def.equals("ERASE_INPUT")) def = "alt E"; - else if (def.equals("NEWLINE")) def = "shift ENTER"; - else if (def.equals("DUP")) def = "alt D"; - else if (def.equals("FIELD_MARK")) def = "alt M"; - else if (def.equals("ATTN")) def = "alt A"; - else if (def.equals("SYSREQ")) def = "alt S"; - else if (def.equals("CURSEL")) def = "alt Q"; - else if (def.equals("CLEAR")) def = "alt C"; + String def = getDefaultBinding(act); String current = haus.nightmare.j3270.config.Settings.getKeyBinding(act, def); tempKeyBindings.put(act, current); keymapModel.addRow(new Object[]{act, current}); @@ -667,13 +661,12 @@ public class SettingsDialog extends JDialog { /** Returns the factory-default binding for a given action name. */ private String getDefaultBinding(String action) { - if (action.startsWith("PF")) { + if (action.matches("PF[0-9]+")) { int n = Integer.parseInt(action.substring(2)); return n <= 12 ? "F" + n : "shift F" + (n - 12); } - if (action.startsWith("PA")) return "alt " + action.substring(2); - if (action.equals("CLEAR")) return "alt C"; - return action; // nav keys default to their own name + if (action.matches("PA[1-3]")) return "alt " + action.substring(2); + return haus.nightmare.j3270.config.Settings.getDefaultBinding(action); } // ========== Apply Settings ========== diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java b/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java index 731fa91..8bc4bff 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java @@ -43,31 +43,35 @@ public class StatusBar extends JPanel { luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); lockStatus = createLabel("", oiaFont, ThemeManager.getOiaInputInhibited()); insertStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Insert Mode (^ / Insert key) - Click to toggle", () -> { - if (client != null) { + if (terminalPanel != null) { + terminalPanel.toggleInsertMode(); + } else if (client != null) { client.toggleInsert(); - if (terminalPanel != null) terminalPanel.refreshScreen(); updateStatus(); } }); aplStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaFgAlert(), "APL Keyboard Mode (Alt+F3) - Click to toggle", () -> { - if (client != null) { + if (terminalPanel != null) { + terminalPanel.toggleAplMode(); + } else if (client != null) { client.toggleAplMode(); - if (terminalPanel != null) terminalPanel.refreshScreen(); updateStatus(); } }); fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); docModeStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Entry Assist Document Mode (Alt+F1) - Click to toggle", () -> { - if (client != null) { + if (terminalPanel != null) { + terminalPanel.toggleDocMode(); + } else if (client != null) { client.toggleDocMode(); - if (terminalPanel != null) terminalPanel.refreshScreen(); updateStatus(); } }); wordWrapStatus = createClickableLabel("", oiaFont, ThemeManager.getOiaStatusSysAvail(), "Entry Assist Word Wrap (Alt+F2) - Click to toggle", () -> { - if (client != null) { + if (terminalPanel != null) { + terminalPanel.toggleWordWrap(); + } else if (client != null) { client.toggleWordWrap(); - if (terminalPanel != null) terminalPanel.refreshScreen(); updateStatus(); } }); @@ -269,7 +273,13 @@ public class StatusBar extends JPanel { } // Insert Mode Indicator - if (client.getInputProcessor() != null && client.getInputProcessor().isInsertMode()) { + boolean isInsert = false; + if (terminalPanel != null) { + isInsert = terminalPanel.isInsertMode(); + } else if (client.getInputProcessor() != null) { + isInsert = client.getInputProcessor().isInsertMode(); + } + if (isInsert) { insertStatus.setText("^ INS"); insertStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme)); } else { 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 0b47f66..e3e5b6d 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -86,6 +86,32 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { private boolean resizeGuard = false; private StatusBar statusBar = null; + // ========== Mode and Listener State ========== + private boolean insertMode = false; + + public interface ModeChangeListener { + void onModeChanged(); + } + private final java.util.List modeChangeListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); + + public void addModeChangeListener(ModeChangeListener listener) { + if (listener != null && !modeChangeListeners.contains(listener)) { + modeChangeListeners.add(listener); + } + } + + public void removeModeChangeListener(ModeChangeListener listener) { + modeChangeListeners.remove(listener); + } + + public void fireModeChanged() { + for (ModeChangeListener l : modeChangeListeners) { + try { + l.onModeChanged(); + } catch (Exception ignored) {} + } + } + /** * Compute the horizontal render offset to center the grid within the panel. */ @@ -462,13 +488,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { try { String text = (String) Toolkit.getDefaultToolkit().getSystemClipboard() .getData(DataFlavor.stringFlavor); - if (text != null) { - for (char ch : text.toCharArray()) { - if (ch == '\n' || ch == '\r') { - continue; - } - if (ch >= 0x20 && ch != 0x7F) { - client.typeCharacter(ch); + if (text != null && !text.isEmpty()) { + ScreenBuffer sb = client.getScreenBuffer(); + int curPos = sb != null ? sb.getDisplayCursorAddress() : 0; + int cols = sb != null ? sb.getDisplayCols() : 80; + int curRow = cols > 0 ? curPos / cols : 0; + int curCol = cols > 0 ? curPos % cols : 0; + + if (client.getPS() != null && (text.contains("\n") || text.contains("\r"))) { + client.getPS().pasteString(text, curRow, curCol); + } else { + for (char ch : text.toCharArray()) { + if (ch == '\n' || ch == '\r') { + continue; + } + if (ch >= 0x20 && ch != 0x7F) { + client.typeCharacter(ch); + } } } refreshScreen(); @@ -570,7 +606,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { resizeGuard = true; revalidate(); Container top = getTopLevelAncestor(); - if (top instanceof java.awt.Window) { + if (top instanceof java.awt.Frame) { + if ((((java.awt.Frame) top).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) { + ((java.awt.Window) top).pack(); + } + } else if (top instanceof java.awt.Window) { ((java.awt.Window) top).pack(); } SwingUtilities.invokeLater(() -> resizeGuard = false); @@ -603,7 +643,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { "PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE" }; for (String key : navKeys) { - String binding = haus.nightmare.j3270.config.Settings.getKeyBinding(key, key); + String defBinding = haus.nightmare.j3270.config.Settings.getDefaultBinding(key); + String binding = haus.nightmare.j3270.config.Settings.getKeyBinding(key, defBinding); bindKeyToMap(im, key, binding); } @@ -637,11 +678,16 @@ 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 - int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY"); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE"); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, shortcutMask), "j3270-SELECTALL"); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, java.awt.event.InputEvent.ALT_DOWN_MASK), "j3270-LIGHTPEN"); + 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, "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)); + bindKeyToMap(im, "LIGHTPEN", haus.nightmare.j3270.config.Settings.getKeyBinding("LIGHTPEN", "alt L")); + + String defStatusBar = haus.nightmare.j3270.config.Settings.getDefaultBinding("STATUS_BAR"); + bindKeyToMap(im, "STATUS_BAR", haus.nightmare.j3270.config.Settings.getKeyBinding("STATUS_BAR", defStatusBar)); // Action map implementations am.put("j3270-ENTER", createAction(this::handleEnter)); @@ -661,7 +707,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { am.put("j3270-END", createAction(this::handleEraseEOF)); am.put("j3270-DELETE", createAction(this::handleDelete)); am.put("j3270-BACK_SPACE", createAction(this::handleBackspace)); - am.put("j3270-INSERT", createAction(this::handleInsert)); + am.put("j3270-INSERT", createAction(this::toggleInsertMode)); am.put("j3270-CLEAR", createAction(this::handleClear)); am.put("j3270-ERASE_INPUT", createAction(this::handleEraseInput)); am.put("j3270-NEWLINE", createAction(this::handleNewline)); @@ -675,6 +721,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { am.put("j3270-PASTE", createAction(this::pasteClipboard)); am.put("j3270-SELECTALL", createAction(this::selectAll)); am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen)); + am.put("j3270-STATUS_BAR", createAction(this::toggleStatusBar)); for (int i = 1; i <= 24; i++) { final int pf = i; @@ -789,6 +836,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { } else if (state.isFullSession()) { client.sendEnter(); refreshScreen(); + fireModeChanged(); + if (statusBar != null) statusBar.updateStatus(); } } } @@ -805,6 +854,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { clearSelection(); client.reset(); refreshScreen(); + fireModeChanged(); + if (statusBar != null) statusBar.updateStatus(); } } @@ -873,6 +924,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { } client.sendPF(n); refreshScreen(); + fireModeChanged(); + if (statusBar != null) statusBar.updateStatus(); } } @@ -898,6 +951,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { if (client != null && client.getConnectionState().isFullSession()) { client.sendPA(n); refreshScreen(); + fireModeChanged(); + if (statusBar != null) statusBar.updateStatus(); } } @@ -941,13 +996,24 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { } } - private void handleInsert() { - if (client != null) { + public void toggleInsertMode() { + if (client != null && client.getInputProcessor() != null) { var ip = client.getInputProcessor(); ip.setInsertMode(!ip.isInsertMode()); - refreshScreen(); - if (statusBar != null) statusBar.updateStatus(); + insertMode = ip.isInsertMode(); + } else { + insertMode = !insertMode; } + refreshScreen(); + if (statusBar != null) statusBar.updateStatus(); + fireModeChanged(); + } + + public boolean isInsertMode() { + if (client != null && client.getInputProcessor() != null) { + return client.getInputProcessor().isInsertMode(); + } + return insertMode; } public void toggleDocMode() { @@ -956,6 +1022,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { haus.nightmare.j3270.config.Settings.setEntryAssistDocMode(client.isDocMode()); refreshScreen(); if (statusBar != null) statusBar.updateStatus(); + fireModeChanged(); } } @@ -965,6 +1032,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { haus.nightmare.j3270.config.Settings.setEntryAssistWordWrap(client.isWordWrap()); refreshScreen(); if (statusBar != null) statusBar.updateStatus(); + fireModeChanged(); } } @@ -973,6 +1041,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { client.toggleAplMode(); refreshScreen(); if (statusBar != null) statusBar.updateStatus(); + fireModeChanged(); } } @@ -998,6 +1067,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { } client.sendClear(); refreshScreen(); + fireModeChanged(); + if (statusBar != null) statusBar.updateStatus(); } } @@ -1061,6 +1132,23 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { return statusBar; } + private Runnable statusBarToggleCallback; + + public void setStatusBarToggleCallback(Runnable callback) { + this.statusBarToggleCallback = callback; + } + + public void toggleStatusBar() { + if (statusBarToggleCallback != null) { + statusBarToggleCallback.run(); + } else if (statusBar != null) { + boolean visible = !statusBar.isVisible(); + statusBar.setVisible(visible); + haus.nightmare.j3270.config.Settings.setStatusBarVisible(visible); + guardedPack(); + } + } + public void applyModeSettings() { if (client != null) { ScreenBuffer sb = client.getScreenBuffer(); @@ -1184,6 +1272,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { applyModeSettings(); if (client.getInputProcessor() != null) { + client.getInputProcessor().setInsertMode(insertMode); client.getInputProcessor().setBellListener(() -> { SwingUtilities.invokeLater(() -> Toolkit.getDefaultToolkit().beep()); }); @@ -1220,6 +1309,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { }); }); } + fireModeChanged(); + if (statusBar != null) statusBar.updateStatus(); } private void setupGraphicsPlaneRenderer() { diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/KeyBindingsTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/KeyBindingsTest.java new file mode 100644 index 0000000..7a1f534 --- /dev/null +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/KeyBindingsTest.java @@ -0,0 +1,337 @@ +package haus.nightmare.j3270.ui; + +import haus.nightmare.j3270.J3270App; +import haus.nightmare.j3270.config.Settings; +import haus.nightmare.lib3270j.ConnectionConfig; +import haus.nightmare.lib3270j.Telnet3270Client; +import org.junit.jupiter.api.Test; + +import javax.swing.*; +import java.awt.HeadlessException; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +public class KeyBindingsTest { + + @Test + public void testDefaultBindingsContainRequiredKeys() { + String insertBindings = Settings.getDefaultBinding("INSERT"); + assertNotNull(insertBindings); + assertTrue(insertBindings.contains("INSERT"), "Default INSERT binding should contain INSERT key"); + if (Settings.isMac()) { + assertTrue(insertBindings.contains("HELP"), "macOS INSERT binding should contain HELP key"); + assertTrue(insertBindings.contains("alt I"), "macOS INSERT binding should contain alt I fallback"); + } else { + assertTrue(insertBindings.contains("ctrl I"), "Non-macOS INSERT binding should contain ctrl I fallback"); + } + + String copyBindings = Settings.getDefaultBinding("COPY"); + assertNotNull(copyBindings); + assertTrue(copyBindings.contains("ctrl INSERT"), "Default COPY binding should support 3270 Ctrl+Insert"); + assertTrue(copyBindings.contains("ctrl C"), "Default COPY binding should support Ctrl+C"); + assertTrue(copyBindings.contains("ctrl shift C"), "Default COPY binding should support Ctrl+Shift+C"); + if (Settings.isMac()) { + assertTrue(copyBindings.contains("meta C"), "macOS COPY binding should support Cmd+C"); + } + + String pasteBindings = Settings.getDefaultBinding("PASTE"); + assertNotNull(pasteBindings); + assertTrue(pasteBindings.contains("shift INSERT"), "Default PASTE binding should support 3270 Shift+Insert"); + assertTrue(pasteBindings.contains("ctrl V"), "Default PASTE binding should support Ctrl+V"); + assertTrue(pasteBindings.contains("ctrl shift V"), "Default PASTE binding should support Ctrl+Shift+V"); + if (Settings.isMac()) { + assertTrue(pasteBindings.contains("meta V"), "macOS PASTE binding should support Cmd+V"); + } + + String selectAllBindings = Settings.getDefaultBinding("SELECTALL"); + assertNotNull(selectAllBindings); + assertTrue(selectAllBindings.contains("ctrl A"), "Default SELECTALL binding should support Ctrl+A"); + if (Settings.isMac()) { + assertTrue(selectAllBindings.contains("meta A"), "macOS SELECTALL binding should support Cmd+A"); + } + } + + @Test + public void testTerminalPanelInputMapAndAction() { + TerminalPanel panel = new TerminalPanel(); + InputMap im = panel.getInputMap(JComponent.WHEN_FOCUSED); + ActionMap am = panel.getActionMap(); + + // Check INSERT key mappings + KeyStroke ksInsert = KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0); + assertEquals("j3270-INSERT", im.get(ksInsert), "VK_INSERT must map to j3270-INSERT"); + + if (Settings.isMac()) { + KeyStroke ksHelp = KeyStroke.getKeyStroke(KeyEvent.VK_HELP, 0); + assertEquals("j3270-INSERT", im.get(ksHelp), "VK_HELP must map to j3270-INSERT on Mac"); + KeyStroke ksAltI = KeyStroke.getKeyStroke("alt I"); + assertEquals("j3270-INSERT", im.get(ksAltI), "alt I must map to j3270-INSERT on Mac"); + } + + // Check COPY key mappings + KeyStroke ksCtrlIns = KeyStroke.getKeyStroke("ctrl INSERT"); + assertEquals("j3270-COPY", im.get(ksCtrlIns), "ctrl INSERT must map to j3270-COPY"); + KeyStroke ksCtrlC = KeyStroke.getKeyStroke("ctrl C"); + assertEquals("j3270-COPY", im.get(ksCtrlC), "ctrl C must map to j3270-COPY"); + if (Settings.isMac()) { + KeyStroke ksMetaC = KeyStroke.getKeyStroke("meta C"); + assertEquals("j3270-COPY", im.get(ksMetaC), "meta C must map to j3270-COPY on Mac"); + } + + // Check PASTE key mappings + KeyStroke ksShiftIns = KeyStroke.getKeyStroke("shift INSERT"); + assertEquals("j3270-PASTE", im.get(ksShiftIns), "shift INSERT must map to j3270-PASTE"); + KeyStroke ksCtrlV = KeyStroke.getKeyStroke("ctrl V"); + assertEquals("j3270-PASTE", im.get(ksCtrlV), "ctrl V must map to j3270-PASTE"); + if (Settings.isMac()) { + KeyStroke ksMetaV = KeyStroke.getKeyStroke("meta V"); + assertEquals("j3270-PASTE", im.get(ksMetaV), "meta V must map to j3270-PASTE on Mac"); + } + + // Check SELECTALL key mappings + KeyStroke ksCtrlA = KeyStroke.getKeyStroke("ctrl A"); + assertEquals("j3270-SELECTALL", im.get(ksCtrlA), "ctrl A must map to j3270-SELECTALL"); + if (Settings.isMac()) { + KeyStroke ksMetaA = KeyStroke.getKeyStroke("meta A"); + assertEquals("j3270-SELECTALL", im.get(ksMetaA), "meta A must map to j3270-SELECTALL on Mac"); + } + + // Test insert mode toggle action + assertFalse(panel.isInsertMode(), "Initial insertMode should be false"); + Action insertAction = am.get("j3270-INSERT"); + assertNotNull(insertAction, "j3270-INSERT action must exist"); + + AtomicBoolean modeChangedFired = new AtomicBoolean(false); + panel.addModeChangeListener(() -> modeChangedFired.set(true)); + + insertAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, "")); + assertTrue(panel.isInsertMode(), "insertMode should be true after invoking action"); + assertTrue(modeChangedFired.get(), "ModeChangeListener should have fired"); + + modeChangedFired.set(false); + insertAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, "")); + assertFalse(panel.isInsertMode(), "insertMode should be false after invoking action again"); + assertTrue(modeChangedFired.get(), "ModeChangeListener should have fired again"); + } + + @Test + public void testInsertModeSyncWithClient() { + TerminalPanel panel = new TerminalPanel(); + panel.toggleInsertMode(); + assertTrue(panel.isInsertMode()); + + Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23)); + panel.setClient(client); + + // When client is attached, client's inputProcessor should receive current insertMode + assertNotNull(client.getInputProcessor()); + assertTrue(client.getInputProcessor().isInsertMode(), "Client inputProcessor should inherit insertMode"); + assertTrue(panel.isInsertMode()); + + // Toggling on panel updates client + panel.toggleInsertMode(); + assertFalse(client.getInputProcessor().isInsertMode()); + assertFalse(panel.isInsertMode()); + + panel.toggleInsertMode(); + assertTrue(client.getInputProcessor().isInsertMode()); + assertTrue(panel.isInsertMode()); + } + + @Test + public void testJ3270AppModesMenuSync() { + J3270App app; + try { + app = new J3270App(); + } catch (HeadlessException e) { + return; + } + + try { + JMenuBar mb = app.getJMenuBar(); + assertNotNull(mb); + + 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 should exist"); + + JCheckBoxMenuItem insertItem = null; + for (int i = 0; i < modesMenu.getItemCount(); i++) { + JMenuItem item = modesMenu.getItem(i); + if (item instanceof JCheckBoxMenuItem && "Insert Mode".equals(item.getText())) { + insertItem = (JCheckBoxMenuItem) item; + break; + } + } + assertNotNull(insertItem, "Insert Mode menu item should exist"); + assertFalse(insertItem.isSelected(), "Initial Insert Mode item should not be selected"); + + // Find terminal panel and toggle + TerminalPanel panel = null; + for (java.awt.Component c : app.getContentPane().getComponents()) { + if (c instanceof TerminalPanel) { + panel = (TerminalPanel) c; + break; + } + } + assertNotNull(panel, "TerminalPanel should be found"); + + // Toggling via panel updates menu item + panel.toggleInsertMode(); + assertTrue(panel.isInsertMode()); + assertTrue(insertItem.isSelected(), "Menu item should reflect true insert mode"); + + panel.toggleInsertMode(); + assertFalse(panel.isInsertMode()); + assertFalse(insertItem.isSelected(), "Menu item should reflect false insert mode"); + + // Clicking menu item toggles insert mode on panel + insertItem.doClick(); + assertTrue(panel.isInsertMode(), "Panel should be in insert mode after clicking menu item"); + assertTrue(insertItem.isSelected(), "Menu item should be selected after click"); + + insertItem.doClick(); + assertFalse(panel.isInsertMode(), "Panel should exit insert mode after clicking menu item"); + assertFalse(insertItem.isSelected(), "Menu item should be deselected after click"); + } finally { + app.dispose(); + } + } + + @Test + public void testCopyAndPasteActions() { + try { + java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); + } catch (HeadlessException e) { + return; + } + + TerminalPanel panel = new TerminalPanel(); + Telnet3270Client client = new Telnet3270Client(new ConnectionConfig("localhost", 23)); + panel.setClient(client); + + var sb = client.getScreenBuffer(); + sb.setChar(0, 0, 'J'); + sb.setChar(0, 1, '3'); + sb.setChar(0, 2, '2'); + sb.setChar(0, 3, '7'); + sb.setChar(0, 4, '0'); + + panel.setSelectionRange(0, 4); + assertTrue(panel.hasSelection()); + assertEquals("J3270", panel.getSelectedText()); + + Action copyAction = panel.getActionMap().get("j3270-COPY"); + assertNotNull(copyAction); + copyAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, "")); + + try { + String clip = (String) java.awt.Toolkit.getDefaultToolkit().getSystemClipboard() + .getData(java.awt.datatransfer.DataFlavor.stringFlavor); + assertEquals("J3270", clip); + } catch (Exception e) { + fail("Clipboard read failed: " + e.getMessage()); + } + + // Test multiline paste handling + try { + java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents( + new java.awt.datatransfer.StringSelection("LINE1\nLINE2"), null); + Action pasteAction = panel.getActionMap().get("j3270-PASTE"); + assertNotNull(pasteAction); + pasteAction.actionPerformed(new ActionEvent(panel, ActionEvent.ACTION_PERFORMED, "")); + // Shouldn't throw any exceptions + } catch (Exception e) { + fail("Paste action failed: " + e.getMessage()); + } + } + + @Test + public void testToggleStatusBar() { + assertEquals("alt B", Settings.getDefaultBinding("STATUS_BAR")); + + TerminalPanel panel = new TerminalPanel(); + InputMap im = panel.getInputMap(JComponent.WHEN_FOCUSED); + ActionMap am = panel.getActionMap(); + + KeyStroke ksAltB = KeyStroke.getKeyStroke("alt B"); + assertEquals("j3270-STATUS_BAR", im.get(ksAltB), "alt B must map to j3270-STATUS_BAR"); + assertNotNull(am.get("j3270-STATUS_BAR"), "j3270-STATUS_BAR action must exist"); + + J3270App app; + try { + app = new J3270App(); + } catch (HeadlessException e) { + return; + } + + try { + JMenuBar mb = app.getJMenuBar(); + assertNotNull(mb); + + JMenu viewMenu = null; + for (int i = 0; i < mb.getMenuCount(); i++) { + JMenu m = mb.getMenu(i); + if (m != null && "View".equals(m.getText())) { + viewMenu = m; + break; + } + } + assertNotNull(viewMenu); + + JCheckBoxMenuItem statusBarItem = null; + for (int i = 0; i < viewMenu.getItemCount(); i++) { + JMenuItem item = viewMenu.getItem(i); + if (item instanceof JCheckBoxMenuItem && "Status Bar".equals(item.getText())) { + statusBarItem = (JCheckBoxMenuItem) item; + break; + } + } + assertNotNull(statusBarItem, "Status Bar menu item must exist in View menu"); + assertTrue(statusBarItem.isSelected(), "Status bar should be selected by default"); + + StatusBar bar = null; + for (java.awt.Component c : app.getContentPane().getComponents()) { + if (c instanceof StatusBar) { + bar = (StatusBar) c; + break; + } + } + assertNotNull(bar, "StatusBar component should exist in content pane"); + assertTrue(bar.isVisible(), "StatusBar should be visible by default"); + + // Test toggling via toggleStatusBar() + app.toggleStatusBar(); + assertFalse(bar.isVisible(), "StatusBar should be hidden after toggle"); + assertFalse(statusBarItem.isSelected(), "Menu item should be unchecked"); + assertFalse(Settings.getStatusBarVisible()); + + app.toggleStatusBar(); + assertTrue(bar.isVisible(), "StatusBar should be shown after second toggle"); + assertTrue(statusBarItem.isSelected(), "Menu item should be checked"); + assertTrue(Settings.getStatusBarVisible()); + + // Test toggling via menu item click + statusBarItem.doClick(); + assertFalse(bar.isVisible(), "StatusBar should be hidden after menu click"); + assertFalse(statusBarItem.isSelected(), "Menu item should be unchecked"); + + statusBarItem.doClick(); + assertTrue(bar.isVisible(), "StatusBar should be shown after menu click"); + assertTrue(statusBarItem.isSelected(), "Menu item should be checked"); + } finally { + app.dispose(); + Settings.setStatusBarVisible(true); + } + } +} diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java index 2d2e41b..714d1ba 100644 --- a/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/MenuBarShortcutsTest.java @@ -63,6 +63,7 @@ public class MenuBarShortcutsTest { assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS); assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS); assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0); + assertAcceleratorUsesAlt(viewItems.get("Status Bar"), KeyEvent.VK_B); // Crosshair Ruler must use Alt+Shift+R JMenuItem rulerItem = viewItems.get("Crosshair Ruler");