diff --git a/build.gradle b/build.gradle index 0b6bdc6..956032e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ allprojects { group = 'haus.nightmare.j3270' - version = '0.1.0' + version = '1.2.4' repositories { mavenCentral() @@ -23,6 +23,11 @@ subprojects { } } + tasks.withType(Javadoc).configureEach { + options.addStringOption('Xdoclint:none', '-quiet') + options.encoding = 'UTF-8' + } + dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/build_all.sh b/build_all.sh index 72be89e..63f6503 100755 --- a/build_all.sh +++ b/build_all.sh @@ -91,15 +91,23 @@ if [ -d "$SCRIPT_DIR/lib3270j/src/main/resources" ]; then cp -r "$SCRIPT_DIR/lib3270j/src/main/resources/"* "$BUILD_DIR/lib3270j/" 2>/dev/null || true fi -# 4. Package executable JAR -echo "Packaging j3270.jar..." -printf "Manifest-Version: 1.0\nMain-Class: haus.nightmare.j3270.J3270App\nImplementation-Title: j3270\nImplementation-Version: 0.1.0\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/MANIFEST.MF" +# 4. Package JARs +echo "Packaging lib3270j.jar (standalone library)..." +printf "Manifest-Version: 1.0\nImplementation-Title: lib3270j\nImplementation-Version: 1.2.4\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/LIB_MANIFEST.MF" + +"$JAR_BIN" cvfm "$BUILD_DIR/lib3270j.jar" "$BUILD_DIR/LIB_MANIFEST.MF" \ + -C "$BUILD_DIR/lib3270j" . > /dev/null + +echo "Packaging j3270.jar (standalone executable)..." +printf "Manifest-Version: 1.0\nMain-Class: haus.nightmare.j3270.J3270App\nImplementation-Title: j3270\nImplementation-Version: 1.2.4\nCreated-By: j3270 build_all.sh\n\n" > "$BUILD_DIR/MANIFEST.MF" "$JAR_BIN" cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \ -C "$BUILD_DIR/lib3270j" . \ -C "$BUILD_DIR/j3270" . > /dev/null echo "=== Build Complete ===" +echo "Library JAR created: $BUILD_DIR/lib3270j.jar" +ls -lh "$BUILD_DIR/lib3270j.jar" echo "Executable JAR created: $BUILD_DIR/j3270.jar" ls -lh "$BUILD_DIR/j3270.jar" diff --git a/gradle.properties b/gradle.properties index 50234ab..569f7b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1 @@ -android.useAndroidX=true org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f78a6..ad7845b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1aa94a4..379a658 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,10 +15,12 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -27,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob//platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -170,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -203,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 6689b85..8508ef6 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,16 +13,18 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -43,13 +45,13 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -57,36 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java index f7ce565..d500c89 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/J3270App.java +++ b/j3270/src/main/java/haus/nightmare/j3270/J3270App.java @@ -9,6 +9,7 @@ import haus.nightmare.lib3270j.screen.ScreenBuffer; import haus.nightmare.j3270.ui.*; import haus.nightmare.j3270.ft.FileTransfer; import haus.nightmare.j3270.ft.FileTransferDialog; +import haus.nightmare.j3270.config.Settings; import javax.swing.*; import java.awt.*; @@ -182,7 +183,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate terminalPanel.setStatusBarToggleCallback(this::toggleStatusBar); terminalPanel.addModeChangeListener(this::syncModeMenuItems); - statusBar.setVisible(haus.nightmare.j3270.config.Settings.getStatusBarVisible()); + statusBar.setVisible(Settings.getStatusBarVisible()); getContentPane().setLayout(new BorderLayout()); getContentPane().setBackground(Color.BLACK); @@ -195,10 +196,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (wordWrapItem != null) wordWrapItem.setSelected(terminalPanel.isWordWrap()); if (aplModeItem != null) aplModeItem.setSelected(terminalPanel.isAplMode()); if (insertModeItem != null) insertModeItem.setSelected(terminalPanel.isInsertMode()); - if (fourColorItem != null) fourColorItem.setSelected(haus.nightmare.j3270.config.Settings.getFourColorOverride()); - if (numLockItem != null) numLockItem.setSelected(haus.nightmare.j3270.config.Settings.getNumericFieldLock()); - if (autoSkipItem != null) autoSkipItem.setSelected(haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); - if (insertOffAidItem != null) insertOffAidItem.setSelected(haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); + if (fourColorItem != null) fourColorItem.setSelected(Settings.getFourColorOverride()); + if (numLockItem != null) numLockItem.setSelected(Settings.getNumericFieldLock()); + if (autoSkipItem != null) autoSkipItem.setSelected(Settings.getAutoSkipEnabled()); + if (insertOffAidItem != null) insertOffAidItem.setSelected(Settings.getInsertOffOnAid()); } private void onThemeChanged(UITheme theme) { @@ -302,7 +303,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate JRadioButtonMenuItem darkThemeItem = new JRadioButtonMenuItem("Dark Mode", ThemeManager.isDark()); ThemeManager.styleMenuItem(darkThemeItem); darkThemeItem.addActionListener(e -> { - haus.nightmare.j3270.config.Settings.setJavaUiTheme(UITheme.DARK); + Settings.setJavaUiTheme(UITheme.DARK); ThemeManager.setTheme(UITheme.DARK); }); themeGroup.add(darkThemeItem); @@ -311,7 +312,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate JRadioButtonMenuItem lightThemeItem = new JRadioButtonMenuItem("Light Mode", ThemeManager.isLight()); ThemeManager.styleMenuItem(lightThemeItem); lightThemeItem.addActionListener(e -> { - haus.nightmare.j3270.config.Settings.setJavaUiTheme(UITheme.LIGHT); + Settings.setJavaUiTheme(UITheme.LIGHT); ThemeManager.setTheme(UITheme.LIGHT); }); themeGroup.add(lightThemeItem); @@ -320,7 +321,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate viewMenu.add(themeMenu); viewMenu.addSeparator(); - statusBarItem = new JCheckBoxMenuItem("Status Bar", statusBar != null ? statusBar.isVisible() : haus.nightmare.j3270.config.Settings.getStatusBarVisible()); + statusBarItem = new JCheckBoxMenuItem("Status Bar", statusBar != null ? statusBar.isVisible() : Settings.getStatusBarVisible()); ThemeManager.styleMenuItem(statusBarItem); statusBarItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.ALT_DOWN_MASK)); statusBarItem.addActionListener(e -> setStatusBarVisible(statusBarItem.isSelected())); @@ -332,7 +333,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate rulerItem.addActionListener(e -> { boolean en = rulerItem.isSelected(); terminalPanel.setCrosshairRulerEnabled(en); - haus.nightmare.j3270.config.Settings.setCrosshairRuler(en); + Settings.setCrosshairRuler(en); }); viewMenu.add(rulerItem); @@ -342,7 +343,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate ThemeManager.styleMenuItem(blockCursorItem); blockCursorItem.addActionListener(e -> { terminalPanel.setCursorStyle(TerminalPanel.CursorStyle.BLOCK); - haus.nightmare.j3270.config.Settings.setCursorStyle("BLOCK"); + Settings.setCursorStyle("BLOCK"); }); cursorGroup.add(blockCursorItem); cursorMenu.add(blockCursorItem); @@ -351,7 +352,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate ThemeManager.styleMenuItem(underlineCursorItem); underlineCursorItem.addActionListener(e -> { terminalPanel.setCursorStyle(TerminalPanel.CursorStyle.UNDERLINE); - haus.nightmare.j3270.config.Settings.setCursorStyle("UNDERLINE"); + Settings.setCursorStyle("UNDERLINE"); }); cursorGroup.add(underlineCursorItem); cursorMenu.add(underlineCursorItem); @@ -436,34 +437,34 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate modesMenu.addSeparator(); - fourColorItem = new JCheckBoxMenuItem("Base 4-Color Override", haus.nightmare.j3270.config.Settings.getFourColorOverride()); + fourColorItem = new JCheckBoxMenuItem("Base 4-Color Override", Settings.getFourColorOverride()); ThemeManager.styleMenuItem(fourColorItem); fourColorItem.addActionListener(e -> { - haus.nightmare.j3270.config.Settings.setFourColorOverride(fourColorItem.isSelected()); + Settings.setFourColorOverride(fourColorItem.isSelected()); terminalPanel.repaint(); }); modesMenu.add(fourColorItem); - numLockItem = new JCheckBoxMenuItem("Numeric Field Lock", haus.nightmare.j3270.config.Settings.getNumericFieldLock()); + numLockItem = new JCheckBoxMenuItem("Numeric Field Lock", Settings.getNumericFieldLock()); ThemeManager.styleMenuItem(numLockItem); numLockItem.addActionListener(e -> { - haus.nightmare.j3270.config.Settings.setNumericFieldLock(numLockItem.isSelected()); + Settings.setNumericFieldLock(numLockItem.isSelected()); terminalPanel.applyModeSettings(); }); modesMenu.add(numLockItem); - autoSkipItem = new JCheckBoxMenuItem("Auto-Skip Across Fields", haus.nightmare.j3270.config.Settings.getAutoSkipEnabled()); + autoSkipItem = new JCheckBoxMenuItem("Auto-Skip Across Fields", Settings.getAutoSkipEnabled()); ThemeManager.styleMenuItem(autoSkipItem); autoSkipItem.addActionListener(e -> { - haus.nightmare.j3270.config.Settings.setAutoSkipEnabled(autoSkipItem.isSelected()); + Settings.setAutoSkipEnabled(autoSkipItem.isSelected()); terminalPanel.applyModeSettings(); }); modesMenu.add(autoSkipItem); - insertOffAidItem = new JCheckBoxMenuItem("Reset Insert on AID Key", haus.nightmare.j3270.config.Settings.getInsertOffOnAid()); + insertOffAidItem = new JCheckBoxMenuItem("Reset Insert on AID Key", Settings.getInsertOffOnAid()); ThemeManager.styleMenuItem(insertOffAidItem); insertOffAidItem.addActionListener(e -> { - haus.nightmare.j3270.config.Settings.setInsertOffOnAid(insertOffAidItem.isSelected()); + Settings.setInsertOffOnAid(insertOffAidItem.isSelected()); terminalPanel.applyModeSettings(); }); modesMenu.add(insertOffAidItem); @@ -561,13 +562,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate // ========== Dialog Openers ========== - private void showFindDialog() { + public void showFindDialog() { if (client == null) { JOptionPane.showMessageDialog(this, "Connect to a host before using Find.", "Not Connected", JOptionPane.WARNING_MESSAGE); return; } if (findDialog == null) { findDialog = new FindDialog(this, client, terminalPanel); + } else { + findDialog.setClient(client); } findDialog.setVisible(true); } @@ -620,13 +623,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate } } - private void showScriptDialog() { + public void showScriptDialog() { if (client == null) { JOptionPane.showMessageDialog(this, "Connect to a host before running scripts.", "Not Connected", JOptionPane.WARNING_MESSAGE); return; } if (scriptDialog == null) { scriptDialog = new ScriptDialog(this, client, terminalPanel); + } else { + scriptDialog.setClient(client); } scriptDialog.setVisible(true); } @@ -707,7 +712,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate terminalPanel.refreshScreen(); statusBar.updateStatus(); } - haus.nightmare.j3270.config.Settings.setCodePage(cpId); + Settings.setCodePage(cpId); } private void changeGraphicsMode(GraphicsMode mode) { @@ -715,7 +720,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate client.getDataStreamProcessor().getQueryReplyBuilder().setGraphicsMode(mode); terminalPanel.repaint(); } - haus.nightmare.j3270.config.Settings.setGraphicsMode(mode); + Settings.setGraphicsMode(mode); } private void printScreen() { @@ -814,15 +819,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate } ConnectDialog dialog = new ConnectDialog(this); - String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : haus.nightmare.j3270.config.Settings.getAutoConnectHost(); - int initialPort = lastPort > 0 ? lastPort : haus.nightmare.j3270.config.Settings.getAutoConnectPort(); + String initialHost = lastHost != null && !lastHost.isEmpty() ? lastHost : Settings.getAutoConnectHost(); + int initialPort = lastPort > 0 ? lastPort : Settings.getAutoConnectPort(); dialog.setInitialHost(initialHost); dialog.setInitialPort(initialPort); - dialog.setInitialTls(haus.nightmare.j3270.config.Settings.getAutoConnectTls()); - dialog.setInitialVerifyCert(haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert()); - dialog.setInitialCodePage(haus.nightmare.j3270.config.Settings.getCodePage()); - dialog.setInitialGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode()); - dialog.setInitialHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType()); + dialog.setInitialTls(Settings.getAutoConnectTls()); + dialog.setInitialVerifyCert(Settings.getAutoConnectVerifyCert()); + dialog.setInitialCodePage(Settings.getCodePage()); + dialog.setInitialGraphicsMode(Settings.getGraphicsMode()); + dialog.setInitialHostType(Settings.getDefaultHostType()); dialog.setVisible(true); if (dialog.isConfirmed()) { @@ -835,7 +840,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate public void setConnectionHostType(haus.nightmare.lib3270j.ft.FTConfig.HostType ht) { if (ht == null) return; - haus.nightmare.j3270.config.Settings.setDefaultHostType(ht.name()); + Settings.setDefaultHostType(ht.name()); if (client != null && client.getConfig() != null) { client.getConfig().setFtHostType(ht); String profileId = client.getConfig().getSavedProfileId(); @@ -857,7 +862,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (client != null && client.getConfig() != null) { currentHt = client.getConfig().getFtHostType(); } else { - String def = haus.nightmare.j3270.config.Settings.getDefaultHostType(); + String def = Settings.getDefaultHostType(); try { if (def != null) currentHt = haus.nightmare.lib3270j.ft.FTConfig.HostType.valueOf(def.trim().toUpperCase()); } catch (Exception ignored) {} @@ -905,6 +910,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (refreshTimer != null) { refreshTimer.stop(); } + if (findDialog != null) { + findDialog.dispose(); + findDialog = null; + } + if (scriptDialog != null) { + scriptDialog.dispose(); + scriptDialog = null; + } + if (fieldInspectorDialog != null) { + fieldInspectorDialog.dispose(); + fieldInspectorDialog = null; + } if (printerSessionDialog != null) { printerSessionDialog.dispose(); } @@ -985,6 +1002,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate return statusBar; } + public FindDialog getFindDialog() { + return findDialog; + } + + public ScriptDialog getScriptDialog() { + return scriptDialog; + } + + public FieldInspectorDialog getFieldInspectorDialog() { + return fieldInspectorDialog; + } + public void connect(ConnectionConfig config) { connect(config, true); } @@ -1004,19 +1033,19 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate lastHost = config.getHost(); lastPort = config.getPort(); - haus.nightmare.j3270.config.Settings.setAutoConnectHost(config.getHost()); - haus.nightmare.j3270.config.Settings.setAutoConnectPort(config.getPort()); - haus.nightmare.j3270.config.Settings.setAutoConnectTls(config.isUseTls()); - haus.nightmare.j3270.config.Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert()); - haus.nightmare.j3270.config.Settings.setCodePage(config.getCodePage()); + Settings.setAutoConnectHost(config.getHost()); + Settings.setAutoConnectPort(config.getPort()); + Settings.setAutoConnectTls(config.isUseTls()); + Settings.setAutoConnectVerifyCert(config.isTlsVerifyCert()); + Settings.setCodePage(config.getCodePage()); if (config.getHostType() != null) { - haus.nightmare.j3270.config.Settings.setDefaultHostType(config.getHostType()); + Settings.setDefaultHostType(config.getHostType()); } if (FileTransferDialog.getLastTransferState() != null && config.isSavedConnection()) { FileTransferDialog.getLastTransferState().hostType = config.getFtHostType(); } if (config.getGraphicsMode() != null) { - haus.nightmare.j3270.config.Settings.setGraphicsMode(config.getGraphicsMode()); + Settings.setGraphicsMode(config.getGraphicsMode()); } Telnet3270Client oldClient = client; @@ -1045,6 +1074,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate terminalPanel.setClient(client); statusBar.setClient(client, terminalPanel); + if (findDialog != null) { + findDialog.setClient(client); + } + if (scriptDialog != null) { + scriptDialog.setClient(client); + } + if (fieldInspectorDialog != null) { + fieldInspectorDialog.setClient(client); + } String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : ""; setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator); @@ -1101,6 +1139,15 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate client = null; c.removeConnectionListener(this); c.removeScreenUpdateListener(this); + if (findDialog != null) { + findDialog.setClient(null); + } + if (scriptDialog != null) { + scriptDialog.setClient(null); + } + if (fieldInspectorDialog != null) { + fieldInspectorDialog.setClient(null); + } new Thread(() -> { try { @@ -1142,7 +1189,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (statusBar != null) { statusBar.setVisible(visible); } - haus.nightmare.j3270.config.Settings.setStatusBarVisible(visible); + Settings.setStatusBarVisible(visible); if (statusBarItem != null) { statusBarItem.setSelected(visible); } @@ -1203,6 +1250,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (screenUpdatePending.compareAndSet(false, true)) { SwingUtilities.invokeLater(() -> { screenUpdatePending.set(false); + terminalPanel.onScreenUpdated(); if (fileTransfer != null) { fileTransfer.onScreenUpdated(); } @@ -1224,6 +1272,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate @Override public void onScreenSizeChanged(int rows, int cols) { SwingUtilities.invokeLater(() -> { + terminalPanel.onScreenSizeChanged(rows, cols); terminalPanel.revalidate(); terminalPanel.repaint(); statusBar.updateStatus(); @@ -1504,7 +1553,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (configFile != null) { try { - haus.nightmare.j3270.config.Settings.loadFromIniFile(configFile); + Settings.loadFromIniFile(configFile); } catch (Exception e) { System.err.println("Failed to load config file: " + configFile); System.err.println(e.getMessage()); @@ -1517,8 +1566,8 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate } catch (Exception e) { log.fine("Could not set system look and feel"); } - ThemeManager.applyUIManagerDefaults(haus.nightmare.j3270.config.Settings.getJavaUiTheme()); - ThemeManager.setTheme(haus.nightmare.j3270.config.Settings.getJavaUiTheme()); + ThemeManager.applyUIManagerDefaults(Settings.getJavaUiTheme()); + ThemeManager.setTheme(Settings.getJavaUiTheme()); System.setProperty("apple.laf.useScreenMenuBar", "true"); System.setProperty("apple.awt.application.name", "j3270"); @@ -1590,12 +1639,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (finalAutoSysUnlock != null) { config.setAutoSysUnlock(finalAutoSysUnlock); } else { - config.setAutoSysUnlock(haus.nightmare.j3270.config.Settings.getAutoSysUnlock()); + config.setAutoSysUnlock(Settings.getAutoSysUnlock()); } if (finalGraphicsMode != null) { config.setGraphicsMode(finalGraphicsMode); } else { - config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode()); + config.setGraphicsMode(Settings.getGraphicsMode()); } if (finalProxyUrl != null) { applyProxyUrl(config, finalProxyUrl); @@ -1603,11 +1652,11 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (finalHostType != null) { config.setHostType(finalHostType); } else { - config.setHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType()); + config.setHostType(Settings.getDefaultHostType()); } app.connect(config); } else { - haus.nightmare.j3270.config.Settings.StartupBehavior behavior = haus.nightmare.j3270.config.Settings + Settings.StartupBehavior behavior = Settings .getStartupBehavior(); switch (behavior) { case SHOW_CONNECT: @@ -1626,22 +1675,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (finalProxyUrl != null) applyProxyUrl(config, finalProxyUrl); app.connect(config); } else { - String host = haus.nightmare.j3270.config.Settings.getAutoConnectHost(); - int port = haus.nightmare.j3270.config.Settings.getAutoConnectPort(); - boolean tls = haus.nightmare.j3270.config.Settings.getAutoConnectTls(); - boolean verify = haus.nightmare.j3270.config.Settings.getAutoConnectVerifyCert(); - boolean tn3270e = haus.nightmare.j3270.config.Settings.getAutoConnectTn3270e(); + String host = Settings.getAutoConnectHost(); + int port = Settings.getAutoConnectPort(); + boolean tls = Settings.getAutoConnectTls(); + boolean verify = Settings.getAutoConnectVerifyCert(); + boolean tn3270e = Settings.getAutoConnectTn3270e(); if (host != null && !host.isEmpty()) { ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4, tls); config.setTlsVerifyCert(verify); config.setTn3270eEnabled(finalTn3270e != null ? finalTn3270e : tn3270e); - config.setAutoReconnect(haus.nightmare.j3270.config.Settings.getAutoConnectAutoReconnect()); - config.setReconnectMaxRetries(haus.nightmare.j3270.config.Settings.getAutoConnectReconnectMaxRetries()); - config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : haus.nightmare.j3270.config.Settings.getAutoSysUnlock()); + config.setAutoReconnect(Settings.getAutoConnectAutoReconnect()); + config.setReconnectMaxRetries(Settings.getAutoConnectReconnectMaxRetries()); + config.setAutoSysUnlock(finalAutoSysUnlock != null ? finalAutoSysUnlock : Settings.getAutoSysUnlock()); if (finalGraphicsMode != null) { config.setGraphicsMode(finalGraphicsMode); } else { - config.setGraphicsMode(haus.nightmare.j3270.config.Settings.getGraphicsMode()); + config.setGraphicsMode(Settings.getGraphicsMode()); } if (finalProxyUrl != null) { applyProxyUrl(config, finalProxyUrl); @@ -1649,7 +1698,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate if (finalHostType != null) { config.setHostType(finalHostType); } else { - config.setHostType(haus.nightmare.j3270.config.Settings.getDefaultHostType()); + config.setHostType(Settings.getDefaultHostType()); } app.connect(config); } else { 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 497651a..6718398 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/FieldInspectorDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/FieldInspectorDialog.java @@ -21,7 +21,7 @@ import java.util.List; */ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListener { - private final Telnet3270Client client; + private Telnet3270Client client; private final TerminalPanel terminalPanel; private DefaultTableModel tableModel; @@ -46,13 +46,39 @@ public class FieldInspectorDialog extends JDialog implements ScreenUpdateListene addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { - if (client != null) { - // Screen update listener removed on close + if (FieldInspectorDialog.this.client != null) { + FieldInspectorDialog.this.client.removeScreenUpdateListener(FieldInspectorDialog.this); } } }); } + public void setClient(Telnet3270Client newClient) { + if (this.client != null) { + this.client.removeScreenUpdateListener(this); + } + this.client = newClient; + if (this.client != null) { + this.client.addScreenUpdateListener(this); + refreshFields(); + } else if (tableModel != null && countLabel != null) { + tableModel.setRowCount(0); + countLabel.setText("Not connected"); + } + } + + public Telnet3270Client getClient() { + return this.client; + } + + @Override + public void dispose() { + if (client != null) { + client.removeScreenUpdateListener(this); + } + super.dispose(); + } + private void buildUI() { JPanel mainPanel = new JPanel(new BorderLayout(8, 8)); mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); 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 ade58d6..758514e 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/FindDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/FindDialog.java @@ -15,7 +15,7 @@ import java.awt.event.KeyEvent; */ public class FindDialog extends JDialog { - private final Telnet3270Client client; + private Telnet3270Client client; private final TerminalPanel terminalPanel; private JTextField searchField; @@ -40,6 +40,24 @@ public class FindDialog extends JDialog { setResizable(false); } + public void setClient(Telnet3270Client client) { + this.client = client; + } + + public Telnet3270Client getClient() { + return this.client; + } + + public void setSearchText(String text) { + if (searchField != null) { + searchField.setText(text); + } + } + + public String getSearchText() { + return searchField != null ? searchField.getText() : ""; + } + private void buildUI() { JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/HotspotEngine.java b/j3270/src/main/java/haus/nightmare/j3270/ui/HotspotEngine.java index 59bb321..29653a7 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/HotspotEngine.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/HotspotEngine.java @@ -143,6 +143,9 @@ public class HotspotEngine { private int cachedRows = 0; private int cachedCols = 0; private List cachedHotspots = Collections.emptyList(); + private ScreenBuffer lastScreenBuffer; + private long lastSnapshotVersion = -1; + private long scanCount = 0; public HotspotEngine() { } @@ -174,9 +177,19 @@ public class HotspotEngine { invalidateCache(); } - public void invalidateCache() { + public synchronized void invalidateCache() { cachedScreenText = ""; cachedHotspots = Collections.emptyList(); + lastScreenBuffer = null; + lastSnapshotVersion = -1; + } + + public synchronized void markDirty() { + invalidateCache(); + } + + public synchronized long getScanCount() { + return scanCount; } /** @@ -186,6 +199,13 @@ public class HotspotEngine { if (sb == null) return Collections.emptyList(); int rows = sb.getDisplayRows(); int cols = sb.getDisplayCols(); + long version = sb.getSnapshotVersion(); + + if (sb == lastScreenBuffer && version == lastSnapshotVersion && rows == cachedRows && cols == cachedCols) { + return cachedHotspots; + } + + scanCount++; // Build plain text representation StringBuilder fullText = new StringBuilder(rows * (cols + 1)); @@ -204,7 +224,10 @@ public class HotspotEngine { } } - return scanText(fullText.toString(), rows, cols); + List results = scanText(fullText.toString(), rows, cols); + lastScreenBuffer = sb; + lastSnapshotVersion = version; + return results; } /** 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 e08a5e2..17f88f8 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/ScriptDialog.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/ScriptDialog.java @@ -15,7 +15,7 @@ import java.nio.charset.StandardCharsets; */ public class ScriptDialog extends JDialog { - private final Telnet3270Client client; + private Telnet3270Client client; private final TerminalPanel terminalPanel; private JTextArea scriptArea; @@ -32,6 +32,24 @@ public class ScriptDialog extends JDialog { setLocationRelativeTo(parent); } + public void setClient(Telnet3270Client client) { + this.client = client; + } + + public Telnet3270Client getClient() { + return this.client; + } + + public void setScriptText(String text) { + if (scriptArea != null) { + scriptArea.setText(text); + } + } + + public String getScriptText() { + return scriptArea != null ? scriptArea.getText() : ""; + } + private void buildUI() { JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); mainPanel.setBorder(new EmptyBorder(12, 14, 12, 14)); 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 3ac69b3..ac0da82 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -64,6 +64,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I CHAR_STRINGS[i] = String.valueOf((char) i); } } + // Reusable single-character buffer for drawing non-ASCII cells (Phase 27.4 / T1-A) + private final char[] charScratch = new char[1]; public enum CursorStyle { BLOCK, UNDERLINE @@ -110,10 +112,14 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I // ========== Phase 25: Hotspots & Mouse Wheel ========== private final HotspotEngine hotspotEngine = new HotspotEngine(); private HotspotEngine.Hotspot hoveredHotspot = null; + private static final Color HOTSPOT_HOVER_COLOR = new Color(0, 220, 255); + private static final Stroke HOTSPOT_HOVER_STROKE = new BasicStroke(2.0f); private boolean hotspotsEnabled = haus.nightmare.j3270.config.Settings.getHotspotsEnabled(); private boolean mouseWheelEnabled = haus.nightmare.j3270.config.Settings.getMouseWheelEnabled(); private String mouseWheelUpAction = haus.nightmare.j3270.config.Settings.getMouseWheelUpKey(); private String mouseWheelDownAction = haus.nightmare.j3270.config.Settings.getMouseWheelDownKey(); + private boolean hotspotsDirty = true; + private boolean[] selectedCellsScratch = new boolean[0]; // ========== Mode and Listener State ========== private boolean insertMode = false; @@ -400,7 +406,10 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I int col = (e.getX() - ox) / cellWidth; int row = (e.getY() - oy) / cellHeight; if (row >= 0 && row < rows && col >= 0 && col < cols) { - hotspotEngine.scan(sb); + if (hotspotsDirty) { + hotspotEngine.scan(sb); + hotspotsDirty = false; + } HotspotEngine.Hotspot h = hotspotEngine.findHotspot(row, col); if (h != null) { if (hoveredHotspot != h) { @@ -515,6 +524,10 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I // Phase 25.2: Interactive Hotspot click execution (URLs, PF keys, choices) if (hotspotsEnabled && client.getConnectionState().isFullSession()) { + if (hotspotsDirty) { + hotspotEngine.scan(sb); + hotspotsDirty = false; + } HotspotEngine.Hotspot h = hotspotEngine.findHotspot(row, col); if (h != null) { if (h.getType() == HotspotEngine.HotspotType.URL) { @@ -1054,7 +1067,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I popup.show(this, e.getX(), e.getY()); } - private boolean isCellSelected(int row, int col) { + public boolean isCellSelected(int row, int col) { if (!hasSelection()) return false; int r1 = Math.min(selectionStartRow, selectionEndRow); @@ -1066,7 +1079,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I return row >= r1 && row <= r2 && col >= c1 && col <= c2; } else { int cols = 80; - if (client != null) cols = client.getScreenBuffer().getCols(); + if (client != null && client.getScreenBuffer() != null) cols = client.getScreenBuffer().getDisplayCols(); int addr = row * cols + col; int startAddr, endAddr; if (selectionStartRow < selectionEndRow || @@ -1081,6 +1094,51 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I } } + public void precomputeSelectionScratch(int rows, int cols) { + int totalCells = rows * cols; + if (selectedCellsScratch == null || selectedCellsScratch.length < totalCells) { + selectedCellsScratch = new boolean[totalCells]; + } + java.util.Arrays.fill(selectedCellsScratch, 0, totalCells, false); + if (hasSelection()) { + int r1 = Math.min(selectionStartRow, selectionEndRow); + int r2 = Math.max(selectionStartRow, selectionEndRow); + if (blockSelectMode || trimRectSizingHandles) { + int c1 = Math.min(selectionStartCol, selectionEndCol); + int c2 = Math.max(selectionStartCol, selectionEndCol); + int minR = Math.max(0, r1); + int maxR = Math.min(rows - 1, r2); + int minC = Math.max(0, c1); + int maxC = Math.min(cols - 1, c2); + if (minC <= maxC) { + for (int r = minR; r <= maxR; r++) { + int rowBase = r * cols; + java.util.Arrays.fill(selectedCellsScratch, rowBase + minC, rowBase + maxC + 1, true); + } + } + } else { + int startAddr, endAddr; + if (selectionStartRow < selectionEndRow || + (selectionStartRow == selectionEndRow && selectionStartCol <= selectionEndCol)) { + startAddr = selectionStartRow * cols + selectionStartCol; + endAddr = selectionEndRow * cols + selectionEndCol; + } else { + startAddr = selectionEndRow * cols + selectionEndCol; + endAddr = selectionStartRow * cols + selectionStartCol; + } + int s = Math.max(0, startAddr); + int e = Math.min(totalCells - 1, endAddr); + if (s <= e) { + java.util.Arrays.fill(selectedCellsScratch, s, e + 1, true); + } + } + } +} + + public boolean[] getSelectedCellsScratch() { + return selectedCellsScratch; + } + public void packForModel(int modelRows, int modelCols) { if (isDisplayable() && !SwingUtilities.isEventDispatchThread()) { @@ -1687,6 +1745,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I } public void applyModeSettings() { + hotspotsDirty = true; if (client != null) { ScreenBuffer sb = client.getScreenBuffer(); if (sb != null) { @@ -1723,6 +1782,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I } public void refreshScreen() { + hotspotsDirty = true; if (client != null) { client.getScreenBuffer().updateDisplaySnapshot(); } @@ -1740,6 +1800,26 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I } } + public void onScreenUpdated() { + hotspotsDirty = true; + } + + public void onScreenSizeChanged() { + hotspotsDirty = true; + } + + public void onScreenSizeChanged(int rows, int cols) { + hotspotsDirty = true; + } + + public boolean isHotspotsDirty() { + return hotspotsDirty; + } + + public void setHotspotsDirty(boolean hotspotsDirty) { + this.hotspotsDirty = hotspotsDirty; + } + private void setupFont() { currentFontSize = haus.nightmare.j3270.config.Settings.getFontSize(); String fontFamily = haus.nightmare.j3270.config.Settings.getFontFamily(); @@ -1880,6 +1960,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I public void setClient(Telnet3270Client client) { this.client = client; + hotspotsDirty = true; if (client != null) { setupGraphicsPlaneRenderer(); updateCellSize(); @@ -2076,6 +2157,9 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I g2.fillRect(cx, oy, cellWidth, gridH); } + // Phase 28.2 / T2-B: Precompute selection scratch lookup once per frame + precomputeSelectionScratch(rows, cols); + // Layer 3: Character / Text Plane byte currentFA = 0; ExtendedAttribute currentFieldEa = null; @@ -2097,7 +2181,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I if (ea.isFieldAttribute()) { currentFA = ea.fa; currentFieldEa = ea; - if (isCellSelected(row, col)) { + if (selectedCellsScratch[baddr]) { g2.setColor(SELECTION_COLOR); g2.fillRect(x, y, cellWidth, cellHeight); } @@ -2142,7 +2226,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I // Field Outlining (0xC2 / TY_FOUTLINE) drawOutlining(g2, ea, currentFieldEa, baddr, row, col, x, y, cellWidth, cellHeight, rows, cols, sb, fgColor); - if (isCellSelected(row, col)) { + if (selectedCellsScratch[baddr]) { g2.setColor(SELECTION_COLOR); g2.fillRect(x, y, cellWidth, cellHeight); } @@ -2194,7 +2278,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I if (ch < 128) { g2.drawString(CHAR_STRINGS[ch], x, textY); } else { - g2.drawString(String.valueOf(ch), x, textY); + charScratch[0] = ch; + g2.drawChars(charScratch, 0, 1, x, textY); } } } @@ -2215,7 +2300,7 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I } // Draw selection highlight (40% alpha blend over cell) - if (isCellSelected(row, col)) { + if (selectedCellsScratch[baddr]) { g2.setColor(SELECTION_COLOR); g2.fillRect(x, y, cellWidth, cellHeight); } @@ -2261,8 +2346,8 @@ public class TerminalPanel extends JPanel implements java.awt.print.Printable, I if (hotspotsEnabled && hoveredHotspot != null) { Color oldColor = g2.getColor(); Stroke oldStroke = g2.getStroke(); - g2.setColor(new Color(0, 220, 255)); // Bright cyan hotspot underline - g2.setStroke(new BasicStroke(2.0f)); + g2.setColor(HOTSPOT_HOVER_COLOR); // Bright cyan hotspot underline + g2.setStroke(HOTSPOT_HOVER_STROKE); int hr0 = hoveredHotspot.getStartRow(); int hr1 = hoveredHotspot.getEndRow(); diff --git a/lib3270j/build.gradle b/lib3270j/build.gradle index d822073..00f2042 100644 --- a/lib3270j/build.gradle +++ b/lib3270j/build.gradle @@ -1,5 +1,11 @@ plugins { id 'java-library' + id 'maven-publish' +} + +java { + withSourcesJar() + withJavadocJar() } dependencies { @@ -17,3 +23,39 @@ jar { 'Implementation-Version': archiveVersion } } + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + groupId = 'haus.nightmare.j3270' + artifactId = 'lib3270j' + version = project.version + + pom { + name = 'lib3270j' + description = 'Pure Java IBM 3270 terminal emulation and protocol library with Host On-Demand (HOD) parity' + url = 'https://github.com/rudi/j3270' + licenses { + license { + name = 'The Unlicense' + url = 'https://unlicense.org/' + distribution = 'repo' + } + } + developers { + developer { + id = 'rudi' + name = 'Rudi' + } + } + scm { + connection = 'scm:git:git://github.com/rudi/j3270.git' + developerConnection = 'scm:git:ssh://github.com/rudi/j3270.git' + url = 'https://github.com/rudi/j3270' + } + } + } + } +} + diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/FastByteBuffer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/FastByteBuffer.java index cfad999..752c9e8 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/FastByteBuffer.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/FastByteBuffer.java @@ -8,6 +8,12 @@ import java.util.Arrays; * Reusable, resizable byte buffer that provides zero-copy access to its internal * array and supports ByteBuffer slicing to minimize heap allocations during * network stream processing. + *

+ * Threading Contract: This class is NOT thread-safe and is designed for + * high-throughput single-threaded or thread-confined stream accumulation (such as + * Telnet network receiver loops matching IBM Host On-Demand {@code sArray} and + * {@code sb_buffer}). External synchronization must be used if instances are shared + * across threads. */ public class FastByteBuffer extends OutputStream { @@ -31,13 +37,13 @@ public class FastByteBuffer extends OutputStream { } @Override - public synchronized void write(int b) { + public void write(int b) { ensureCapacity(count + 1); buf[count++] = (byte) b; } @Override - public synchronized void write(byte[] b, int off, int len) { + public void write(byte[] b, int off, int len) { if (b == null || len <= 0) return; ensureCapacity(count + len); System.arraycopy(b, off, buf, count, len); @@ -48,29 +54,29 @@ public class FastByteBuffer extends OutputStream { * Direct reference to internal buffer array. * Use {@link #size()} to determine active length. */ - public synchronized byte[] buffer() { + public byte[] buffer() { return buf; } - public synchronized int size() { + public int size() { return count; } - public synchronized void reset() { + public void reset() { count = 0; } /** * Creates a read-only ByteBuffer view wrapping active bytes without copying. */ - public synchronized ByteBuffer asByteBuffer() { + public ByteBuffer asByteBuffer() { return ByteBuffer.wrap(buf, 0, count).asReadOnlyBuffer(); } /** * Creates a read-only ByteBuffer slice for a sub-range without copying. */ - public synchronized ByteBuffer slice(int offset, int length) { + public ByteBuffer slice(int offset, int length) { if (offset < 0 || length < 0 || offset + length > count) { throw new IndexOutOfBoundsException("offset=" + offset + " length=" + length + " size=" + count); } @@ -80,7 +86,7 @@ public class FastByteBuffer extends OutputStream { /** * Produces a copied byte array if an isolated copy is explicitly required. */ - public synchronized byte[] toByteArray() { + public byte[] toByteArray() { return Arrays.copyOf(buf, count); } } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java index b2f6f9c..f847ebb 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ExtendedAttribute.java @@ -110,6 +110,14 @@ public class ExtendedAttribute { */ public char ucs4; + public ExtendedAttribute() {} + + public ExtendedAttribute(ExtendedAttribute other) { + if (other != null) { + copyFrom(other); + } + } + /** Clear all attributes. */ public void clear() { ec = 0; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java index 6f7acd3..e207adc 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/screen/ScreenBuffer.java @@ -197,48 +197,99 @@ public class ScreenBuffer { return buffer[addr]; } + /** + * Immutable snapshot container for tear-free, lock-free screen display. + * Published atomically to UI thread / EDT via volatile reference release. + */ + public static final class DisplaySnapshot { + public final ExtendedAttribute[] cells; + public final int rows; + public final int cols; + public final int cursorAddress; + public final long version; + + public DisplaySnapshot(ExtendedAttribute[] cells, int rows, int cols, int cursorAddress) { + this(cells, rows, cols, cursorAddress, 0L); + } + + public DisplaySnapshot(ExtendedAttribute[] cells, int rows, int cols, int cursorAddress, long version) { + this.cells = cells; + this.rows = rows; + this.cols = cols; + this.cursorAddress = cursorAddress; + this.version = version; + } + + public ExtendedAttribute getCell(int addr) { + if (cells == null || addr < 0 || addr >= cells.length) { + return null; + } + return cells[addr]; + } + + public int getRows() { return rows; } + public int getCols() { return cols; } + public int getCursorAddress() { return cursorAddress; } + public long getVersion() { return version; } + } + + private volatile DisplaySnapshot currentSnapshot; private ExtendedAttribute[] displaySnapshot; private int displayRows; private int displayCols; private int displayCursorAddress; + private long snapshotVersion = 0; /** - * Atomically creates a snapshot of the current presentation buffer for tear-free rendering. - * Takes ~2 microseconds and eliminates mutual thread contention with the UI thread. + * Atomically creates an immutable snapshot of the current presentation buffer for tear-free rendering. + * Published via a volatile release fence, eliminating mutual monitor contention with the UI paint loop. */ public synchronized void updateDisplaySnapshot() { int size = rows * cols; - if (displaySnapshot == null || displaySnapshot.length < size) { - displaySnapshot = new ExtendedAttribute[size]; - for (int i = 0; i < size; i++) { - displaySnapshot[i] = new ExtendedAttribute(); - } - } + ExtendedAttribute[] snapshotCells = new ExtendedAttribute[size]; for (int i = 0; i < size; i++) { - displaySnapshot[i].copyFrom(buffer[i]); + ExtendedAttribute ea = new ExtendedAttribute(); + ea.copyFrom(buffer[i]); + snapshotCells[i] = ea; } this.displayRows = rows; this.displayCols = cols; this.displayCursorAddress = cursorAddress; + this.snapshotVersion++; + this.displaySnapshot = snapshotCells; + this.currentSnapshot = new DisplaySnapshot(snapshotCells, rows, cols, cursorAddress, snapshotVersion); } - public synchronized ExtendedAttribute getDisplayCell(int addr) { - if (displaySnapshot == null || addr < 0 || addr >= displayRows * displayCols) { + public DisplaySnapshot getDisplaySnapshot() { + return currentSnapshot; + } + + public long getSnapshotVersion() { + DisplaySnapshot snap = this.currentSnapshot; + return snap != null ? snap.version : snapshotVersion; + } + + public ExtendedAttribute getDisplayCell(int addr) { + DisplaySnapshot snap = this.currentSnapshot; + if (snap == null || addr < 0 || addr >= snap.cells.length) { return getCell(addr); } - return displaySnapshot[addr]; + return snap.cells[addr]; } - public synchronized int getDisplayRows() { - return displayRows > 0 ? displayRows : rows; + public int getDisplayRows() { + DisplaySnapshot snap = this.currentSnapshot; + return (snap != null && snap.rows > 0) ? snap.rows : (displayRows > 0 ? displayRows : rows); } - public synchronized int getDisplayCols() { - return displayCols > 0 ? displayCols : cols; + public int getDisplayCols() { + DisplaySnapshot snap = this.currentSnapshot; + return (snap != null && snap.cols > 0) ? snap.cols : (displayCols > 0 ? displayCols : cols); } - public synchronized int getDisplayCursorAddress() { - return displayRows > 0 ? displayCursorAddress : cursorAddress; + public int getDisplayCursorAddress() { + DisplaySnapshot snap = this.currentSnapshot; + return (snap != null && snap.rows > 0) ? snap.cursorAddress : (displayRows > 0 ? displayCursorAddress : cursorAddress); } // ========== Dimension accessors ========== @@ -346,6 +397,10 @@ public class ScreenBuffer { oldAddr = this.cursorAddress; this.cursorAddress = addr; this.displayCursorAddress = addr; + DisplaySnapshot snap = this.currentSnapshot; + if (snap != null) { + this.currentSnapshot = new DisplaySnapshot(snap.cells, snap.rows, snap.cols, addr, snap.version); + } } if (oldAddr != addr) { notifyCursorMoved(oldAddr, addr); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java index 1ac0c4a..1f4663e 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/telnet/TelnetFSM.java @@ -49,6 +49,9 @@ public class TelnetFSM { // 3270 input buffer (accumulated between telnet framing) private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768); + // Reusable single-byte scratch buffer for plain NVT data dispatch (Phase 27.5 / T1-C) + private final byte[] nvtSingleByteScratch = new byte[1]; + // Sub-negotiation buffer private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096); @@ -324,7 +327,8 @@ public class TelnetFSM { boolean isPlainNvt = (connectionState == ConnectionState.CONNECTED_NVT || connectionState == ConnectionState.CONNECTED_NVT_CHAR) && !(hisOpts[TELOPT_BINARY] && hisOpts[TELOPT_EOR]); if (isPlainNvt) { - nvtProcessor.processNVTData(new byte[] { (byte) c }, 0, 1); + nvtSingleByteScratch[0] = (byte) c; + nvtProcessor.processNVTData(nvtSingleByteScratch, 0, 1); return; } diff --git a/setup.ps1 b/setup.ps1 index 53d0df9..7b885b1 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -413,11 +413,19 @@ if (Test-Path $libResDir) { } # ------------------------------------------------------------------------------ -# 7. Package Standalone Executable JAR +# 7. Package Standalone JARs # ------------------------------------------------------------------------------ +Write-LogInfo "Packaging standalone lib3270j library JAR..." +$libManifestFile = Join-Path $BuildDir "LIB_MANIFEST.MF" +$libManifestText = "Manifest-Version: 1.0`r`nImplementation-Title: lib3270j`r`nImplementation-Version: 1.2.4`r`nCreated-By: j3270 setup.ps1`r`n`r`n" +[System.IO.File]::WriteAllText($libManifestFile, $libManifestText) + +$libJarFile = Join-Path $BuildDir "lib3270j.jar" +& $detectedJdk.Jar cvfm $libJarFile $libManifestFile -C $LibBuildDir . | Out-Null + Write-LogInfo "Packaging standalone executable JAR..." $manifestFile = Join-Path $BuildDir "MANIFEST.MF" -$manifestText = "Manifest-Version: 1.0`r`nMain-Class: haus.nightmare.j3270.J3270App`r`nImplementation-Title: j3270`r`nImplementation-Version: 0.1.0`r`nCreated-By: j3270 setup.ps1`r`n`r`n" +$manifestText = "Manifest-Version: 1.0`r`nMain-Class: haus.nightmare.j3270.J3270App`r`nImplementation-Title: j3270`r`nImplementation-Version: 1.2.4`r`nCreated-By: j3270 setup.ps1`r`n`r`n" [System.IO.File]::WriteAllText($manifestFile, $manifestText) $jarFile = Join-Path $BuildDir "j3270.jar" @@ -428,10 +436,13 @@ if (-not (Test-Path $jarFile)) { exit 1 } +$libJarItem = Get-Item $libJarFile +$libJarSizeKB = [math]::Round($libJarItem.Length / 1KB, 1) $jarItem = Get-Item $jarFile $jarSizeKB = [math]::Round($jarItem.Length / 1KB, 1) Write-Host "`n=== Build Successful ===`n" -ForegroundColor Green +Write-LogSuccess "Library JAR created: $libJarFile (${libJarSizeKB} KB)" Write-LogSuccess "Executable JAR created: $jarFile (${jarSizeKB} KB)" Write-Host @" diff --git a/setup.sh b/setup.sh index 32e3f9c..3aef583 100755 --- a/setup.sh +++ b/setup.sh @@ -369,15 +369,28 @@ if [ -d "$SCRIPT_DIR/lib3270j/src/main/resources" ]; then fi # ------------------------------------------------------------------------------ -# 5. Package Standalone Executable JAR +# 5. Package Standalone JARs # ------------------------------------------------------------------------------ +log_info "Packaging standalone lib3270j library JAR..." + +cat << 'EOF' > "$BUILD_DIR/LIB_MANIFEST.MF" +Manifest-Version: 1.0 +Implementation-Title: lib3270j +Implementation-Version: 1.2.4 +Created-By: j3270 setup.sh + +EOF + +"$JAR_BIN" cvfm "$BUILD_DIR/lib3270j.jar" "$BUILD_DIR/LIB_MANIFEST.MF" \ + -C "$BUILD_DIR/lib3270j" . > /dev/null + log_info "Packaging standalone executable JAR..." cat << 'EOF' > "$BUILD_DIR/MANIFEST.MF" Manifest-Version: 1.0 Main-Class: haus.nightmare.j3270.J3270App Implementation-Title: j3270 -Implementation-Version: 0.1.0 +Implementation-Version: 1.2.4 Created-By: j3270 setup.sh EOF @@ -391,8 +404,10 @@ if [ ! -f "$BUILD_DIR/j3270.jar" ]; then exit 1 fi -JAR_SIZE=$(ls -lh "$BUILD_DIR/j3270.jar" | awk '{print $5}') +LIB_JAR_SIZE=$(du -h "$BUILD_DIR/lib3270j.jar" | awk '{print $1}') +JAR_SIZE=$(du -h "$BUILD_DIR/j3270.jar" | awk '{print $1}') printf "\n%b=== Build Successful ===%b\n" "$BOLD" "$RESET" +log_success "Library JAR: $BUILD_DIR/lib3270j.jar ($LIB_JAR_SIZE)" log_success "Executable JAR: $BUILD_DIR/j3270.jar ($JAR_SIZE)" printf "\n%bTo run j3270:%b\n" "$BOLD" "$RESET"