diff --git a/j3270.ini.sample b/j3270.ini.sample index 9add8f9..e457c96 100644 --- a/j3270.ini.sample +++ b/j3270.ini.sample @@ -3,6 +3,11 @@ ; or: ./run.sh -c j3270.ini [appearance] +; UI font settings (menus, dialogs, status bar) +; uiFontFamily = SansSerif +; uiFontSize = 13 +; +; Terminal font settings (3270 screen display) ; fontFamily = IBM 3270 ; fontSize = 18 @@ -10,6 +15,7 @@ ; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING ; autoConnectHost = mainframe.example.com ; autoConnectPort = 23 +; blockPaste = true [colors] ; Host colors 0-15 (hex RGB) diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index 9f474c7..916ebda 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -717,6 +717,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate return client; } + public StatusBar getStatusBar() { + return statusBar; + } + void connect(ConnectionConfig config) { lastHost = config.getHost(); lastPort = config.getPort(); @@ -946,7 +950,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate JTextArea area = new JTextArea(text); area.setEditable(false); - area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); + area.setFont(ThemeManager.getMonospacedUiFont()); ThemeManager.styleTextArea(area); JScrollPane sp = new JScrollPane(area); ThemeManager.styleScrollPane(sp); @@ -979,7 +983,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate JTextArea area = new JTextArea(text); area.setEditable(false); - area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); + area.setFont(ThemeManager.getMonospacedUiFont()); ThemeManager.styleTextArea(area); JScrollPane sp = new JScrollPane(area); ThemeManager.styleScrollPane(sp); 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 a55fef6..72ad4f1 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java +++ b/j3270/src/main/java/haus/nightmare/j3270/config/Settings.java @@ -45,6 +45,24 @@ public class Settings { flushPrefs(); } + public static String getUiFontFamily() { + return prefs.get("uiFontFamily", "SansSerif"); + } + + public static void setUiFontFamily(String family) { + prefs.put("uiFontFamily", (family != null && !family.trim().isEmpty()) ? family : "SansSerif"); + flushPrefs(); + } + + public static int getUiFontSize() { + return prefs.getInt("uiFontSize", 13); + } + + public static void setUiFontSize(int size) { + prefs.putInt("uiFontSize", Math.max(8, Math.min(size, 48))); + flushPrefs(); + } + private static void flushPrefs() { try { prefs.flush(); @@ -390,6 +408,17 @@ public class Settings { prefs.putBoolean("blockSelectMode", block); } + // ========== Block paste ========== + + public static boolean getBlockPaste() { + return prefs.getBoolean("blockPaste", true); + } + + public static void setBlockPaste(boolean block) { + prefs.putBoolean("blockPaste", block); + flushPrefs(); + } + // ========== Clipboard & Tabular Paste ========== public static boolean getEnablePasteFromExcel() { @@ -541,6 +570,16 @@ public class Settings { switch (key) { case "fontFamily": setFontFamily(value); break; case "fontSize": setFontSize(Integer.parseInt(value)); break; + case "uiFontFamily": + case "uifontfamily": + case "uifont": + case "uiFont": + setUiFontFamily(value); + break; + case "uiFontSize": + case "uifontsize": + setUiFontSize(Integer.parseInt(value)); + break; case "javaUiTheme": case "theme": case "uiTheme": @@ -608,6 +647,7 @@ public class Settings { setDynamicCols(Integer.parseInt(value)); break; case "blockSelectMode": setBlockSelectMode(Boolean.parseBoolean(value)); break; + case "blockPaste": setBlockPaste(Boolean.parseBoolean(value)); break; case "autoReconnect": case "auto_reconnect": case "autoConnectAutoReconnect": @@ -632,6 +672,9 @@ public class Settings { case "auto_sys_unlock": setAutoSysUnlock(Boolean.parseBoolean(value)); break; + case "blockPasteMode": + setBlockPaste(Boolean.parseBoolean(value)); + break; case "enablePasteFromExcel": case "pasteFromExcel": case "excelPaste": @@ -649,6 +692,10 @@ public class Settings { case "clipboard": switch (key) { + case "blockPaste": + case "blockPasteMode": + setBlockPaste(Boolean.parseBoolean(value)); + break; case "enablePasteFromExcel": case "pasteFromExcel": case "excelPaste": @@ -761,6 +808,8 @@ public class Settings { // [appearance] w.println("[appearance]"); w.println("javaUiTheme = " + getJavaUiTheme().name()); + w.println("uiFontFamily = " + getUiFontFamily()); + w.println("uiFontSize = " + getUiFontSize()); w.println("fontFamily = " + getFontFamily()); w.println("fontSize = " + getFontSize()); w.println("crosshairRuler = " + getCrosshairRuler()); @@ -783,6 +832,7 @@ public class Settings { w.println("dynamicRows = " + getDynamicRows()); w.println("dynamicCols = " + getDynamicCols()); w.println("blockSelectMode = " + getBlockSelectMode()); + w.println("blockPaste = " + getBlockPaste()); w.println("autoReconnect = " + getAutoConnectAutoReconnect()); w.println("autoConnectReconnectMaxRetries = " + getAutoConnectReconnectMaxRetries()); w.println("inputMask = " + getInputMask()); 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 8e687db..e6e6e4d 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java @@ -45,7 +45,7 @@ public class ConnectDialog extends JDialog { gbc.insets = new Insets(5, 6, 5, 6); gbc.fill = GridBagConstraints.HORIZONTAL; - Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); + Font labelFont = ThemeManager.getUiFont(); // Host gbc.gridx = 0; @@ -82,7 +82,7 @@ public class ConnectDialog extends JDialog { gbc.weightx = 1.0; modelCombo = new JComboBox<>(TerminalModel.values()); modelCombo.setSelectedItem(TerminalModel.IBM_3279_4); - modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); + modelCombo.setFont(ThemeManager.getMonospacedUiFont()); ThemeManager.styleComboBox(modelCombo); mainPanel.add(modelCombo, gbc); @@ -148,7 +148,7 @@ public class ConnectDialog extends JDialog { gbc.weightx = 1.0; graphicsCombo = new JComboBox<>(haus.nightmare.lib3270j.graphics.GraphicsMode.values()); graphicsCombo.setSelectedItem(haus.nightmare.j3270.config.Settings.getGraphicsMode()); - graphicsCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); + graphicsCombo.setFont(ThemeManager.getUiFont()); ThemeManager.styleComboBox(graphicsCombo); mainPanel.add(graphicsCombo, gbc); @@ -207,7 +207,7 @@ public class ConnectDialog extends JDialog { break; } } - codePageCombo.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); + codePageCombo.setFont(ThemeManager.getUiFont()); ThemeManager.styleComboBox(codePageCombo); mainPanel.add(codePageCombo, gbc); @@ -235,7 +235,7 @@ public class ConnectDialog extends JDialog { gbc.gridy = 8; verifyCertCheckBox = new JCheckBox("Verify Server Certificate"); ThemeManager.styleCheckBox(verifyCertCheckBox); - verifyCertCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); + verifyCertCheckBox.setFont(ThemeManager.getUiFont()); verifyCertCheckBox.setSelected(true); verifyCertCheckBox.setEnabled(false); mainPanel.add(verifyCertCheckBox, gbc); @@ -245,7 +245,7 @@ public class ConnectDialog extends JDialog { gbc.gridy = 9; tn3270eCheckBox = new JCheckBox("Enable TN3270E (Extended 3270)"); ThemeManager.styleCheckBox(tn3270eCheckBox); - tn3270eCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); + tn3270eCheckBox.setFont(ThemeManager.getUiFont()); tn3270eCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e()); mainPanel.add(tn3270eCheckBox, gbc); @@ -254,7 +254,7 @@ public class ConnectDialog extends JDialog { gbc.gridy = 10; keepAliveCheckBox = new JCheckBox("Enable Keep-Alive Heartbeat (NOP)"); ThemeManager.styleCheckBox(keepAliveCheckBox); - keepAliveCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); + keepAliveCheckBox.setFont(ThemeManager.getUiFont()); keepAliveCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectKeepAlive()); mainPanel.add(keepAliveCheckBox, gbc); @@ -263,7 +263,7 @@ public class ConnectDialog extends JDialog { gbc.gridy = 11; autoReconnectCheckBox = new JCheckBox("Auto-Reconnect on Disconnect"); ThemeManager.styleCheckBox(autoReconnectCheckBox); - autoReconnectCheckBox.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13)); + autoReconnectCheckBox.setFont(ThemeManager.getUiFont()); autoReconnectCheckBox.setSelected(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect()); mainPanel.add(autoReconnectCheckBox, gbc); @@ -300,7 +300,7 @@ public class ConnectDialog extends JDialog { private JTextField createField(int cols) { JTextField field = new JTextField(cols); - field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); + field.setFont(ThemeManager.getMonospacedUiFont()); ThemeManager.styleTextField(field); return field; } diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/FieldInspectorDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/FieldInspectorDialog.java index 0c6c795..497651a 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/FieldInspectorDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/FieldInspectorDialog.java @@ -61,7 +61,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene JPanel topPanel = new JPanel(new BorderLayout()); topPanel.setOpaque(false); countLabel = new JLabel("0 fields detected on screen"); - countLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); + countLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 0)); topPanel.add(countLabel, BorderLayout.WEST); JButton refreshBtn = new JButton("Refresh"); @@ -84,12 +84,12 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene table = new JTable(tableModel); ThemeManager.styleTable(table); - table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + table.setFont(ThemeManager.getMonospacedUiFont()); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - table.setRowHeight(20); + table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8)); JTableHeader header = table.getTableHeader(); - header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); + header.setFont(ThemeManager.getUiFont(Font.BOLD, 0)); // Column widths int[] widths = {35, 45, 60, 60, 40, 45, 45, 45, 50, 55, 60, 260}; diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/FindDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/FindDialog.java index 7c5f572..ade58d6 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/FindDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/FindDialog.java @@ -51,7 +51,7 @@ public class FindDialog extends JDialog { gbc.insets = new Insets(4, 4, 4, 4); gbc.fill = GridBagConstraints.HORIZONTAL; - Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); + Font labelFont = ThemeManager.getUiFont(); // Search text gbc.gridx = 0; @@ -64,7 +64,7 @@ public class FindDialog extends JDialog { gbc.weightx = 1.0; searchField = new JTextField(20); searchField.setText(lastSearchText); - searchField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); + searchField.setFont(ThemeManager.getMonospacedUiFont()); ThemeManager.styleTextField(searchField); formPanel.add(searchField, gbc); @@ -103,7 +103,7 @@ public class FindDialog extends JDialog { gbc.gridwidth = 2; statusLabel = new JLabel(" "); statusLabel.setForeground(ThemeManager.getOiaFgAlert()); - statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + statusLabel.setFont(labelFont); formPanel.add(statusLabel, gbc); mainPanel.add(formPanel, BorderLayout.CENTER); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/HostDirectoryDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/HostDirectoryDialog.java index 73b0e67..852fb11 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/HostDirectoryDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/HostDirectoryDialog.java @@ -51,7 +51,7 @@ public class HostDirectoryDialog extends JDialog { JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); mainPanel.setBorder(new EmptyBorder(12, 12, 12, 12)); - Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 13); + Font font = ThemeManager.getUiFont(); // Top Query bar JPanel topPanel = new JPanel(new GridBagLayout()); @@ -79,7 +79,7 @@ public class HostDirectoryDialog extends JDialog { gbc.gridx = 3; gbc.weightx = 1.0; queryField = new JTextField(initialQuery != null ? initialQuery : "", 16); - queryField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); + queryField.setFont(ThemeManager.getMonospacedUiFont()); ThemeManager.styleTextField(queryField); topPanel.add(queryField, gbc); @@ -97,12 +97,12 @@ public class HostDirectoryDialog extends JDialog { tableModel = new DefaultTableModel(); table = new JTable(tableModel); ThemeManager.styleTable(table); - table.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + table.setFont(ThemeManager.getMonospacedUiFont()); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - table.setRowHeight(20); + table.setRowHeight(Math.max(22, ThemeManager.getMonospacedUiFont().getSize() + 8)); JTableHeader header = table.getTableHeader(); - header.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); + header.setFont(ThemeManager.getUiFont(Font.BOLD, 0)); table.addMouseListener(new MouseAdapter() { @Override @@ -123,7 +123,7 @@ public class HostDirectoryDialog extends JDialog { statusLabel = new JLabel("Enter a dataset pattern or parse active screen."); statusLabel.setForeground(ThemeManager.getFgMuted()); - statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + statusLabel.setFont(font); bottomPanel.add(statusLabel, BorderLayout.WEST); JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/PrinterSessionDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/PrinterSessionDialog.java index f026540..7cab81d 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/PrinterSessionDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/PrinterSessionDialog.java @@ -72,7 +72,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene gbc.insets = new Insets(3, 4, 3, 4); gbc.fill = GridBagConstraints.HORIZONTAL; - Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); + Font labelFont = ThemeManager.getUiFont(); // Host / Port gbc.gridx = 0; gbc.gridy = 0; @@ -206,7 +206,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene // Spool text area spoolArea = new JTextArea(); - spoolArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + spoolArea.setFont(ThemeManager.getMonospacedUiFont()); spoolArea.setEditable(false); ThemeManager.styleTextArea(spoolArea); @@ -257,7 +257,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene private JTextField createField(String text, int cols) { JTextField tf = new JTextField(text, cols); - tf.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + tf.setFont(ThemeManager.getMonospacedUiFont()); ThemeManager.styleTextField(tf); return tf; } diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/ScriptDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/ScriptDialog.java index a23edab..e08a5e2 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/ScriptDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ScriptDialog.java @@ -36,7 +36,7 @@ public class ScriptDialog extends JDialog { JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); - Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); + Font labelFont = ThemeManager.getUiFont(); // Header JPanel topPanel = new JPanel(new BorderLayout(5, 5)); @@ -49,7 +49,7 @@ public class ScriptDialog extends JDialog { // Script area scriptArea = new JTextArea(); - scriptArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13)); + scriptArea.setFont(ThemeManager.getMonospacedUiFont()); scriptArea.setLineWrap(true); scriptArea.setWrapStyleWord(false); ThemeManager.styleTextArea(scriptArea); @@ -75,7 +75,7 @@ public class ScriptDialog extends JDialog { for (String token : tokens) { JButton btn = new JButton(token); - btn.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11)); + btn.setFont(ThemeManager.getMonospacedUiFont(Font.PLAIN, -2)); ThemeManager.styleButton(btn, ThemeManager.ButtonVariant.DEFAULT); btn.setFocusable(false); btn.setMargin(new Insets(2, 4, 2, 4)); @@ -95,7 +95,7 @@ public class ScriptDialog extends JDialog { statusLabel = new JLabel("Ready"); statusLabel.setForeground(ThemeManager.getFgMuted()); - statusLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + statusLabel.setFont(labelFont); bottomPanel.add(statusLabel, BorderLayout.WEST); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); 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 0cefcb3..7d8d925 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java @@ -8,6 +8,8 @@ import haus.nightmare.j3270.config.Settings; import java.awt.*; import java.awt.event.*; import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.swing.table.DefaultTableModel; @@ -22,25 +24,35 @@ public class SettingsDialog extends JDialog { // Appearance tab private JComboBox uiThemeBox; + private JComboBox uiFontBox; + private JSpinner uiFontSizeSpinner; private JComboBox fontBox; private JSpinner fontSizeSpinner; + private JComboBox cursorStyleBox; + private JCheckBox crosshairRulerCheck; private JComboBox graphicsModeBox; // Behavior tab + private JCheckBox insertOffOnAidCheck; + private JCheckBox numericFieldLockCheck; + private JCheckBox autoSkipCheck; + private JCheckBox inputMaskCheck; + private JTextField inputMaskCharField; + private JCheckBox blockSelectCheck; + private JCheckBox blockPasteCheck; + private JCheckBox enablePasteFromExcelCheck; + private JCheckBox pasteStopAtProtectedCheck; + + // Connection tab private JComboBox startupBehaviorBox; private JPanel autoConnectPanel; private JTextField hostField; private JTextField portField; private JCheckBox autoReconnectCheck; - private JCheckBox inputMaskCheck; - private JTextField inputMaskCharField; - private JCheckBox blockSelectCheck; private JSpinner dynamicRowsSpinner; private JSpinner dynamicColsSpinner; - private JCheckBox enablePasteFromExcelCheck; - private JCheckBox pasteStopAtProtectedCheck; - // Entry Assist & Modes tab + // Entry Assist tab private JCheckBox docModeCheck; private JCheckBox wordWrapCheck; private JSpinner startColSpinner; @@ -48,24 +60,29 @@ public class SettingsDialog extends JDialog { private JCheckBox bellCheck; private JSpinner bellColSpinner; private JTextField tabStopsField; - private JCheckBox insertOffOnAidCheck; - private JCheckBox fourColorOverrideCheck; - private JCheckBox numericFieldLockCheck; - private JCheckBox autoSkipCheck; - // Advanced tab state tracking + // Colors tab state tracking + private JCheckBox fourColorOverrideCheck; private final Color[] tempHostColors = new Color[16]; private final Map tempMonoColors = new HashMap<>(); private final Map tempKeyBindings = new HashMap<>(); private DefaultTableModel keymapModel; + // Bottom action buttons + private JButton btnImport; + private JButton btnExport; + + // Colors tab component tracking + private final JPanel[] hostColorSwatches = new JPanel[16]; + private final Map monoColorSwatches = new HashMap<>(); + public SettingsDialog(J3270App parent) { super(parent, "Settings", true); this.parentApp = parent; AppIcon.applyTo(this); initComponents(); - setSize(600, 520); + setSize(660, 540); setLocationRelativeTo(parent); } @@ -75,11 +92,14 @@ public class SettingsDialog extends JDialog { tabbedPane.addTab("Appearance", createAppearancePanel()); tabbedPane.addTab("Behavior", createBehaviorPanel()); - tabbedPane.addTab("Entry Assist & Modes", createEntryAssistPanel()); - tabbedPane.addTab("Advanced", createAdvancedPanel()); + tabbedPane.addTab("Connection", createConnectionPanel()); + tabbedPane.addTab("Entry Assist", createEntryAssistPanel()); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8)); - JButton btnExport = new JButton("Export Config..."); + btnImport = new JButton("Import Config..."); + ThemeManager.styleButton(btnImport, ThemeManager.ButtonVariant.DEFAULT); + + btnExport = new JButton("Export Config..."); ThemeManager.styleButton(btnExport, ThemeManager.ButtonVariant.DEFAULT); JButton btnApply = new JButton("Apply"); @@ -91,6 +111,10 @@ public class SettingsDialog extends JDialog { JButton btnOk = new JButton("OK"); ThemeManager.styleButton(btnOk, ThemeManager.ButtonVariant.PRIMARY); + btnImport.addActionListener((ActionEvent e) -> { + importConfig(); + }); + btnExport.addActionListener((ActionEvent e) -> { exportConfig(); }); @@ -110,6 +134,7 @@ public class SettingsDialog extends JDialog { dispose(); }); + buttonPanel.add(btnImport); buttonPanel.add(btnExport); buttonPanel.add(Box.createHorizontalStrut(20)); buttonPanel.add(btnApply); @@ -129,12 +154,26 @@ public class SettingsDialog extends JDialog { // ========== Tab Panels ========== private JPanel createAppearancePanel() { + JPanel panel = new JPanel(new BorderLayout()); + JTabbedPane appearanceTabs = new JTabbedPane(); + ThemeManager.styleTabbedPane(appearanceTabs); + + appearanceTabs.addTab("Display & Font", createDisplayFontPanel()); + appearanceTabs.addTab("Colors", createColorsPanel()); + + panel.add(appearanceTabs, BorderLayout.CENTER); + return panel; + } + + private JPanel createDisplayFontPanel() { JPanel panel = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(10, 10, 10, 10); + gbc.insets = new Insets(8, 10, 8, 10); gbc.anchor = GridBagConstraints.WEST; - // UI Theme + String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + + // 0. Java UI Theme gbc.gridx = 0; gbc.gridy = 0; JLabel themeLabel = new JLabel("Java UI Theme:"); @@ -146,14 +185,65 @@ public class SettingsDialog extends JDialog { gbc.fill = GridBagConstraints.HORIZONTAL; panel.add(uiThemeBox, gbc); - // Font family + // 1. UI Font Family gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE; + JLabel uiFontLabel = new JLabel("UI Font Family:"); + panel.add(uiFontLabel, gbc); + + java.util.List uiFontList = new java.util.ArrayList<>(); + boolean hasSans = false; + boolean hasDialog = false; + String currentUiFont = Settings.getUiFontFamily(); + for (String f : fonts) { + if ("SansSerif".equalsIgnoreCase(f)) hasSans = true; + if ("Dialog".equalsIgnoreCase(f)) hasDialog = true; + uiFontList.add(f); + } + if (!hasSans) uiFontList.add(0, "SansSerif"); + if (!hasDialog) uiFontList.add(1, "Dialog"); + boolean hasCurrentUi = false; + for (String f : uiFontList) { + if (f.equalsIgnoreCase(currentUiFont)) { + hasCurrentUi = true; + break; + } + } + if (!hasCurrentUi && currentUiFont != null && !currentUiFont.trim().isEmpty()) { + uiFontList.add(0, currentUiFont); + } + uiFontBox = new JComboBox<>(uiFontList.toArray(new String[0])); + for (int i = 0; i < uiFontBox.getItemCount(); i++) { + if (uiFontBox.getItemAt(i).equalsIgnoreCase(currentUiFont)) { + uiFontBox.setSelectedIndex(i); + break; + } + } + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + panel.add(uiFontBox, gbc); + + // 2. UI Font Size + gbc.gridx = 0; + gbc.gridy = 2; + gbc.fill = GridBagConstraints.NONE; + JLabel uiSizeLabel = new JLabel("UI Font Size:"); + panel.add(uiSizeLabel, gbc); + + SpinnerNumberModel uiSizeModel = new SpinnerNumberModel(Settings.getUiFontSize(), 8, 48, 1); + uiFontSizeSpinner = new JSpinner(uiSizeModel); + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + panel.add(uiFontSizeSpinner, gbc); + + // 3. Terminal Font family + gbc.gridx = 0; + gbc.gridy = 3; + gbc.fill = GridBagConstraints.NONE; JLabel fontLabel = new JLabel("Terminal Font:"); panel.add(fontLabel, gbc); - String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); java.util.List fontList = new java.util.ArrayList<>(); boolean hasMonospaced = false; String currentFont = Settings.getFontFamily(); @@ -187,11 +277,11 @@ public class SettingsDialog extends JDialog { gbc.fill = GridBagConstraints.HORIZONTAL; panel.add(fontBox, gbc); - // Font size + // 4. Terminal Font size gbc.gridx = 0; - gbc.gridy = 2; + gbc.gridy = 4; gbc.fill = GridBagConstraints.NONE; - JLabel sizeLabel = new JLabel("Font Size:"); + JLabel sizeLabel = new JLabel("Terminal Font Size:"); panel.add(sizeLabel, gbc); SpinnerNumberModel sizeModel = new SpinnerNumberModel(Settings.getFontSize(), 8, 72, 1); @@ -200,9 +290,30 @@ public class SettingsDialog extends JDialog { gbc.fill = GridBagConstraints.HORIZONTAL; panel.add(fontSizeSpinner, gbc); - // Graphics Mode + // 5. Cursor Style gbc.gridx = 0; - gbc.gridy = 3; + gbc.gridy = 5; + gbc.fill = GridBagConstraints.NONE; + panel.add(new JLabel("Cursor Style:"), gbc); + + cursorStyleBox = new JComboBox<>(new String[]{"BLOCK", "UNDERLINE"}); + cursorStyleBox.setSelectedItem(Settings.getCursorStyle()); + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + panel.add(cursorStyleBox, gbc); + + // 6. Crosshair Ruler + gbc.gridx = 0; + gbc.gridy = 6; + gbc.gridwidth = 2; + crosshairRulerCheck = new JCheckBox("Enable Crosshair Ruler", Settings.getCrosshairRuler()); + ThemeManager.styleCheckBox(crosshairRulerCheck); + panel.add(crosshairRulerCheck, gbc); + + // 7. Graphics Mode + gbc.gridx = 0; + gbc.gridy = 7; + gbc.gridwidth = 1; gbc.fill = GridBagConstraints.NONE; JLabel graphicsLabel = new JLabel("Graphics Mode:"); panel.add(graphicsLabel, gbc); @@ -214,7 +325,8 @@ public class SettingsDialog extends JDialog { panel.add(graphicsModeBox, gbc); // Fill remaining space - gbc.gridy = 4; + gbc.gridy = 8; + gbc.gridwidth = 2; gbc.weighty = 1.0; panel.add(Box.createGlue(), gbc); @@ -222,21 +334,130 @@ public class SettingsDialog extends JDialog { } private JPanel createBehaviorPanel() { - JPanel panel = new JPanel(new GridBagLayout()); + JPanel panel = new JPanel(new BorderLayout()); + JTabbedPane behaviorTabs = new JTabbedPane(); + ThemeManager.styleTabbedPane(behaviorTabs); + + behaviorTabs.addTab("General", createGeneralBehaviorPanel()); + behaviorTabs.addTab("Keymap", createKeymapPanel()); + + panel.add(behaviorTabs, BorderLayout.CENTER); + return panel; + } + + private JPanel createGeneralBehaviorPanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Group 1: Typing & Operational Modes + JPanel typingGroup = new JPanel(new GridBagLayout()); + typingGroup.setBorder(BorderFactory.createTitledBorder("Typing & Operational Modes")); GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(10, 10, 10, 10); + gbc.insets = new Insets(4, 6, 4, 6); + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.gridx = 0; + gbc.weightx = 1.0; + + insertOffOnAidCheck = new JCheckBox("Reset Insert mode on AID key (Enter, PF, PA, Clear)", Settings.getInsertOffOnAid()); + ThemeManager.styleCheckBox(insertOffOnAidCheck); + gbc.gridy = 0; + typingGroup.add(insertOffOnAidCheck, gbc); + + numericFieldLockCheck = new JCheckBox("Lock keyboard on non-numeric input in numeric fields (-NUMERIC)", Settings.getNumericFieldLock()); + ThemeManager.styleCheckBox(numericFieldLockCheck); + gbc.gridy = 1; + typingGroup.add(numericFieldLockCheck, gbc); + + autoSkipCheck = new JCheckBox("Auto-Skip to next unprotected field when field is filled", Settings.getAutoSkipEnabled()); + ThemeManager.styleCheckBox(autoSkipCheck); + gbc.gridy = 2; + typingGroup.add(autoSkipCheck, gbc); + + // Input Mask feature control + gbc.gridy = 3; + JPanel inputMaskPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); + inputMaskPanel.setOpaque(false); + inputMaskCheck = new JCheckBox("Enable Input Mask (Password Masking)", Settings.getInputMask()); + ThemeManager.styleCheckBox(inputMaskCheck); + inputMaskPanel.add(inputMaskCheck); + + JLabel maskCharLabel = new JLabel("Mask Character:"); + inputMaskCharField = new JTextField(Settings.getInputMaskChar(), 2); + ThemeManager.styleTextField(inputMaskCharField); + inputMaskCharField.setEnabled(inputMaskCheck.isSelected()); + maskCharLabel.setEnabled(inputMaskCheck.isSelected()); + inputMaskCheck.addActionListener(e -> { + boolean sel = inputMaskCheck.isSelected(); + inputMaskCharField.setEnabled(sel); + maskCharLabel.setEnabled(sel); + }); + inputMaskPanel.add(maskCharLabel); + inputMaskPanel.add(inputMaskCharField); + typingGroup.add(inputMaskPanel, gbc); + + panel.add(typingGroup); + panel.add(Box.createVerticalStrut(10)); + + // Group 2: Selection & Clipboard + JPanel selGroup = new JPanel(new GridBagLayout()); + selGroup.setBorder(BorderFactory.createTitledBorder("Selection & Clipboard")); + GridBagConstraints gbcS = new GridBagConstraints(); + gbcS.insets = new Insets(4, 6, 4, 6); + gbcS.anchor = GridBagConstraints.WEST; + gbcS.fill = GridBagConstraints.HORIZONTAL; + gbcS.gridx = 0; + gbcS.weightx = 1.0; + + blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode()); + ThemeManager.styleCheckBox(blockSelectCheck); + gbcS.gridy = 0; + selGroup.add(blockSelectCheck, gbcS); + + blockPasteCheck = new JCheckBox("Block paste", Settings.getBlockPaste()); + ThemeManager.styleCheckBox(blockPasteCheck); + gbcS.gridy = 1; + selGroup.add(blockPasteCheck, gbcS); + + enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel()); + ThemeManager.styleCheckBox(enablePasteFromExcelCheck); + gbcS.gridy = 2; + selGroup.add(enablePasteFromExcelCheck, gbcS); + + pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine()); + ThemeManager.styleCheckBox(pasteStopAtProtectedCheck); + gbcS.gridy = 3; + selGroup.add(pasteStopAtProtectedCheck, gbcS); + + panel.add(selGroup); + panel.add(Box.createVerticalGlue()); + + return panel; + } + + private JPanel createConnectionPanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Group 1: Startup & Auto-Connect + JPanel connGroup = new JPanel(new GridBagLayout()); + connGroup.setBorder(BorderFactory.createTitledBorder("Startup & Auto-Connect")); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(6, 6, 6, 6); gbc.anchor = GridBagConstraints.WEST; gbc.gridx = 0; gbc.gridy = 0; JLabel actionLabel = new JLabel("Startup Action:"); - panel.add(actionLabel, gbc); + connGroup.add(actionLabel, gbc); startupBehaviorBox = new JComboBox<>(Settings.StartupBehavior.values()); startupBehaviorBox.setSelectedItem(Settings.getStartupBehavior()); gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; - panel.add(startupBehaviorBox, gbc); + connGroup.add(startupBehaviorBox, gbc); // Auto-connect panel autoConnectPanel = new JPanel(new GridBagLayout()); @@ -264,7 +485,7 @@ public class SettingsDialog extends JDialog { gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 2; - panel.add(autoConnectPanel, gbc); + connGroup.add(autoConnectPanel, gbc); // Toggle visibility startupBehaviorBox.addActionListener(e -> { @@ -278,42 +499,21 @@ public class SettingsDialog extends JDialog { gbc.gridwidth = 2; autoReconnectCheck = new JCheckBox("Auto-Reconnect on Disconnect", Settings.getAutoConnectAutoReconnect()); ThemeManager.styleCheckBox(autoReconnectCheck); - panel.add(autoReconnectCheck, gbc); + connGroup.add(autoReconnectCheck, gbc); - // Input Mask feature control - gbc.gridy = 3; - gbc.gridwidth = 2; - JPanel inputMaskPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); - inputMaskPanel.setOpaque(false); - inputMaskCheck = new JCheckBox("Enable Input Mask (Password Masking)", Settings.getInputMask()); - ThemeManager.styleCheckBox(inputMaskCheck); - inputMaskPanel.add(inputMaskCheck); + panel.add(connGroup); + panel.add(Box.createVerticalStrut(10)); - JLabel maskCharLabel = new JLabel("Mask Character:"); - inputMaskCharField = new JTextField(Settings.getInputMaskChar(), 2); - ThemeManager.styleTextField(inputMaskCharField); - inputMaskCharField.setEnabled(inputMaskCheck.isSelected()); - maskCharLabel.setEnabled(inputMaskCheck.isSelected()); - inputMaskCheck.addActionListener(e -> { - boolean sel = inputMaskCheck.isSelected(); - inputMaskCharField.setEnabled(sel); - maskCharLabel.setEnabled(sel); - }); - inputMaskPanel.add(maskCharLabel); - inputMaskPanel.add(inputMaskCharField); - panel.add(inputMaskPanel, gbc); + // Group 2: Screen Dimensions + JPanel screenGroup = new JPanel(new GridBagLayout()); + screenGroup.setBorder(BorderFactory.createTitledBorder("Screen Geometry")); + GridBagConstraints gbcDim = new GridBagConstraints(); + gbcDim.insets = new Insets(6, 6, 6, 6); + gbcDim.anchor = GridBagConstraints.WEST; - // Block select mode checkbox - gbc.gridy = 4; - gbc.gridwidth = 2; - blockSelectCheck = new JCheckBox("Block selection mode (rectangular select)", Settings.getBlockSelectMode()); - panel.add(blockSelectCheck, gbc); - - // Default Dynamic Screen Size - gbc.gridy = 5; - gbc.gridwidth = 1; - gbc.gridx = 0; - panel.add(new JLabel("Default Dynamic Screen:"), gbc); + gbcDim.gridx = 0; + gbcDim.gridy = 0; + screenGroup.add(new JLabel("Default Dynamic Screen:"), gbcDim); JPanel dynDimPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); dynDimPanel.setOpaque(false); @@ -326,23 +526,11 @@ public class SettingsDialog extends JDialog { ThemeManager.styleSpinner(dynamicColsSpinner); dynDimPanel.add(dynamicColsSpinner); - gbc.gridx = 1; - panel.add(dynDimPanel, gbc); + gbcDim.gridx = 1; + screenGroup.add(dynDimPanel, gbcDim); - // Clipboard & Tabular Paste options - gbc.gridx = 0; - gbc.gridy = 6; - gbc.gridwidth = 2; - enablePasteFromExcelCheck = new JCheckBox("Enable Excel / Tabular Paste (advance with tabs & newlines)", Settings.getEnablePasteFromExcel()); - panel.add(enablePasteFromExcelCheck, gbc); - - gbc.gridy = 7; - pasteStopAtProtectedCheck = new JCheckBox("Stop Paste at Protected Boundary", Settings.getPasteStopAtProtectedLine()); - panel.add(pasteStopAtProtectedCheck, gbc); - - gbc.gridy = 8; - gbc.weighty = 1.0; - panel.add(Box.createGlue(), gbc); + panel.add(screenGroup); + panel.add(Box.createVerticalGlue()); return panel; } @@ -352,7 +540,7 @@ public class SettingsDialog extends JDialog { panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - // Group 1: Entry Assist / Document Mode + // Entry Assist / Document Mode JPanel eaGroup = new JPanel(new GridBagLayout()); eaGroup.setBorder(BorderFactory.createTitledBorder("Entry Assist (Document Mode)")); GridBagConstraints gbc = new GridBagConstraints(); @@ -360,12 +548,14 @@ public class SettingsDialog extends JDialog { gbc.anchor = GridBagConstraints.WEST; docModeCheck = new JCheckBox("Enable Document Mode (DOC)", Settings.getEntryAssistDocMode()); + ThemeManager.styleCheckBox(docModeCheck); gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 2; eaGroup.add(docModeCheck, gbc); wordWrapCheck = new JCheckBox("Enable Word Wrap (V)", Settings.getEntryAssistWordWrap()); + ThemeManager.styleCheckBox(wordWrapCheck); gbc.gridy = 1; eaGroup.add(wordWrapCheck, gbc); @@ -393,6 +583,7 @@ public class SettingsDialog extends JDialog { gbc.gridx = 0; gbc.gridy = 3; bellCheck = new JCheckBox("Audible EOL Bell at Col:", Settings.getEntryAssistBell()); + ThemeManager.styleCheckBox(bellCheck); eaGroup.add(bellCheck, gbc); bellColSpinner = new JSpinner(new SpinnerNumberModel(Settings.getEntryAssistBellCol(), 1, 80, 1)); @@ -406,57 +597,18 @@ public class SettingsDialog extends JDialog { eaGroup.add(new JLabel("Tab Stops:"), gbc); tabStopsField = new JTextField(Settings.getEntryAssistTabStops(), 20); + ThemeManager.styleTextField(tabStopsField); tabStopsField.setToolTipText("Comma-separated column numbers (1-80, e.g. 1,9,17,25,33,41,49,57,65,73)"); gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL; eaGroup.add(tabStopsField, gbc); panel.add(eaGroup); - panel.add(Box.createVerticalStrut(10)); - - // Group 2: Terminal Operational Modes - JPanel modesGroup = new JPanel(new GridBagLayout()); - modesGroup.setBorder(BorderFactory.createTitledBorder("Terminal Operational Modes")); - GridBagConstraints gbcM = new GridBagConstraints(); - gbcM.insets = new Insets(4, 6, 4, 6); - gbcM.anchor = GridBagConstraints.WEST; - gbcM.fill = GridBagConstraints.HORIZONTAL; - gbcM.gridx = 0; - gbcM.weightx = 1.0; - - insertOffOnAidCheck = new JCheckBox("Reset Insert mode on AID key (Enter, PF, PA, Clear)", Settings.getInsertOffOnAid()); - gbcM.gridy = 0; - modesGroup.add(insertOffOnAidCheck, gbcM); - - fourColorOverrideCheck = new JCheckBox("Base 4-Color Override mode (3279 green/white/red/turquoise)", Settings.getFourColorOverride()); - gbcM.gridy = 1; - modesGroup.add(fourColorOverrideCheck, gbcM); - - numericFieldLockCheck = new JCheckBox("Lock keyboard on non-numeric input in numeric fields (-NUMERIC)", Settings.getNumericFieldLock()); - gbcM.gridy = 2; - modesGroup.add(numericFieldLockCheck, gbcM); - - autoSkipCheck = new JCheckBox("Auto-Skip to next unprotected field when field is filled", Settings.getAutoSkipEnabled()); - gbcM.gridy = 3; - modesGroup.add(autoSkipCheck, gbcM); - - panel.add(modesGroup); panel.add(Box.createVerticalGlue()); return panel; } - private JPanel createAdvancedPanel() { - JPanel panel = new JPanel(new BorderLayout()); - - JTabbedPane advancedTabs = new JTabbedPane(); - advancedTabs.addTab("Colors", createColorsPanel()); - advancedTabs.addTab("Keymap", createKeymapPanel()); - - panel.add(advancedTabs, BorderLayout.CENTER); - return panel; - } - private JPanel createColorsPanel() { JPanel main = new JPanel(new BorderLayout()); @@ -472,6 +624,7 @@ public class SettingsDialog extends JDialog { cb.setBackground(init); cb.setPreferredSize(new Dimension(30, 30)); cb.setBorder(new LineBorder(Color.DARK_GRAY)); + hostColorSwatches[i] = cb; final int index = i; cb.addMouseListener(new MouseAdapter() { @@ -507,6 +660,7 @@ public class SettingsDialog extends JDialog { cb.setBackground(init); cb.setPreferredSize(new Dimension(40, 40)); cb.setBorder(new LineBorder(Color.DARK_GRAY)); + monoColorSwatches.put(mk, cb); cb.addMouseListener(new MouseAdapter() { @Override @@ -522,36 +676,75 @@ public class SettingsDialog extends JDialog { monoPanel.add(p); } - JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JPanel bottomPanel = new JPanel(new BorderLayout(8, 0)); + bottomPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6)); + + fourColorOverrideCheck = new JCheckBox("Base 4-Color Override mode (3279 green/white/red/turquoise)", Settings.getFourColorOverride()); + ThemeManager.styleCheckBox(fourColorOverrideCheck); + bottomPanel.add(fourColorOverrideCheck, BorderLayout.WEST); + JButton btnResetColors = new JButton("Reset to Defaults"); ThemeManager.styleButton(btnResetColors, ThemeManager.ButtonVariant.DEFAULT); btnResetColors.addActionListener(e -> { for (int i=0; i<16; i++) { tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i]; + if (hostColorSwatches[i] != null) { + hostColorSwatches[i].setBackground(tempHostColors[i]); + } } for (int i=0; i entry : tempKeyBindings.entrySet()) { haus.nightmare.j3270.config.Settings.setKeyBinding(entry.getKey(), entry.getValue()); } - parentApp.getTerminalPanel().reloadSettings(); - if (parentApp != null && (parentApp.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) { - parentApp.getTerminalPanel().guardedPack(); + if (parentApp != null) { + ThemeManager.applyThemeToWindow(parentApp); + if (parentApp.getTerminalPanel() != null) { + parentApp.getTerminalPanel().reloadSettings(); + if ((parentApp.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) { + parentApp.getTerminalPanel().guardedPack(); + } + } + if (parentApp.getStatusBar() != null) { + parentApp.getStatusBar().applyTheme(Settings.getJavaUiTheme()); + } } ThemeManager.applyThemeToWindow(this); @@ -866,6 +1063,207 @@ public class SettingsDialog extends JDialog { } } + // ========== Refresh from Settings ========== + + public void refreshFromSettings() { + // Appearance + if (uiThemeBox != null) { + uiThemeBox.setSelectedItem(Settings.getJavaUiTheme()); + } + if (uiFontBox != null) { + String currentUiFont = Settings.getUiFontFamily(); + for (int i = 0; i < uiFontBox.getItemCount(); i++) { + if (uiFontBox.getItemAt(i).equalsIgnoreCase(currentUiFont)) { + uiFontBox.setSelectedIndex(i); + break; + } + } + } + if (uiFontSizeSpinner != null) { + uiFontSizeSpinner.setValue(Settings.getUiFontSize()); + } + if (fontBox != null) { + String currentFont = Settings.getFontFamily(); + for (int i = 0; i < fontBox.getItemCount(); i++) { + if (fontBox.getItemAt(i).equalsIgnoreCase(currentFont)) { + fontBox.setSelectedIndex(i); + break; + } + } + } + if (fontSizeSpinner != null) { + fontSizeSpinner.setValue(Settings.getFontSize()); + } + if (graphicsModeBox != null) { + graphicsModeBox.setSelectedItem(Settings.getGraphicsMode()); + } + if (cursorStyleBox != null) { + cursorStyleBox.setSelectedItem(Settings.getCursorStyle()); + } + if (crosshairRulerCheck != null) { + crosshairRulerCheck.setSelected(Settings.getCrosshairRuler()); + } + + // Behavior + if (insertOffOnAidCheck != null) { + insertOffOnAidCheck.setSelected(Settings.getInsertOffOnAid()); + } + if (numericFieldLockCheck != null) { + numericFieldLockCheck.setSelected(Settings.getNumericFieldLock()); + } + if (autoSkipCheck != null) { + autoSkipCheck.setSelected(Settings.getAutoSkipEnabled()); + } + if (inputMaskCheck != null) { + inputMaskCheck.setSelected(Settings.getInputMask()); + } + if (inputMaskCharField != null) { + inputMaskCharField.setText(Settings.getInputMaskChar()); + inputMaskCharField.setEnabled(inputMaskCheck != null && inputMaskCheck.isSelected()); + } + if (blockSelectCheck != null) { + blockSelectCheck.setSelected(Settings.getBlockSelectMode()); + } + if (blockPasteCheck != null) { + blockPasteCheck.setSelected(Settings.getBlockPaste()); + } + if (enablePasteFromExcelCheck != null) { + enablePasteFromExcelCheck.setSelected(Settings.getEnablePasteFromExcel()); + } + if (pasteStopAtProtectedCheck != null) { + pasteStopAtProtectedCheck.setSelected(Settings.getPasteStopAtProtectedLine()); + } + + // Connection + if (startupBehaviorBox != null) { + startupBehaviorBox.setSelectedItem(Settings.getStartupBehavior()); + if (autoConnectPanel != null) { + autoConnectPanel.setVisible(startupBehaviorBox.getSelectedItem() == Settings.StartupBehavior.AUTO_CONNECT); + } + } + if (hostField != null) { + hostField.setText(Settings.getAutoConnectHost()); + } + if (portField != null) { + portField.setText(String.valueOf(Settings.getAutoConnectPort())); + } + if (autoReconnectCheck != null) { + autoReconnectCheck.setSelected(Settings.getAutoConnectAutoReconnect()); + } + if (dynamicRowsSpinner != null) { + dynamicRowsSpinner.setValue(Settings.getDynamicRows()); + } + if (dynamicColsSpinner != null) { + dynamicColsSpinner.setValue(Settings.getDynamicCols()); + } + + // Entry Assist + if (docModeCheck != null) { + docModeCheck.setSelected(Settings.getEntryAssistDocMode()); + } + if (wordWrapCheck != null) { + wordWrapCheck.setSelected(Settings.getEntryAssistWordWrap()); + } + if (startColSpinner != null) { + startColSpinner.setValue(Settings.getEntryAssistStartCol()); + } + if (endColSpinner != null) { + endColSpinner.setValue(Settings.getEntryAssistEndCol()); + } + if (bellCheck != null) { + bellCheck.setSelected(Settings.getEntryAssistBell()); + } + if (bellColSpinner != null) { + bellColSpinner.setValue(Settings.getEntryAssistBellCol()); + } + if (tabStopsField != null) { + tabStopsField.setText(Settings.getEntryAssistTabStops()); + } + + // Colors + if (fourColorOverrideCheck != null) { + fourColorOverrideCheck.setSelected(Settings.getFourColorOverride()); + } + for (int i = 0; i < 16; i++) { + Color c = haus.nightmare.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]); + tempHostColors[i] = c; + if (hostColorSwatches[i] != null) { + hostColorSwatches[i].setBackground(c); + } + } + String[] monoKeys = {"NORMAL", "INTENSIFY", "PROTECTED", "PROTECTED_HIGH", "BACKGROUND"}; + Color[] monoDefs = {TerminalPanel.DEFAULT_MONO_NORMAL, TerminalPanel.DEFAULT_MONO_INTENSIFY, + TerminalPanel.DEFAULT_MONO_PROTECTED, TerminalPanel.DEFAULT_MONO_PROTECTED_HIGH, TerminalPanel.DEFAULT_BG_COLOR}; + for (int i = 0; i < monoKeys.length; i++) { + String mk = monoKeys[i]; + Color c = haus.nightmare.j3270.config.Settings.getMonoColorOverride(mk, monoDefs[i]); + tempMonoColors.put(mk, c); + JPanel swatch = monoColorSwatches.get(mk); + if (swatch != null) { + swatch.setBackground(c); + } + } + + // Keymap + populateKeymapModel(); + + // Apply theme to dialog and parent window + ThemeManager.setTheme(Settings.getJavaUiTheme()); + ThemeManager.applyThemeToWindow(this); + if (parentApp != null) { + ThemeManager.applyThemeToWindow(parentApp); + if (parentApp.getTerminalPanel() != null) { + parentApp.getTerminalPanel().reloadSettings(); + if ((parentApp.getExtendedState() & Frame.MAXIMIZED_BOTH) == 0) { + parentApp.getTerminalPanel().guardedPack(); + } + } + if (parentApp.getClient() != null && parentApp.getClient().getConfig() != null) { + parentApp.getClient().getConfig().setAutoReconnect(Settings.getAutoConnectAutoReconnect()); + } + } + } + + // ========== Import Config ========== + + void importConfigFile(File file) throws IOException { + if (file == null || !file.exists()) { + throw new FileNotFoundException("Config file not found: " + (file != null ? file.getAbsolutePath() : "null")); + } + Settings.loadFromIniFile(file.getAbsolutePath()); + refreshFromSettings(); + } + + private void importConfig() { + JFileChooser fc = new JFileChooser(); + fc.setDialogTitle("Import Configuration"); + fc.setSelectedFile(new java.io.File("j3270.ini")); + fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("INI Files (*.ini)", "ini")); + + int result = fc.showOpenDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + java.io.File file = fc.getSelectedFile(); + try { + importConfigFile(file); + JOptionPane.showMessageDialog(this, + "Configuration imported successfully from:\n" + file.getAbsolutePath(), + "Import Successful", JOptionPane.INFORMATION_MESSAGE); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, + "Import failed: " + ex.getMessage(), + "Import Error", JOptionPane.ERROR_MESSAGE); + } + } + } + + public JButton getBtnImport() { + return btnImport; + } + + public JButton getBtnExport() { + return btnExport; + } + // ========== Export Config ========== private void exportConfig() { 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 f9c20d6..eb941d9 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/StatusBar.java @@ -34,9 +34,9 @@ public class StatusBar extends JPanel { setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setBackground(ThemeManager.getStatusBarBg()); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder())); - setPreferredSize(new Dimension(800, 22)); - Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12); + Font oiaFont = ThemeManager.getMonospacedUiFont(); + updateBarHeight(oiaFont); connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim()); tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); @@ -143,9 +143,36 @@ public class StatusBar extends JPanel { } setBackground(ThemeManager.getStatusBarBg(theme)); setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, ThemeManager.getStatusBarBorder(theme))); + Font oiaFont = ThemeManager.getMonospacedUiFont(); + updateBarHeight(oiaFont); + applyFontRecursively(this, oiaFont); + revalidate(); + repaint(); updateStatus(); } + private void updateBarHeight(Font font) { + int fontHeight = 16; + try { + FontMetrics fm = getFontMetrics(font); + if (fm != null) fontHeight = fm.getHeight(); + } catch (Exception ignored) { + fontHeight = font.getSize() + 4; + } + int barHeight = Math.max(22, fontHeight + 6); + setPreferredSize(new Dimension(800, barHeight)); + } + + private void applyFontRecursively(Component comp, Font font) { + if (comp instanceof JLabel) { + comp.setFont(font); + } else if (comp instanceof Container) { + for (Component child : ((Container) comp).getComponents()) { + applyFontRecursively(child, font); + } + } + } + public void updateStatus() { if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(this::updateStatus); 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 052d29a..d4bc17d 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -498,11 +498,16 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { int curCol = cols > 0 ? curPos % cols : 0; boolean excelPaste = haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel(); boolean stopAtProtected = haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine(); + boolean blockPaste = haus.nightmare.j3270.config.Settings.getBlockPaste(); - if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || text.contains("\t") || text.contains("\n") || text.contains("\r"))) { - client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected); + if (client.getInputProcessor() != null && (excelPaste || stopAtProtected || blockPaste || text.contains("\t") || text.contains("\n") || text.contains("\r"))) { + client.getInputProcessor().pasteText(text, excelPaste, stopAtProtected, blockPaste); } else if (client.getPS() != null) { - client.getPS().pasteString(text, curRow, curCol); + if (blockPaste) { + client.getPS().pasteRectangular(text, curRow, curCol); + } else { + client.getPS().pasteString(text, curRow, curCol); + } } else { for (char ch : text.toCharArray()) { if (ch == '\n' || ch == '\r') { @@ -577,6 +582,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { }); popup.add(blockModeItem); + JCheckBoxMenuItem blockPasteItem = new JCheckBoxMenuItem("Block Paste", haus.nightmare.j3270.config.Settings.getBlockPaste()); + ThemeManager.styleMenuItem(blockPasteItem); + blockPasteItem.addActionListener(ev -> { + haus.nightmare.j3270.config.Settings.setBlockPaste(blockPasteItem.isSelected()); + applyModeSettings(); + }); + popup.add(blockPasteItem); + popup.show(this, e.getX(), e.getY()); } @@ -1181,10 +1194,12 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { ip.setInsertOffOnAid(haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); ip.setNumericFieldLock(haus.nightmare.j3270.config.Settings.getNumericFieldLock()); ip.setAutoSkipEnabled(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); + ip.setBlockPaste(haus.nightmare.j3270.config.Settings.getBlockPaste()); } if (client.getPS() != null) { client.getPS().setEnablePasteFromExcel(haus.nightmare.j3270.config.Settings.getEnablePasteFromExcel()); client.getPS().setPasteStopAtProtectedLine(haus.nightmare.j3270.config.Settings.getPasteStopAtProtectedLine()); + client.getPS().setBlockPaste(haus.nightmare.j3270.config.Settings.getBlockPaste()); } } if (statusBar != null) { diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/ThemeManager.java b/j3270/src/main/java/haus/nightmare/j3270/ui/ThemeManager.java index ca46496..5077d7e 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/ThemeManager.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ThemeManager.java @@ -20,6 +20,7 @@ import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import haus.nightmare.j3270.config.Settings; /** * Centralized theme manager for j3270 Java desktop UI. @@ -33,6 +34,25 @@ public final class ThemeManager { private ThemeManager() {} + public static Font getUiFont() { + return getUiFont(Font.PLAIN, 0); + } + + public static Font getUiFont(int style, int sizeDelta) { + String family = Settings.getUiFontFamily(); + int size = Math.max(8, Settings.getUiFontSize() + sizeDelta); + return new Font(family, style, size); + } + + public static Font getMonospacedUiFont() { + return getMonospacedUiFont(Font.PLAIN, 0); + } + + public static Font getMonospacedUiFont(int style, int sizeDelta) { + int size = Math.max(8, Settings.getUiFontSize() + sizeDelta); + return new Font(Font.MONOSPACED, style, size); + } + public static UITheme getTheme() { return currentTheme; } @@ -433,6 +453,47 @@ public final class ThemeManager { UIManager.put("TitledBorder.titleColor", fgMain); UIManager.put("OptionPane.background", bgPanel); UIManager.put("OptionPane.messageForeground", fgMain); + + // UI Font Defaults + Font uiFont = getUiFont(); + Font uiFontBold = getUiFont(Font.BOLD, 0); + Font monoUiFont = getMonospacedUiFont(); + + UIManager.put("Button.font", uiFont); + UIManager.put("ToggleButton.font", uiFont); + UIManager.put("RadioButton.font", uiFont); + UIManager.put("CheckBox.font", uiFont); + UIManager.put("ColorChooser.font", uiFont); + UIManager.put("ComboBox.font", uiFont); + UIManager.put("Label.font", uiFont); + UIManager.put("List.font", uiFont); + UIManager.put("MenuBar.font", uiFont); + UIManager.put("MenuItem.font", uiFont); + UIManager.put("RadioButtonMenuItem.font", uiFont); + UIManager.put("CheckBoxMenuItem.font", uiFont); + UIManager.put("Menu.font", uiFont); + UIManager.put("PopupMenu.font", uiFont); + UIManager.put("OptionPane.font", uiFont); + UIManager.put("OptionPane.messageFont", uiFont); + UIManager.put("OptionPane.buttonFont", uiFont); + UIManager.put("Panel.font", uiFont); + UIManager.put("ProgressBar.font", uiFont); + UIManager.put("ScrollPane.font", uiFont); + UIManager.put("Viewport.font", uiFont); + UIManager.put("TabbedPane.font", uiFont); + UIManager.put("Table.font", uiFont); + UIManager.put("TableHeader.font", uiFontBold); + UIManager.put("TextField.font", uiFont); + UIManager.put("PasswordField.font", uiFont); + UIManager.put("TextArea.font", monoUiFont); + UIManager.put("TextPane.font", uiFont); + UIManager.put("EditorPane.font", uiFont); + UIManager.put("TitledBorder.font", uiFontBold); + UIManager.put("ToolBar.font", uiFont); + UIManager.put("ToolTip.font", uiFont); + UIManager.put("Tree.font", uiFont); + UIManager.put("Spinner.font", uiFont); + UIManager.put("FormattedTextField.font", uiFont); } // ========================================================================= @@ -446,6 +507,7 @@ public final class ThemeManager { public static JButton styleButton(JButton button, ButtonVariant variant) { if (button == null) return null; button.setUI(new StyledButtonUI(variant)); + button.setFont(getUiFont()); button.setFocusPainted(false); button.setOpaque(false); button.setContentAreaFilled(false); @@ -465,6 +527,7 @@ public final class ThemeManager { public static JTextField styleTextField(JTextField field) { if (field == null) return null; + field.setFont(getUiFont()); field.setBackground(getBgComponent()); field.setForeground(getFgMain()); field.setCaretColor(getFgMain()); @@ -478,6 +541,7 @@ public final class ThemeManager { public static JTextArea styleTextArea(JTextArea area) { if (area == null) return null; + area.setFont(getMonospacedUiFont()); area.setBackground(getCodeAreaBg()); area.setForeground(getCodeAreaFg()); area.setCaretColor(getFgMain()); @@ -488,6 +552,7 @@ public final class ThemeManager { public static JComboBox styleComboBox(JComboBox box) { if (box == null) return null; + box.setFont(getUiFont()); box.setBackground(getBgComponent()); box.setForeground(getFgMain()); box.setRenderer(new DefaultListCellRenderer() { @@ -510,12 +575,14 @@ public final class ThemeManager { public static JTable styleTable(JTable table) { if (table == null) return null; + table.setFont(getUiFont()); table.setBackground(getBgComponent()); table.setForeground(getFgMain()); table.setSelectionBackground(getSelectionBg()); table.setSelectionForeground(getSelectionFg()); table.setGridColor(getTableGrid()); - table.setRowHeight(24); + int rowHeight = Math.max(24, getUiFont().getSize() + 10); + table.setRowHeight(rowHeight); table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() { @Override @@ -537,14 +604,14 @@ public final class ThemeManager { if (header != null) { header.setBackground(getTableHeaderBg()); header.setForeground(getTableHeaderFg()); - header.setFont(header.getFont().deriveFont(Font.BOLD)); + header.setFont(getUiFont(Font.BOLD, 0)); header.setDefaultRenderer(new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent(JTable tbl, Object val, boolean isSel, boolean hasFocus, int row, int col) { super.getTableCellRendererComponent(tbl, val, isSel, hasFocus, row, col); setBackground(getTableHeaderBg()); setForeground(getTableHeaderFg()); - setFont(getFont().deriveFont(Font.BOLD)); + setFont(getUiFont(Font.BOLD, 0)); setBorder(BorderFactory.createCompoundBorder( BorderFactory.createMatteBorder(0, 0, 1, 1, getBorderColor()), new EmptyBorder(4, 6, 4, 6))); @@ -558,6 +625,7 @@ public final class ThemeManager { public static JTabbedPane styleTabbedPane(JTabbedPane tp) { if (tp == null) return null; tp.setUI(new StyledTabbedPaneUI()); + tp.setFont(getUiFont()); tp.setBackground(getBgMain()); tp.setForeground(getFgMain()); return tp; @@ -566,6 +634,7 @@ public final class ThemeManager { public static JMenuBar styleMenuBar(JMenuBar bar) { if (bar == null) return null; bar.setUI(new StyledMenuBarUI()); + bar.setFont(getUiFont()); bar.setBackground(getMenuBarBg()); bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, getBorder())); return bar; @@ -574,6 +643,7 @@ public final class ThemeManager { public static JMenu styleMenu(JMenu menu) { if (menu == null) return null; menu.setUI(new StyledMenuUI()); + menu.setFont(getUiFont()); menu.setForeground(getMenuBarFg()); menu.setBackground(getMenuBarBg()); menu.setOpaque(false); @@ -583,6 +653,7 @@ public final class ThemeManager { public static JMenuItem styleMenuItem(JMenuItem item) { if (item == null) return null; item.setUI(new StyledMenuItemUI()); + item.setFont(getUiFont()); item.setBackground(getMenuPopupBg()); item.setForeground(getMenuItemFg()); return item; @@ -591,6 +662,7 @@ public final class ThemeManager { public static JPopupMenu stylePopupMenu(JPopupMenu popup) { if (popup == null) return null; popup.setUI(new StyledPopupMenuUI()); + popup.setFont(getUiFont()); popup.setBackground(getMenuPopupBg()); popup.setBorder(BorderFactory.createCompoundBorder( new LineBorder(getBorder(), 1), @@ -610,12 +682,14 @@ public final class ThemeManager { public static JSpinner styleSpinner(JSpinner sp) { if (sp == null) return null; + sp.setFont(getUiFont()); sp.setBackground(getBgComponent()); sp.setForeground(getFgMain()); sp.setBorder(new LineBorder(getBorder(), 1)); JComponent editor = sp.getEditor(); if (editor instanceof JSpinner.DefaultEditor) { JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField(); + tf.setFont(getUiFont()); tf.setBackground(getBgComponent()); tf.setForeground(getFgMain()); tf.setCaretColor(getFgMain()); @@ -626,6 +700,7 @@ public final class ThemeManager { public static JCheckBox styleCheckBox(JCheckBox cb) { if (cb == null) return null; + cb.setFont(getUiFont()); cb.setOpaque(false); cb.setForeground(getFgMain()); cb.setFocusPainted(false); @@ -634,6 +709,7 @@ public final class ThemeManager { public static JRadioButton styleRadioButton(JRadioButton rb) { if (rb == null) return null; + rb.setFont(getUiFont()); rb.setOpaque(false); rb.setForeground(getFgMain()); rb.setFocusPainted(false); @@ -646,7 +722,7 @@ public final class ThemeManager { title, TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, - new Font(Font.SANS_SERIF, Font.BOLD, 12), + getUiFont(Font.BOLD, 0), getFgMain()); } @@ -732,6 +808,7 @@ public final class ThemeManager { if (comp instanceof JButton) { JButton btn = (JButton) comp; + btn.setFont(getUiFont()); if (!(btn.getUI() instanceof StyledButtonUI)) { styleButton(btn, ButtonVariant.DEFAULT); } @@ -770,6 +847,21 @@ public final class ThemeManager { if (comp instanceof JLabel) { comp.setForeground(getFgMain(theme)); + JLabel label = (JLabel) comp; + Font cur = label.getFont(); + int style = (cur != null && cur.isBold()) ? Font.BOLD : Font.PLAIN; + Object deltaProp = label.getClientProperty("uiFontSizeDelta"); + int delta = 0; + if (deltaProp instanceof Integer) { + delta = (Integer) deltaProp; + } else { + int baseSize = Settings.getUiFontSize(); + if (cur != null && cur.getSize() > baseSize) { + delta = cur.getSize() - baseSize; + } + label.putClientProperty("uiFontSizeDelta", delta); + } + label.setFont(getUiFont(style, delta)); return; } @@ -783,6 +875,7 @@ public final class ThemeManager { if (b instanceof TitledBorder) { TitledBorder tb = (TitledBorder) b; tb.setTitleColor(getFgMain(theme)); + tb.setTitleFont(getUiFont(Font.BOLD, 0)); tb.setBorder(new LineBorder(getBorder(theme), 1)); } } else if (comp instanceof Container) { diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/UntrustedCertificateDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ui/UntrustedCertificateDialog.java index 15a17b8..7176ad0 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/UntrustedCertificateDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/UntrustedCertificateDialog.java @@ -32,18 +32,20 @@ public class UntrustedCertificateDialog extends JDialog { JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); headerPanel.setOpaque(false); JLabel iconLabel = new JLabel("⚠️"); - iconLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 28)); + iconLabel.putClientProperty("uiFontSizeDelta", 15); + iconLabel.setFont(ThemeManager.getUiFont(Font.PLAIN, 15)); headerPanel.add(iconLabel); JPanel titleBox = new JPanel(new GridLayout(2, 1, 0, 2)); titleBox.setOpaque(false); JLabel titleLabel = new JLabel("Untrusted SSL Certificate"); - titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + titleLabel.putClientProperty("uiFontSizeDelta", 3); + titleLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3)); titleLabel.setForeground(ThemeManager.getOiaFgWarn()); titleBox.add(titleLabel); JLabel subtitleLabel = new JLabel("The server certificate for " + host + ":" + port + " could not be verified."); - subtitleLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + subtitleLabel.setFont(ThemeManager.getUiFont()); subtitleLabel.setForeground(ThemeManager.getFgMuted()); titleBox.add(subtitleLabel); headerPanel.add(titleBox); @@ -70,7 +72,7 @@ public class UntrustedCertificateDialog extends JDialog { JTextArea detailsArea = new JTextArea(sb.toString()); detailsArea.setEditable(false); - detailsArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + detailsArea.setFont(ThemeManager.getMonospacedUiFont()); ThemeManager.styleTextArea(detailsArea); detailsArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); @@ -92,7 +94,7 @@ public class UntrustedCertificateDialog extends JDialog { JButton trustBtn = new JButton("Connect Anyway"); ThemeManager.styleButton(trustBtn, ThemeManager.ButtonVariant.DANGER); - trustBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 13)); + trustBtn.setFont(ThemeManager.getUiFont(Font.BOLD, 0)); trustBtn.addActionListener(e -> { accepted = true; dispose(); diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/BehaviorSettingsTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/BehaviorSettingsTest.java index 025d23e..247eb4b 100644 --- a/j3270/src/test/java/haus/nightmare/j3270/ui/BehaviorSettingsTest.java +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/BehaviorSettingsTest.java @@ -20,6 +20,7 @@ public class BehaviorSettingsTest { private int origReconnectMaxRetries; private boolean origInputMask; private String origInputMaskChar; + private boolean origBlockPaste; @BeforeEach public void setUp() { @@ -27,6 +28,7 @@ public class BehaviorSettingsTest { origReconnectMaxRetries = Settings.getAutoConnectReconnectMaxRetries(); origInputMask = Settings.getInputMask(); origInputMaskChar = Settings.getInputMaskChar(); + origBlockPaste = Settings.getBlockPaste(); } @AfterEach @@ -35,6 +37,7 @@ public class BehaviorSettingsTest { Settings.setAutoConnectReconnectMaxRetries(origReconnectMaxRetries); Settings.setInputMask(origInputMask); Settings.setInputMaskChar(origInputMaskChar); + Settings.setBlockPaste(origBlockPaste); } @Test @@ -74,6 +77,39 @@ public class BehaviorSettingsTest { assertEquals("*", Settings.getInputMaskChar()); } + @Test + @DisplayName("Block paste setting defaults to true and binds to SettingsDialog") + public void testBlockPasteSettingAndDialogBinding() throws Exception { + // Default must be true + assertTrue(Settings.getBlockPaste(), "Settings.getBlockPaste() should default to true"); + + Settings.setBlockPaste(false); + assertFalse(Settings.getBlockPaste()); + Settings.setBlockPaste(true); + assertTrue(Settings.getBlockPaste()); + + J3270App app; + try { + app = new J3270App(); + } catch (HeadlessException e) { + return; + } + + try { + SettingsDialog dialog = new SettingsDialog(app); + Field blockPasteField = SettingsDialog.class.getDeclaredField("blockPasteCheck"); + blockPasteField.setAccessible(true); + JCheckBox blockPasteCheck = (JCheckBox) blockPasteField.get(dialog); + assertNotNull(blockPasteCheck, "blockPasteCheck must exist in SettingsDialog"); + assertEquals("Block paste", blockPasteCheck.getText()); + assertEquals(Settings.getBlockPaste(), blockPasteCheck.isSelected()); + + dialog.dispose(); + app.dispose(); + } catch (HeadlessException ignored) { + } + } + @Test @DisplayName("INI export and load preserves autoReconnect and inputMask") public void testIniExportAndLoad() throws Exception { @@ -173,4 +209,90 @@ public class BehaviorSettingsTest { } catch (HeadlessException ignored) { } } + + @Test + @DisplayName("SettingsDialog tabs are organized into Appearance, Behavior, Connection, and Entry Assist") + public void testSettingsDialogReorganizedTabsAndGrouping() throws Exception { + J3270App app; + try { + app = new J3270App(); + } catch (HeadlessException e) { + return; + } + + try { + SettingsDialog dialog = new SettingsDialog(app); + + // Find top-level JTabbedPane + JTabbedPane mainTabs = null; + for (Component c : dialog.getContentPane().getComponents()) { + if (c instanceof JTabbedPane) { + mainTabs = (JTabbedPane) c; + break; + } + } + assertNotNull(mainTabs, "Main JTabbedPane must exist"); + assertEquals(4, mainTabs.getTabCount(), "Must have 4 main tabs"); + assertEquals("Appearance", mainTabs.getTitleAt(0)); + assertEquals("Behavior", mainTabs.getTitleAt(1)); + assertEquals("Connection", mainTabs.getTitleAt(2)); + assertEquals("Entry Assist", mainTabs.getTitleAt(3)); + + // Verify Appearance subtabs: Display & Font, Colors + Component appComp = mainTabs.getComponentAt(0); + assertTrue(appComp instanceof JPanel); + JTabbedPane appTabs = findTabbedPane((JPanel) appComp); + assertNotNull(appTabs, "Appearance panel must have subtabs"); + assertEquals(2, appTabs.getTabCount()); + assertEquals("Display & Font", appTabs.getTitleAt(0)); + assertEquals("Colors", appTabs.getTitleAt(1)); + + // Verify Behavior subtabs: General, Keymap + Component behComp = mainTabs.getComponentAt(1); + assertTrue(behComp instanceof JPanel); + JTabbedPane behTabs = findTabbedPane((JPanel) behComp); + assertNotNull(behTabs, "Behavior panel must have subtabs"); + assertEquals(2, behTabs.getTabCount()); + assertEquals("General", behTabs.getTitleAt(0)); + assertEquals("Keymap", behTabs.getTitleAt(1)); + + // Verify controls exist in SettingsDialog + Field cursorStyleField = SettingsDialog.class.getDeclaredField("cursorStyleBox"); + cursorStyleField.setAccessible(true); + assertNotNull(cursorStyleField.get(dialog), "cursorStyleBox must exist"); + + Field crosshairField = SettingsDialog.class.getDeclaredField("crosshairRulerCheck"); + crosshairField.setAccessible(true); + assertNotNull(crosshairField.get(dialog), "crosshairRulerCheck must exist"); + + Field fourColorField = SettingsDialog.class.getDeclaredField("fourColorOverrideCheck"); + fourColorField.setAccessible(true); + assertNotNull(fourColorField.get(dialog), "fourColorOverrideCheck must exist"); + + Field numericLockField = SettingsDialog.class.getDeclaredField("numericFieldLockCheck"); + numericLockField.setAccessible(true); + assertNotNull(numericLockField.get(dialog), "numericFieldLockCheck must exist"); + + Field autoSkipField = SettingsDialog.class.getDeclaredField("autoSkipCheck"); + autoSkipField.setAccessible(true); + assertNotNull(autoSkipField.get(dialog), "autoSkipCheck must exist"); + + dialog.dispose(); + app.dispose(); + } catch (HeadlessException ignored) { + } + } + + private static JTabbedPane findTabbedPane(Container container) { + for (Component c : container.getComponents()) { + if (c instanceof JTabbedPane) { + return (JTabbedPane) c; + } + if (c instanceof Container) { + JTabbedPane nested = findTabbedPane((Container) c); + if (nested != null) return nested; + } + } + return null; + } } diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/FontSettingsPersistenceTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/FontSettingsPersistenceTest.java index 23691b6..732e166 100644 --- a/j3270/src/test/java/haus/nightmare/j3270/ui/FontSettingsPersistenceTest.java +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/FontSettingsPersistenceTest.java @@ -15,17 +15,23 @@ public class FontSettingsPersistenceTest { private int originalFontSize; private String originalFontFamily; + private int originalUiFontSize; + private String originalUiFontFamily; @BeforeEach public void setup() { originalFontSize = Settings.getFontSize(); originalFontFamily = Settings.getFontFamily(); + originalUiFontSize = Settings.getUiFontSize(); + originalUiFontFamily = Settings.getUiFontFamily(); } @AfterEach public void tearDown() { Settings.setFontSize(originalFontSize); Settings.setFontFamily(originalFontFamily); + Settings.setUiFontSize(originalUiFontSize); + Settings.setUiFontFamily(originalUiFontFamily); } @Test @@ -39,6 +45,76 @@ public class FontSettingsPersistenceTest { assertEquals(18, Settings.getFontSize()); } + @Test + public void testUiFontSettingsIndependence() { + // Configure terminal fonts + Settings.setFontFamily("Courier"); + Settings.setFontSize(20); + + // Configure UI fonts + Settings.setUiFontFamily("Dialog"); + Settings.setUiFontSize(15); + + // Verify independent getters + assertEquals("Courier", Settings.getFontFamily()); + assertEquals(20, Settings.getFontSize()); + assertEquals("Dialog", Settings.getUiFontFamily()); + assertEquals(15, Settings.getUiFontSize()); + + // Change UI font and ensure terminal font is untouched + Settings.setUiFontSize(18); + Settings.setUiFontFamily("SansSerif"); + assertEquals("Courier", Settings.getFontFamily(), "Terminal font family must not change when UI font changes"); + assertEquals(20, Settings.getFontSize(), "Terminal font size must not change when UI font changes"); + assertEquals("SansSerif", Settings.getUiFontFamily()); + assertEquals(18, Settings.getUiFontSize()); + + // Change terminal font and ensure UI font is untouched + Settings.setFontSize(24); + Settings.setFontFamily("IBM 3270"); + assertEquals("SansSerif", Settings.getUiFontFamily(), "UI font family must not change when terminal font changes"); + assertEquals(18, Settings.getUiFontSize(), "UI font size must not change when terminal font changes"); + assertEquals("IBM 3270", Settings.getFontFamily()); + assertEquals(24, Settings.getFontSize()); + } + + @Test + public void testUiFontIniImportExport() throws Exception { + java.io.File tempIni = java.io.File.createTempFile("j3270_ui_font_test", ".ini"); + tempIni.deleteOnExit(); + + try { + try (java.io.PrintWriter pw = new java.io.PrintWriter(tempIni)) { + pw.println("[appearance]"); + pw.println("uiFontFamily = Dialog"); + pw.println("uiFontSize = 16"); + pw.println("fontFamily = Monospaced"); + pw.println("fontSize = 22"); + } + + Settings.loadFromIniFile(tempIni.getAbsolutePath()); + + assertEquals("Dialog", Settings.getUiFontFamily()); + assertEquals(16, Settings.getUiFontSize()); + assertEquals("Monospaced", Settings.getFontFamily()); + assertEquals(22, Settings.getFontSize()); + + // Export to another file and check contents + java.io.File exportIni = java.io.File.createTempFile("j3270_ui_font_export", ".ini"); + exportIni.deleteOnExit(); + + Settings.exportToIniFile(exportIni.getAbsolutePath()); + String exported = new String(java.nio.file.Files.readAllBytes(exportIni.toPath())); + + assertTrue(exported.contains("uiFontFamily = Dialog"), "Exported INI must contain uiFontFamily"); + assertTrue(exported.contains("uiFontSize = 16"), "Exported INI must contain uiFontSize"); + assertTrue(exported.contains("fontFamily = Monospaced"), "Exported INI must contain terminal fontFamily"); + assertTrue(exported.contains("fontSize = 22"), "Exported INI must contain terminal fontSize"); + } finally { + tempIni.delete(); + } + } + @Test public void testTerminalPanelResizeDoesNotResetFontSize() { try { @@ -77,4 +153,57 @@ public class FontSettingsPersistenceTest { // In headless environment without display, Settings persistence is verified above } } + + @Test + public void testSettingsDialogApplyDoesNotIncrementFontSize() throws Exception { + try { + Settings.setUiFontSize(16); + Settings.setFontSize(22); + Settings.setUiFontFamily("Dialog"); + Settings.setFontFamily("Monospaced"); + ThemeManager.applyUIManagerDefaults(ThemeManager.getTheme()); + + SettingsDialog dialog = new SettingsDialog(null); + + java.lang.reflect.Method applyMethod = SettingsDialog.class.getDeclaredMethod("applySettings"); + applyMethod.setAccessible(true); + + // Find all JLabels in SettingsDialog + java.util.List labels = new java.util.ArrayList<>(); + findLabelsRecursively(dialog.getContentPane(), labels); + assertFalse(labels.isEmpty(), "SettingsDialog must contain labels"); + + for (javax.swing.JLabel label : labels) { + assertEquals(16, label.getFont().getSize(), + "Label '" + label.getText() + "' must initially have size 16"); + } + + // Click Apply multiple times + for (int click = 1; click <= 5; click++) { + applyMethod.invoke(dialog); + + assertEquals(16, Settings.getUiFontSize(), "Settings uiFontSize must stay 16"); + assertEquals(22, Settings.getFontSize(), "Settings fontSize must stay 22"); + + for (javax.swing.JLabel label : labels) { + assertEquals(16, label.getFont().getSize(), + "Label '" + label.getText() + "' must remain at size 16 after click #" + click); + } + } + + dialog.dispose(); + } catch (HeadlessException e) { + // Ignored in headless environment if dialog creation fails + } + } + + private void findLabelsRecursively(java.awt.Container container, java.util.List labels) { + for (java.awt.Component c : container.getComponents()) { + if (c instanceof javax.swing.JLabel) { + labels.add((javax.swing.JLabel) c); + } else if (c instanceof java.awt.Container) { + findLabelsRecursively((java.awt.Container) c, labels); + } + } + } } diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/ImportConfigTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/ImportConfigTest.java new file mode 100644 index 0000000..ad1be8b --- /dev/null +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/ImportConfigTest.java @@ -0,0 +1,157 @@ +package haus.nightmare.j3270.ui; + +import haus.nightmare.j3270.J3270App; +import haus.nightmare.j3270.config.Settings; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +public class ImportConfigTest { + + private String origFontFamily; + private int origFontSize; + private String origHost; + private int origPort; + private boolean origAutoReconnect; + + @BeforeEach + public void setUp() { + origFontFamily = Settings.getFontFamily(); + origFontSize = Settings.getFontSize(); + origHost = Settings.getAutoConnectHost(); + origPort = Settings.getAutoConnectPort(); + origAutoReconnect = Settings.getAutoConnectAutoReconnect(); + } + + @AfterEach + public void tearDown() { + Settings.setFontFamily(origFontFamily); + Settings.setFontSize(origFontSize); + Settings.setAutoConnectHost(origHost); + Settings.setAutoConnectPort(origPort); + Settings.setAutoConnectAutoReconnect(origAutoReconnect); + } + + @Test + @DisplayName("SettingsDialog contains Import Config button positioned to the left of Export Config") + public void testImportButtonPlacementAndProperties() { + J3270App app; + try { + app = new J3270App(); + } catch (HeadlessException e) { + return; + } + + try { + SettingsDialog dialog = new SettingsDialog(app); + + JButton btnImport = dialog.getBtnImport(); + JButton btnExport = dialog.getBtnExport(); + + assertNotNull(btnImport, "Import Config button must exist"); + assertNotNull(btnExport, "Export Config button must exist"); + + assertEquals("Import Config...", btnImport.getText()); + assertEquals("Export Config...", btnExport.getText()); + + // Verify btnImport is in the same container and positioned before (to the left of) btnExport + Container parent = btnImport.getParent(); + assertNotNull(parent, "Import button must have a parent container"); + assertSame(parent, btnExport.getParent(), "Import and Export buttons must share the same parent panel"); + + Component[] comps = parent.getComponents(); + int importIdx = -1; + int exportIdx = -1; + for (int i = 0; i < comps.length; i++) { + if (comps[i] == btnImport) importIdx = i; + if (comps[i] == btnExport) exportIdx = i; + } + + assertTrue(importIdx >= 0, "btnImport must be in parent container"); + assertTrue(exportIdx >= 0, "btnExport must be in parent container"); + assertTrue(importIdx < exportIdx, "btnImport must be placed to the left of btnExport (index " + importIdx + " < " + exportIdx + ")"); + + dialog.dispose(); + app.dispose(); + } catch (HeadlessException ignored) { + } + } + + @Test + @DisplayName("importConfigFile updates Settings and refreshes SettingsDialog controls") + public void testImportConfigFileUpdatesSettingsAndDialog() throws IOException { + J3270App app; + try { + app = new J3270App(); + } catch (HeadlessException e) { + return; + } + + File tempIni = File.createTempFile("j3270_test_import", ".ini"); + try { + try (FileWriter writer = new FileWriter(tempIni)) { + writer.write("[appearance]\n"); + writer.write("fontSize = 24\n"); + writer.write("fontFamily = Monospaced\n"); + writer.write("\n[behavior]\n"); + writer.write("startupBehavior = AUTO_CONNECT\n"); + writer.write("autoConnectHost = testhost.example.org\n"); + writer.write("autoConnectPort = 2323\n"); + writer.write("autoConnectAutoReconnect = true\n"); + } + + SettingsDialog dialog = new SettingsDialog(app); + dialog.importConfigFile(tempIni); + + // Verify Settings updated + assertEquals(24, Settings.getFontSize()); + assertEquals("testhost.example.org", Settings.getAutoConnectHost()); + assertEquals(2323, Settings.getAutoConnectPort()); + assertTrue(Settings.getAutoConnectAutoReconnect()); + + // Verify dialog state reflects the imported values + assertEquals(Settings.StartupBehavior.AUTO_CONNECT, Settings.getStartupBehavior()); + + dialog.dispose(); + app.dispose(); + } catch (HeadlessException ignored) { + } finally { + if (tempIni.exists()) { + tempIni.delete(); + } + } + } + + @Test + @DisplayName("importConfigFile throws FileNotFoundException for nonexistent file") + public void testImportNonExistentFileThrows() { + J3270App app; + try { + app = new J3270App(); + } catch (HeadlessException e) { + return; + } + + try { + SettingsDialog dialog = new SettingsDialog(app); + File nonExistent = new File("/path/to/nonexistent/file_" + System.currentTimeMillis() + ".ini"); + assertThrows(FileNotFoundException.class, () -> { + dialog.importConfigFile(nonExistent); + }); + + dialog.dispose(); + app.dispose(); + } catch (HeadlessException ignored) { + } + } +} diff --git a/j3270/src/test/java/haus/nightmare/j3270/ui/ThemeManagerTest.java b/j3270/src/test/java/haus/nightmare/j3270/ui/ThemeManagerTest.java index 51aeb08..9378407 100644 --- a/j3270/src/test/java/haus/nightmare/j3270/ui/ThemeManagerTest.java +++ b/j3270/src/test/java/haus/nightmare/j3270/ui/ThemeManagerTest.java @@ -262,4 +262,81 @@ public class ThemeManagerTest { // Handled in headless CI } } + + @Test + public void testUiFontRetrievalAndDefaults() { + String prevFamily = Settings.getUiFontFamily(); + int prevSize = Settings.getUiFontSize(); + + try { + Settings.setUiFontFamily("Dialog"); + Settings.setUiFontSize(16); + + Font uiFont = ThemeManager.getUiFont(); + assertEquals("Dialog", uiFont.getFamily()); + assertEquals(16, uiFont.getSize()); + assertEquals(Font.PLAIN, uiFont.getStyle()); + + Font boldOffset = ThemeManager.getUiFont(Font.BOLD, 2); + assertEquals(18, boldOffset.getSize()); + assertEquals(Font.BOLD, boldOffset.getStyle()); + + Font monoFont = ThemeManager.getMonospacedUiFont(); + assertEquals(Font.MONOSPACED, monoFont.getName()); + assertEquals(16, monoFont.getSize()); + + // Test applyUIManagerDefaults sets font properties + ThemeManager.applyUIManagerDefaults(UITheme.DARK); + assertEquals(uiFont, UIManager.get("Button.font")); + assertEquals(uiFont, UIManager.get("Label.font")); + assertEquals(uiFont, UIManager.get("Menu.font")); + assertEquals(uiFont, UIManager.get("Table.font")); + assertEquals(monoFont, UIManager.get("TextArea.font")); + } finally { + Settings.setUiFontFamily(prevFamily); + Settings.setUiFontSize(prevSize); + ThemeManager.applyUIManagerDefaults(UITheme.DARK); + } + } + + @Test + public void testLabelFontNotIncrementedOnRepeatedThemeApplication() { + String prevFamily = Settings.getUiFontFamily(); + int prevSize = Settings.getUiFontSize(); + + try { + Settings.setUiFontFamily("Dialog"); + Settings.setUiFontSize(16); + ThemeManager.applyUIManagerDefaults(UITheme.DARK); + + JLabel label = new JLabel("Settings Label"); + ThemeManager.applyTheme(label); + assertEquals(16, label.getFont().getSize(), "Initial label font size must match configured uiFontSize"); + + // Repeated theme applications (e.g. subsequent clicks of 'Apply' in Settings dialog) + // must never increment font size + for (int i = 1; i <= 5; i++) { + ThemeManager.applyTheme(label); + assertEquals(16, label.getFont().getSize(), + "Label font size must remain 16 on subsequent application #" + i); + } + + // Test label with custom delta + JLabel headerLabel = new JLabel("Header Label"); + headerLabel.setFont(ThemeManager.getUiFont(Font.BOLD, 3)); + headerLabel.putClientProperty("uiFontSizeDelta", 3); + ThemeManager.applyTheme(headerLabel); + assertEquals(19, headerLabel.getFont().getSize(), "Custom header label should have base + 3 size"); + + for (int i = 1; i <= 5; i++) { + ThemeManager.applyTheme(headerLabel); + assertEquals(19, headerLabel.getFont().getSize(), + "Header label font size must remain 19 on subsequent application #" + i); + } + } finally { + Settings.setUiFontFamily(prevFamily); + Settings.setUiFontSize(prevSize); + ThemeManager.applyUIManagerDefaults(UITheme.DARK); + } + } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java index f4b13fd..c0f984b 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLPS.java @@ -546,6 +546,29 @@ public class ECLPS implements ECLConstants { private boolean enablePasteFromExcel = true; private boolean pasteStopAtProtectedLine = false; + private boolean blockPaste = true; + + public boolean isBlockPaste() { + if (session != null && session.getProperties() != null) { + String p = session.getProperties().getProperty(ECLSession.BLOCK_PASTE); + if (p != null) return Boolean.parseBoolean(p); + } + return blockPaste; + } + + public boolean IsBlockPaste() { return isBlockPaste(); } + + public void setBlockPaste(boolean val) { + this.blockPaste = val; + if (session != null && session.getProperties() != null) { + session.getProperties().setProperty(ECLSession.BLOCK_PASTE, String.valueOf(val)); + } + if (inputProcessor != null) { + inputProcessor.setBlockPaste(val); + } + } + + public void SetBlockPaste(boolean val) { setBlockPaste(val); } public boolean isEnablePasteFromExcel() { if (session != null && session.getProperties() != null) { @@ -591,7 +614,7 @@ public class ECLPS implements ECLConstants { setCursorPos(row, col); } if (inputProcessor != null) { - return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine()); + return inputProcessor.pasteText(text, true, isPasteStopAtProtectedLine(), isBlockPaste()); } return 0; } @@ -605,9 +628,9 @@ public class ECLPS implements ECLConstants { */ public synchronized int pasteString(String text, int row, int col) { if (text == null || text.isEmpty() || screen == null) return 0; - if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine())) { + if (inputProcessor != null && (text.contains("\t") || isEnablePasteFromExcel() || isPasteStopAtProtectedLine() || isBlockPaste())) { setCursorPos(row, col); - return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine()); + return inputProcessor.pasteText(text, isEnablePasteFromExcel(), isPasteStopAtProtectedLine(), isBlockPaste()); } int rows = screen.getRows(); int cols = screen.getCols(); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLSession.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLSession.java index 70a5b97..61a58f9 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLSession.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/ecl/ECLSession.java @@ -38,6 +38,7 @@ public class ECLSession { public static final String ENABLE_PASTE_FROM_EXCEL = "enablePasteFromExcel"; public static final String PASTE_TAB_OPTIONS = "pasteTabOptions"; public static final String PASTE_STOP_AT_PROTECTED_LINE = "pasteStopAtProtectedLine"; + public static final String BLOCK_PASTE = "blockPaste"; public static final String PASTE_FIELD_WRAP = "pasteFieldWrap"; public static final String PASTE_LINE_WRAP = "pasteLineWrap"; public static final String ENTRYASSIST_DOCMODE = "EntryAssist_DOCmode"; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java index 556ffcb..186cd29 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -137,6 +137,10 @@ public class InputProcessor { private boolean aplKeyboardMode = false; private boolean numericFieldLock = true; private boolean autoSkipEnabled = true; + private boolean blockPaste = true; + + public boolean isBlockPaste() { return blockPaste; } + public void setBlockPaste(boolean val) { this.blockPaste = val; } public void setBellListener(BellListener listener) { this.bellListener = listener; } public BellListener getBellListener() { return bellListener; } @@ -446,6 +450,19 @@ public class InputProcessor { * @return number of characters pasted */ public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine) { + return pasteText(text, enablePasteFromExcel, pasteStopAtProtectedLine, this.blockPaste); + } + + /** + * Pastes text onto the screen with tabular, boundary, and block alignment controls. + * + * @param text text to paste + * @param enablePasteFromExcel whether to parse tabs as field advances and newlines as row advances + * @param pasteStopAtProtectedLine whether to halt paste when encountering protected boundaries + * @param blockPaste whether newlines align each line with the initial starting cursor column + * @return number of characters pasted + */ + public int pasteText(String text, boolean enablePasteFromExcel, boolean pasteStopAtProtectedLine, boolean blockPaste) { if (text == null || text.isEmpty() || screen == null || keyboardLocked) { return 0; } @@ -463,6 +480,7 @@ public class InputProcessor { int count = 0; int len = text.length(); int i = 0; + int startCol = screen.getCursorCol(); while (i < len && !keyboardLocked) { char ch = text.charAt(i); @@ -472,7 +490,34 @@ public class InputProcessor { if (ch == '\r' && (i + 1) < len && text.charAt(i + 1) == '\n') { i++; // skip \n of \r\n } - if (enablePasteFromExcel) { + if (blockPaste) { + int curRow = screen.getCursorRow(); + int nextRow = (curRow + 1) % screen.getRows(); + if (pasteStopAtProtectedLine && isLineProtected(nextRow)) { + break; // Stop paste when next line is protected + } + int cols = screen.getCols(); + int rows = screen.getRows(); + int targetAddr = nextRow * cols + startCol; + if (screen.isFormatted()) { + int size = rows * cols; + ExtendedAttribute ea = screen.getCell(targetAddr); + if (ea.isFieldAttribute()) { + targetAddr = (targetAddr + 1) % size; + } + byte faVal = screen.getFieldAttributeAt(targetAddr); + if (faIsProtected(faVal & 0xFF)) { + if (pasteStopAtProtectedLine) { + break; + } else { + targetAddr = screen.findNextUnprotected(targetAddr == 0 ? (size - 1) : (targetAddr - 1)); + } + } + } + screen.setCursorAddress(targetAddr); + screen.markAllChanged(); + screen.updateDisplaySnapshot(); + } else if (enablePasteFromExcel) { int curRow = screen.getCursorRow(); int nextRow = (curRow + 1) % screen.getRows(); if (pasteStopAtProtectedLine && isLineProtected(nextRow)) { diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/input/BlockPasteTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/input/BlockPasteTest.java new file mode 100644 index 0000000..b6ecba1 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/input/BlockPasteTest.java @@ -0,0 +1,144 @@ +package haus.nightmare.lib3270j.input; + +import haus.nightmare.lib3270j.TerminalModel; +import haus.nightmare.lib3270j.charset.EbcdicTranslator; +import haus.nightmare.lib3270j.ecl.ECLPS; +import haus.nightmare.lib3270j.screen.ScreenBuffer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static haus.nightmare.lib3270j.protocol.DS3270Constants.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the "Block paste" feature. + * Ensures that multi-line pastes maintain column alignment with the initial start column + * when blockPaste is active (the default), and revert to standard 3270 Newline field advance + * when blockPaste is disabled. + */ +public class BlockPasteTest { + + private ScreenBuffer screen; + private EbcdicTranslator translator; + private InputProcessor input; + private ECLPS ps; + + @BeforeEach + public void setUp() { + translator = new EbcdicTranslator(); + screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator); + input = new InputProcessor(screen, translator, null); + ps = new ECLPS(screen, input, translator); + } + + @Test + public void testBlockPasteDefaultsToTrue() { + assertTrue(input.isBlockPaste(), "InputProcessor blockPaste should default to true"); + assertTrue(ps.isBlockPaste(), "ECLPS blockPaste should default to true"); + } + + @Test + public void testBlockPasteAlignsStartColumnInUnformattedScreen() { + screen.erase(false); + // Start cursor at row 0, column 15 + int startPos = screen.rowColToAddress(0, 15); + screen.setCursorAddress(startPos); + + String multilineText = "FIRST\nSECOND\nTHIRD"; + int pasted = input.pasteText(multilineText, true, false, true); + assertEquals(16, pasted); + + // Verify row 0, col 15-19 has "FIRST" + assertEquals("FIRST", ps.getString(startPos, 5)); + + // Verify row 1, col 15-20 has "SECOND" + int row1Col15 = screen.rowColToAddress(1, 15); + assertEquals("SECOND", ps.getString(row1Col15, 6)); + + // Verify row 2, col 15-19 has "THIRD" + int row2Col15 = screen.rowColToAddress(2, 15); + assertEquals("THIRD", ps.getString(row2Col15, 5)); + + // Verify start of row 1 (cols 0-5) did NOT receive the paste + int row1Col0 = screen.rowColToAddress(1, 0); + assertNotEquals("SECOND", ps.getString(row1Col0, 6)); + } + + @Test + public void testBlockPasteAlignsStartColumnInFormattedScreen() { + screen.erase(false); + // Set up 3 rows each with an unprotected field starting at col 0 (FA at 0, 80, 160) + screen.setFieldAttribute(0, (byte) FA_PRINTABLE); + screen.setFieldAttribute(80, (byte) FA_PRINTABLE); + screen.setFieldAttribute(160, (byte) FA_PRINTABLE); + + // Start pasting at Row 0, Col 25 + int startPos = screen.rowColToAddress(0, 25); + screen.setCursorAddress(startPos); + + String multilineText = "ALPHA\r\nBETA\r\nGAMMA"; + int pasted = input.pasteText(multilineText, true, false, true); + assertEquals(14, pasted); + + assertEquals("ALPHA", ps.getString(screen.rowColToAddress(0, 25), 5)); + assertEquals("BETA", ps.getString(screen.rowColToAddress(1, 25), 4)); + assertEquals("GAMMA", ps.getString(screen.rowColToAddress(2, 25), 5)); + } + + @Test + public void testDisabledBlockPasteRevertsToFirstUnprotectedField() { + screen.erase(false); + // Row 0: FA at 0 (unprotected col 1-79) + // Row 1: FA at 80 (unprotected col 1-79) + screen.setFieldAttribute(0, (byte) FA_PRINTABLE); + screen.setFieldAttribute(80, (byte) FA_PRINTABLE); + + // Start pasting at Row 0, Col 25 + int startPos = screen.rowColToAddress(0, 25); + screen.setCursorAddress(startPos); + + // blockPaste = false: newline should advance to first unprotected field on next row (pos 81 / col 1) + String multilineText = "LINE1\nLINE2"; + input.pasteText(multilineText, true, false, false); + + assertEquals("LINE1", ps.getString(screen.rowColToAddress(0, 25), 5)); + // LINE2 must be at row 1, col 1 (pos 81) rather than col 25 + assertEquals("LINE2", ps.getString(81, 5)); + } + + @Test + public void testBlockPasteStopAtProtectedBoundary() { + screen.erase(false); + // Row 0: unprotected field at pos 0 + screen.setFieldAttribute(0, (byte) FA_PRINTABLE); + // Row 1: entirely protected row + screen.setFieldAttribute(80, (byte) (FA_PRINTABLE | FA_PROTECT)); + + int startPos = screen.rowColToAddress(0, 10); + screen.setCursorAddress(startPos); + + // pasteStopAtProtectedLine = true + String multilineText = "ROW0\nROW1"; + int pasted = input.pasteText(multilineText, true, true, true); + + // Should paste "ROW0" (4 chars) and halt when row 1 is protected + assertEquals(4, pasted); + assertEquals("ROW0", ps.getString(startPos, 4)); + } + + @Test + public void testEclPsPasteStringHonorsBlockPaste() { + screen.erase(false); + screen.setFieldAttribute(0, (byte) FA_PRINTABLE); + screen.setFieldAttribute(80, (byte) FA_PRINTABLE); + + ps.setBlockPaste(true); + assertTrue(ps.isBlockPaste()); + + int pasted = ps.pasteString("DATA1\nDATA2", 0, 12); + assertEquals(10, pasted); + + assertEquals("DATA1", ps.getString(screen.rowColToAddress(0, 12), 5)); + assertEquals("DATA2", ps.getString(screen.rowColToAddress(1, 12), 5)); + } +}