diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index cb02b0d..0b8eee3 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -806,7 +806,19 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : ""; setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator); - terminalPanel.guardedPack(); + int targetRows = 24; + int targetCols = 80; + TerminalModel model = config.getModel(); + if (model != null) { + if (model.isDynamic()) { + targetRows = config.getDynamicRows(); + targetCols = config.getDynamicCols(); + } else { + targetRows = model.getAlternateRows(); + targetCols = model.getAlternateCols(); + } + } + terminalPanel.packForModel(targetRows, targetCols); new Thread(() -> { try { @@ -903,7 +915,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate String dev = (client != null && client.getTelnetFSM() != null && client.getTelnetFSM().getConnectedLu() != null) ? " [" + client.getTelnetFSM().getConnectedLu() + "]" : ""; setTitle("j3270 — " + lastHost + ":" + lastPort + tlsIndicator + dev); - terminalPanel.guardedPack(); + terminalPanel.revalidate(); + terminalPanel.repaint(); terminalPanel.requestFocusInWindow(); } }); @@ -954,7 +967,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate @Override public void onScreenSizeChanged(int rows, int cols) { SwingUtilities.invokeLater(() -> { - terminalPanel.guardedPack(); + terminalPanel.revalidate(); + terminalPanel.repaint(); statusBar.updateStatus(); }); } 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 55b95d2..78cf85d 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java @@ -11,12 +11,16 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.JTableHeader; import javax.swing.border.LineBorder; import javax.swing.border.TitledBorder; +import haus.nightmare.j3270.storage.HostStorage; +import haus.nightmare.j3270.storage.SavedHost; public class SettingsDialog extends JDialog { @@ -58,8 +62,7 @@ public class SettingsDialog extends JDialog { // Connection tab private JComboBox startupBehaviorBox; private JPanel autoConnectPanel; - private JTextField hostField; - private JTextField portField; + private JComboBox savedHostBox; private JCheckBox autoReconnectCheck; private JSpinner dynamicRowsSpinner; private JSpinner dynamicColsSpinner; @@ -588,22 +591,33 @@ public class SettingsDialog extends JDialog { acGbc.insets = new Insets(5, 5, 5, 5); acGbc.anchor = GridBagConstraints.WEST; - JLabel hostLabel = new JLabel("Host:"); + JLabel savedConnLabel = new JLabel("Saved Connection:"); acGbc.gridx = 0; acGbc.gridy = 0; - autoConnectPanel.add(hostLabel, acGbc); + autoConnectPanel.add(savedConnLabel, acGbc); - hostField = new JTextField(Settings.getAutoConnectHost(), 15); + savedHostBox = new JComboBox<>(); + savedHostBox.setRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value instanceof SavedHost) { + SavedHost sh = (SavedHost) value; + if (sh.getName() != null && !sh.getName().trim().isEmpty()) { + setText(sh.getName() + " (" + sh.getHost() + ":" + sh.getPort() + ")"); + } else { + setText(sh.getHost() + ":" + sh.getPort()); + } + } else if (value == null) { + setText("(No saved connections)"); + } + return this; + } + }); + refreshSavedHostsBox(); acGbc.gridx = 1; - autoConnectPanel.add(hostField, acGbc); - - JLabel portLabel = new JLabel("Port:"); - acGbc.gridx = 2; - autoConnectPanel.add(portLabel, acGbc); - - portField = new JTextField(String.valueOf(Settings.getAutoConnectPort()), 4); - acGbc.gridx = 3; - autoConnectPanel.add(portField, acGbc); + acGbc.fill = GridBagConstraints.HORIZONTAL; + autoConnectPanel.add(savedHostBox, acGbc); gbc.gridx = 0; gbc.gridy = 1; @@ -658,6 +672,30 @@ public class SettingsDialog extends JDialog { return panel; } + private void refreshSavedHostsBox() { + if (savedHostBox == null) return; + savedHostBox.removeAllItems(); + List savedHosts = HostStorage.getSavedHosts(); + SavedHost currentAutoHost = HostStorage.getAutoConnectHost(); + SavedHost toSelect = null; + for (SavedHost sh : savedHosts) { + savedHostBox.addItem(sh); + if (currentAutoHost != null && Objects.equals(sh.getId(), currentAutoHost.getId())) { + toSelect = sh; + } + } + if (toSelect != null) { + savedHostBox.setSelectedItem(toSelect); + savedHostBox.setEnabled(true); + } else if (!savedHosts.isEmpty()) { + savedHostBox.setSelectedIndex(0); + savedHostBox.setEnabled(true); + } else { + savedHostBox.addItem(null); + savedHostBox.setEnabled(false); + } + } + private JPanel createEntryAssistPanel() { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); @@ -1187,11 +1225,32 @@ public class SettingsDialog extends JDialog { if (behavior != null) { Settings.setStartupBehavior(behavior); } - if (behavior == Settings.StartupBehavior.AUTO_CONNECT && hostField != null && portField != null) { - Settings.setAutoConnectHost(hostField.getText().trim()); - try { - Settings.setAutoConnectPort(Integer.parseInt(portField.getText().trim())); - } catch (NumberFormatException ignored) { + if (behavior == Settings.StartupBehavior.AUTO_CONNECT && savedHostBox != null) { + Object sel = savedHostBox.getSelectedItem(); + if (sel instanceof SavedHost) { + SavedHost selected = (SavedHost) sel; + List hosts = HostStorage.getSavedHosts(); + for (SavedHost h : hosts) { + h.setAutoConnect(Objects.equals(h.getId(), selected.getId())); + } + HostStorage.saveAll(hosts); + Settings.setAutoConnectHost(selected.getHost()); + Settings.setAutoConnectPort(selected.getPort()); + Settings.setAutoConnectTls(selected.isUseTls()); + Settings.setAutoConnectVerifyCert(selected.isTlsVerifyCert()); + Settings.setAutoConnectTn3270e(selected.isTn3270e()); + } + } else if (behavior != Settings.StartupBehavior.AUTO_CONNECT) { + List hosts = HostStorage.getSavedHosts(); + boolean modified = false; + for (SavedHost h : hosts) { + if (h.isAutoConnect()) { + h.setAutoConnect(false); + modified = true; + } + } + if (modified) { + HostStorage.saveAll(hosts); } } } @@ -1371,12 +1430,7 @@ public class SettingsDialog extends JDialog { autoConnectPanel.setVisible(startupBehaviorBox.getSelectedItem() == Settings.StartupBehavior.AUTO_CONNECT); } } - if (hostField != null) { - hostField.setText(Settings.getAutoConnectHost()); - } - if (portField != null) { - portField.setText(String.valueOf(Settings.getAutoConnectPort())); - } + refreshSavedHostsBox(); if (autoReconnectCheck != null) { autoReconnectCheck.setSelected(Settings.getAutoConnectAutoReconnect()); } @@ -1494,6 +1548,10 @@ public class SettingsDialog extends JDialog { return btnExport; } + public JComboBox getSavedHostBox() { + return savedHostBox; + } + // ========== Export Config ========== private void exportConfig() { 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 52d1bd7..e5fceda 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -29,7 +29,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { int cellHeight; private int fontAscent; private int fontDescent; - private int padding = 4; + private int padding = 1; private int currentFontSize = 16; // Cursor state @@ -96,6 +96,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { private long lastGraphicsUpdateCount = -1; private java.awt.image.BufferedImage cachedGraphicsImage = null; private boolean resizeGuard = false; + private Integer targetModelRows = null; + private Integer targetModelCols = null; private StatusBar statusBar = null; // ========== Phase 25: Hotspots & Mouse Wheel ========== @@ -134,13 +136,21 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { /** * Compute the horizontal render offset to center the grid within the panel. + * The left bound of the window is the left bound of the left cells when constrained. + * Only if the window is too big is there blank space on the left, and it is equal + * to or less than the space on the right at all times. */ int getRenderOffsetX() { int termCols = 80; - if (client != null) termCols = client.getScreenBuffer().getDisplayCols(); + if (client != null && client.getScreenBuffer() != null) { + termCols = client.getScreenBuffer().getDisplayCols(); + } int gridW = termCols * cellWidth; int extra = getWidth() - gridW; - return Math.max(padding, extra / 2); + if (extra > 0) { + return extra / 2; + } + return 0; } /** @@ -148,10 +158,15 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { */ int getRenderOffsetY() { int termRows = 24; - if (client != null) termRows = client.getScreenBuffer().getDisplayRows(); + if (client != null && client.getScreenBuffer() != null) { + termRows = client.getScreenBuffer().getDisplayRows(); + } int gridH = termRows * cellHeight; int extra = getHeight() - gridH; - return Math.max(padding, extra / 2); + if (extra > 0) { + return extra / 2; + } + return 0; } // Default Host color mapping aligned 1:1 with IBM Host On-Demand ColorRemapModel3270 @@ -1017,6 +1032,33 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { } + public void packForModel(int modelRows, int modelCols) { + if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) { + SwingUtilities.invokeLater(() -> packForModel(modelRows, modelCols)); + return; + } + resizeGuard = true; + updateCellSize(); + this.targetModelRows = modelRows; + this.targetModelCols = modelCols; + revalidate(); + Container top = getTopLevelAncestor(); + if (top instanceof java.awt.Window) { + java.awt.Window win = (java.awt.Window) top; + if (win instanceof java.awt.Frame) { + if ((((java.awt.Frame) win).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) { + win.pack(); + } + } else { + win.pack(); + } + } + this.targetModelRows = null; + this.targetModelCols = null; + repaint(); + SwingUtilities.invokeLater(() -> resizeGuard = false); + } + public void guardedPack() { if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(this::guardedPack); @@ -1025,12 +1067,35 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { resizeGuard = true; revalidate(); Container top = getTopLevelAncestor(); - if (top instanceof java.awt.Frame) { - if ((((java.awt.Frame) top).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) { - ((java.awt.Window) top).pack(); + if (top instanceof java.awt.Window) { + java.awt.Window win = (java.awt.Window) top; + if (win.isShowing()) { + int termCols = 80; + int termRows = 24; + if (client != null && client.getScreenBuffer() != null) { + termCols = client.getScreenBuffer().getDisplayCols(); + termRows = client.getScreenBuffer().getDisplayRows(); + } + int gridW = termCols * cellWidth; + int gridH = termRows * cellHeight; + Dimension winSize = win.getSize(); + Dimension winPref = win.getPreferredSize(); + boolean tooSmall = winSize.width < winPref.width || winSize.height < winPref.height + || (getWidth() > 0 && getWidth() < gridW) + || (getHeight() > 0 && getHeight() < gridH); + if (tooSmall) { + repaint(); + SwingUtilities.invokeLater(() -> resizeGuard = false); + return; + } + } + if (win instanceof java.awt.Frame) { + if ((((java.awt.Frame) win).getExtendedState() & java.awt.Frame.MAXIMIZED_BOTH) == 0) { + win.pack(); + } + } else { + win.pack(); } - } else if (top instanceof java.awt.Window) { - ((java.awt.Window) top).pack(); } SwingUtilities.invokeLater(() -> resizeGuard = false); } @@ -1873,7 +1938,11 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable { @Override public Dimension getPreferredSize() { - if (client != null) { + if (targetModelRows != null && targetModelCols != null) { + return new Dimension(targetModelCols * cellWidth + padding * 2, + targetModelRows * cellHeight + padding * 2); + } + if (client != null && client.getScreenBuffer() != null) { ScreenBuffer sb = client.getScreenBuffer(); int displayCols = sb.getDisplayCols(); int displayRows = sb.getDisplayRows();