diff --git a/artwork/j3270_icon.svg b/artwork/j3270_icon.svg new file mode 100644 index 0000000..a332e3a --- /dev/null +++ b/artwork/j3270_icon.svg @@ -0,0 +1,146 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/artwork/lib3270j.svg b/artwork/lib3270j.svg new file mode 100644 index 0000000..6aa51a3 --- /dev/null +++ b/artwork/lib3270j.svg @@ -0,0 +1,146 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index 16ef191..9f474c7 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -60,6 +60,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate super("j3270 — Java TN3270 Terminal Emulator"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setBackground(Color.BLACK); + AppIcon.applyTo(this); buildUI(); buildMenuBar(); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java b/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java index 529889d..8416355 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ft/FileTransferDialog.java @@ -1,5 +1,6 @@ package haus.nightmare.j3270.ft; +import haus.nightmare.j3270.ui.AppIcon; import haus.nightmare.j3270.ui.HostDirectoryDialog; import haus.nightmare.j3270.ui.ThemeManager; import haus.nightmare.lib3270j.ft.FTConfig; @@ -92,6 +93,7 @@ public class FileTransferDialog extends JDialog { super(owner, "File Transfer (IND$FILE)", false); this.owner = owner; this.coordinator = coordinator; + AppIcon.applyTo(this); buildUI(); pack(); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/AppIcon.java b/j3270/src/main/java/haus/nightmare/j3270/ui/AppIcon.java new file mode 100644 index 0000000..29495a0 --- /dev/null +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/AppIcon.java @@ -0,0 +1,138 @@ +package haus.nightmare.j3270.ui; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utility for loading and applying application window and taskbar icons. + * Loads multi-resolution lib3270j icons from resources. + */ +public final class AppIcon { + + private static final Logger log = Logger.getLogger(AppIcon.class.getName()); + + private static final int[] ICON_SIZES = {16, 24, 32, 48, 64, 128, 256, 512}; + private static List cachedIcons = null; + private static Image defaultIcon = null; + + private AppIcon() {} + + /** + * Retrieves the list of available application icon images in various resolutions. + * + * @return an unmodifiable list of Image objects, or empty list if none could be loaded. + */ + public static synchronized List getAppIcons() { + if (cachedIcons == null) { + loadIcons(); + } + return cachedIcons; + } + + /** + * Retrieves the default application icon image (256x256 or best available). + * + * @return the default Image, or null if no icon is available. + */ + public static synchronized Image getDefaultIcon() { + if (cachedIcons == null) { + loadIcons(); + } + return defaultIcon; + } + + /** + * Applies the application icons to a target Window (JFrame, JDialog, etc.) + * and updates the desktop Taskbar icon if supported. + * + * @param window the Window to set icon images for. + */ + public static void applyTo(Window window) { + if (GraphicsEnvironment.isHeadless()) { + return; + } + + List icons = getAppIcons(); + if (icons.isEmpty()) { + return; + } + + if (window != null) { + window.setIconImages(icons); + } + + // Apply to OS Taskbar / Dock if supported (Java 9+) + try { + if (Taskbar.isTaskbarSupported()) { + Taskbar taskbar = Taskbar.getTaskbar(); + if (taskbar.isSupported(Taskbar.Feature.ICON_IMAGE)) { + Image bestIcon = defaultIcon != null ? defaultIcon : icons.get(icons.size() - 1); + taskbar.setIconImage(bestIcon); + } + } + } catch (Throwable t) { + log.log(Level.FINE, "Taskbar icon could not be set: {0}", t.getMessage()); + } + } + + private static void loadIcons() { + List icons = new ArrayList<>(); + + for (int size : ICON_SIZES) { + String path = "/icons/lib3270j_" + size + ".png"; + Image img = loadImage(path); + if (img != null) { + icons.add(img); + } + } + + if (icons.isEmpty()) { + Image img = loadImage("/icons/lib3270j.png"); + if (img != null) { + icons.add(img); + } + } + + if (!icons.isEmpty()) { + for (Image img : icons) { + if (img.getWidth(null) == 256) { + defaultIcon = img; + break; + } + } + if (defaultIcon == null) { + defaultIcon = icons.get(icons.size() - 1); + } + } + + cachedIcons = Collections.unmodifiableList(icons); + } + + private static Image loadImage(String resourcePath) { + try { + URL url = AppIcon.class.getResource(resourcePath); + if (url != null) { + BufferedImage img = ImageIO.read(url); + if (img != null) { + return img; + } + } + try (InputStream in = AppIcon.class.getResourceAsStream(resourcePath)) { + if (in != null) { + return ImageIO.read(in); + } + } + } catch (Exception e) { + log.log(Level.FINE, "Failed to load icon from " + resourcePath, e); + } + return null; + } +} 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 e3a385c..8e687db 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ConnectDialog.java @@ -31,6 +31,7 @@ public class ConnectDialog extends JDialog { public ConnectDialog(Frame parent) { super(parent, "Connect to Host", true); + AppIcon.applyTo(this); buildUI(); pack(); setLocationRelativeTo(parent); 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 4de1862..0c6c795 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/FieldInspectorDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/FieldInspectorDialog.java @@ -32,6 +32,7 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene super(parent, "3270 Presentation Space Field Inspector", false); this.client = client; this.terminalPanel = terminalPanel; + AppIcon.applyTo(this); buildUI(); setSize(850, 420); 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 0668f33..7c5f572 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/FindDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/FindDialog.java @@ -32,6 +32,7 @@ public class FindDialog extends JDialog { super(parent, "Find on Screen", false); this.client = client; this.terminalPanel = terminalPanel; + AppIcon.applyTo(this); buildUI(); pack(); 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 2f462d0..73b0e67 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/HostDirectoryDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/HostDirectoryDialog.java @@ -40,6 +40,7 @@ public class HostDirectoryDialog extends JDialog { super(parent, "Host Catalog & Directory Browser", true); this.client = client; this.initialHostType = hostType; + AppIcon.applyTo(this); buildUI(initialQuery); setSize(700, 420); 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 8180ed5..f026540 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/PrinterSessionDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/PrinterSessionDialog.java @@ -48,6 +48,7 @@ public class PrinterSessionDialog extends JDialog implements PrintSessionListene public PrinterSessionDialog(Frame parent, String defaultHost, int defaultPort, boolean defaultTls) { super(parent, "IBM 3287 Printer Session Manager", false); this.parent = parent; + AppIcon.applyTo(this); config = new PrinterConfig(defaultHost, defaultPort); config.setUseTls(defaultTls); 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 ac95f63..a23edab 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/ScriptDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ScriptDialog.java @@ -25,6 +25,7 @@ public class ScriptDialog extends JDialog { super(parent, "Run Keystrokes / ECL Script", false); this.client = client; this.terminalPanel = terminalPanel; + AppIcon.applyTo(this); buildUI(); setSize(600, 480); 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 852acfd..0cefcb3 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/SettingsDialog.java @@ -62,6 +62,7 @@ public class SettingsDialog extends JDialog { public SettingsDialog(J3270App parent) { super(parent, "Settings", true); this.parentApp = parent; + AppIcon.applyTo(this); initComponents(); setSize(600, 520); 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 5f909ca..15a17b8 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/UntrustedCertificateDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/UntrustedCertificateDialog.java @@ -17,6 +17,7 @@ public class UntrustedCertificateDialog extends JDialog { public UntrustedCertificateDialog(Frame parent, String host, int port, X509Certificate[] chain, CertificateException exception) { super(parent, "Untrusted SSL/TLS Certificate", true); + AppIcon.applyTo(this); buildUI(host, port, chain, exception); pack(); setLocationRelativeTo(parent); diff --git a/j3270/src/main/resources/icons/j3270_icon.svg b/j3270/src/main/resources/icons/j3270_icon.svg new file mode 100644 index 0000000..a332e3a --- /dev/null +++ b/j3270/src/main/resources/icons/j3270_icon.svg @@ -0,0 +1,146 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/j3270/src/main/resources/icons/lib3270j.png b/j3270/src/main/resources/icons/lib3270j.png new file mode 100644 index 0000000..c0cd2c5 Binary files /dev/null and b/j3270/src/main/resources/icons/lib3270j.png differ diff --git a/j3270/src/main/resources/icons/lib3270j.svg b/j3270/src/main/resources/icons/lib3270j.svg new file mode 100644 index 0000000..6aa51a3 --- /dev/null +++ b/j3270/src/main/resources/icons/lib3270j.svg @@ -0,0 +1,146 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/j3270/src/main/resources/icons/lib3270j_128.png b/j3270/src/main/resources/icons/lib3270j_128.png new file mode 100644 index 0000000..8696639 Binary files /dev/null and b/j3270/src/main/resources/icons/lib3270j_128.png differ diff --git a/j3270/src/main/resources/icons/lib3270j_16.png b/j3270/src/main/resources/icons/lib3270j_16.png new file mode 100644 index 0000000..49b1f9b Binary files /dev/null and b/j3270/src/main/resources/icons/lib3270j_16.png differ diff --git a/j3270/src/main/resources/icons/lib3270j_24.png b/j3270/src/main/resources/icons/lib3270j_24.png new file mode 100644 index 0000000..3234f7d Binary files /dev/null and b/j3270/src/main/resources/icons/lib3270j_24.png differ diff --git a/j3270/src/main/resources/icons/lib3270j_256.png b/j3270/src/main/resources/icons/lib3270j_256.png new file mode 100644 index 0000000..c0cd2c5 Binary files /dev/null and b/j3270/src/main/resources/icons/lib3270j_256.png differ diff --git a/j3270/src/main/resources/icons/lib3270j_32.png b/j3270/src/main/resources/icons/lib3270j_32.png new file mode 100644 index 0000000..45c6133 Binary files /dev/null and b/j3270/src/main/resources/icons/lib3270j_32.png differ diff --git a/j3270/src/main/resources/icons/lib3270j_48.png b/j3270/src/main/resources/icons/lib3270j_48.png new file mode 100644 index 0000000..87e9795 Binary files /dev/null and b/j3270/src/main/resources/icons/lib3270j_48.png differ diff --git a/j3270/src/main/resources/icons/lib3270j_512.png b/j3270/src/main/resources/icons/lib3270j_512.png new file mode 100644 index 0000000..88c0456 Binary files /dev/null and b/j3270/src/main/resources/icons/lib3270j_512.png differ diff --git a/j3270/src/main/resources/icons/lib3270j_64.png b/j3270/src/main/resources/icons/lib3270j_64.png new file mode 100644 index 0000000..3567d8e Binary files /dev/null and b/j3270/src/main/resources/icons/lib3270j_64.png differ diff --git a/j3270/src/main/resources/icons/lib3270j_wide.png b/j3270/src/main/resources/icons/lib3270j_wide.png new file mode 100644 index 0000000..63618db Binary files /dev/null and b/j3270/src/main/resources/icons/lib3270j_wide.png differ diff --git a/j3270/src/test/java/haus/nightmare/j3270/AppIconTest.java b/j3270/src/test/java/haus/nightmare/j3270/AppIconTest.java new file mode 100644 index 0000000..0c9d730 --- /dev/null +++ b/j3270/src/test/java/haus/nightmare/j3270/AppIconTest.java @@ -0,0 +1,57 @@ +package haus.nightmare.j3270; + +import haus.nightmare.j3270.ui.AppIcon; +import org.junit.jupiter.api.Test; + +import javax.swing.*; +import java.awt.*; +import java.net.URL; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class AppIconTest { + + @Test + public void testIconsAreLoaded() { + List icons = AppIcon.getAppIcons(); + assertNotNull(icons, "Icons list must not be null"); + assertFalse(icons.isEmpty(), "At least one icon image must be loaded"); + + // Verify standard sizes are present + int[] expectedSizes = {16, 24, 32, 48, 64, 128, 256, 512}; + for (int expectedSize : expectedSizes) { + boolean found = icons.stream().anyMatch(img -> img.getWidth(null) == expectedSize && img.getHeight(null) == expectedSize); + assertTrue(found, "Expected icon of size " + expectedSize + "x" + expectedSize + " was not found"); + } + } + + @Test + public void testDefaultIcon() { + Image defaultIcon = AppIcon.getDefaultIcon(); + assertNotNull(defaultIcon, "Default icon must not be null"); + assertEquals(256, defaultIcon.getWidth(null), "Default icon width should be 256"); + assertEquals(256, defaultIcon.getHeight(null), "Default icon height should be 256"); + } + + @Test + public void testApplyToInHeadless() { + // Must execute without exception even in headless environment + assertDoesNotThrow(() -> AppIcon.applyTo(null)); + + if (!GraphicsEnvironment.isHeadless()) { + JFrame frame = new JFrame(); + assertDoesNotThrow(() -> AppIcon.applyTo(frame)); + frame.dispose(); + } + } + + @Test + public void testSvgResourcesExist() { + URL svgUrl = getClass().getResource("/icons/lib3270j.svg"); + assertNotNull(svgUrl, "lib3270j.svg must exist in /icons/"); + + URL iconSvgUrl = getClass().getResource("/icons/j3270_icon.svg"); + assertNotNull(iconSvgUrl, "j3270_icon.svg must exist in /icons/"); + } +}