From 27b7ad247cfee1ac2e05248d52da1dc4b7628689 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:56:03 +0300 Subject: [PATCH 001/257] Compatibility layers: one relocator for every layer, shared JDK classes and test harness The Android layer's relocation core moves out of AndroidRemapper into ClassRelocator, driven by a Relocation per layer, so further layers (Swing, JavaFX) relocate in one composed pass. The JDK classes the device runtime lacks move to maven/compat-jdk and the headless test implementation to maven/compat-testing, both shared by every layer. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/android-compat.yml | 4 +- .github/workflows/pr.yml | 9 +- maven/android-compat/pom.xml | 15 + .../src/main/java/android/net/Uri.java | 4 +- .../android/animation/AnimatorMathTest.java | 2 +- .../animation/AutoCancelReleaseTest.java | 2 +- .../ObjectAnimatorSetPropertyTest.java | 2 +- .../ReverseRepeatedAnimatorTest.java | 2 +- .../android/app/ActivityBoundaryTest.java | 2 +- .../android/app/DialogDefaultTouchTest.java | 2 +- .../app/FragmentLauncherRecreationTest.java | 2 +- .../java/android/app/FragmentManagerTest.java | 2 +- .../android/app/FragmentViewStateTest.java | 2 +- .../java/android/app/PauseSelfFinishTest.java | 2 +- .../android/app/ResultAfterRelaunchTest.java | 2 +- .../android/app/ResumeConfigurationTest.java | 2 +- .../android/app/SupportFragmentsTest.java | 2 +- .../android/app/WindowKeepScreenOnTest.java | 4 +- .../content/BroadcastFilterMatchTest.java | 2 +- .../content/ContentResolverFileUriTest.java | 4 +- .../android/content/ContentValuesTest.java | 2 +- .../android/content/ContextFileNameTest.java | 2 +- .../content/IntentCloneFilterTest.java | 2 +- .../content/IntentResolveActivityTest.java | 2 +- .../java/android/database/CursorsTest.java | 2 +- .../database/sqlite/SQLiteDatabaseTest.java | 2 +- .../sqlite/SQLiteQueryBuilderTest.java | 2 +- .../graphics/BitmapCompressFormatTest.java | 4 +- .../graphics/BitmapDerivedDensityTest.java | 4 +- .../BitmapDisplayMetricsDensityTest.java | 2 +- .../graphics/BitmapFactoryBoundsTest.java | 4 +- .../BitmapFactoryHeaderBoundsTest.java | 4 +- .../graphics/BitmapFactoryMimeTypeTest.java | 2 +- .../BitmapFactoryUnscaledDensityTest.java | 4 +- .../graphics/BitmapImmutableWriteTest.java | 4 +- .../android/graphics/BitmapMatrixTest.java | 4 +- .../graphics/BitmapMutableSnapshotTest.java | 4 +- .../graphics/BitmapPixelBoundsTest.java | 4 +- .../graphics/CanvasBitmapPixelCacheTest.java | 4 +- .../android/graphics/CanvasClipPathTest.java | 4 +- .../CanvasClipRestoreAndClearTest.java | 4 +- .../graphics/CanvasDrawPaintShaderTest.java | 4 +- .../graphics/CanvasImmutableBitmapTest.java | 2 +- .../graphics/CanvasRecycledBitmapTest.java | 2 +- .../graphics/CanvasRotatedClipRectTest.java | 4 +- .../android/graphics/CanvasSaveLayerTest.java | 4 +- .../graphics/CanvasSpacedSurrogateTest.java | 4 +- .../graphics/CanvasTextScaleXTest.java | 4 +- .../graphics/LetterSpacingMeasureTest.java | 4 +- .../graphics/LetterSpacingWidthsTest.java | 2 +- .../BitmapDrawableTargetDensityTest.java | 4 +- .../drawable/BitmapDrawableTileModeTest.java | 4 +- .../GradientDrawableOrientationTest.java | 4 +- .../drawable/StateListTintModeTest.java | 2 +- .../drawable/WrapperChildStateTest.java | 2 +- .../java/android/net/UriFromFileTest.java | 4 +- .../android/net/UriNormalizeSchemeTest.java | 2 +- .../java/android/os/BundleDeepCopyTest.java | 2 +- .../android/os/BundleEmptyImmutableTest.java | 2 +- .../android/os/HandlerFrontOfQueueTest.java | 2 +- .../java/android/text/AllCapsSpansTest.java | 2 +- .../text/LengthFilterSurrogateTest.java | 2 +- .../text/TextUtilsCommaEllipsizeTest.java | 2 +- .../text/TextUtilsEllipsizeSpansTest.java | 2 +- .../android/text/TextUtilsSplitRegexTest.java | 2 +- .../view/AncestorChangeDispatchTest.java | 2 +- .../android/view/BackgroundTintModeTest.java | 2 +- .../view/DrawableLayoutDirectionTest.java | 2 +- .../view/HiddenViewLosesFocusTest.java | 2 +- .../java/android/view/IncludeThemeTest.java | 2 +- .../view/MergedIncludeFinishInflateTest.java | 2 +- .../android/view/RemovedTouchTargetTest.java | 2 +- .../view/TransformedChildTouchTest.java | 2 +- .../java/android/view/ViewCallbacksTest.java | 2 +- .../android/view/ViewStubReferenceTest.java | 2 +- .../java/android/view/ViewTreeScrollTest.java | 2 +- .../view/VisibleRectAndKeepScreenOnTest.java | 4 +- .../view/animation/AnimationCloneTest.java | 2 +- .../view/animation/AnimationTimingTest.java | 2 +- .../webkit/WebViewHistoryDistanceTest.java | 2 +- .../android/webkit/WebViewHistoryUrlTest.java | 2 +- .../AbsListViewSelectionAndStackTest.java | 2 +- .../DatePickerRangeClampNotifiesTest.java | 2 +- .../widget/DatePickerSavedStateTest.java | 2 +- .../widget/EditTextEditableWatcherTest.java | 2 +- .../android/widget/EditTextSelectionTest.java | 2 +- .../android/widget/ForegroundGravityTest.java | 2 +- .../widget/FormWidgetSavedStateTest.java | 2 +- .../widget/GridViewUniformSpacingTest.java | 2 +- .../widget/ImageViewClearColorModTest.java | 2 +- .../ImageViewMeasureInputsRelayoutTest.java | 2 +- .../widget/LinearLayoutDividersTest.java | 2 +- .../ListCheckedRowsAfterDataChangeTest.java | 2 +- .../widget/ListViewDragOffClickTest.java | 2 +- .../ProgressBarIndeterminateDrawableTest.java | 2 +- .../widget/ProgressBarRangeClampTest.java | 2 +- .../widget/ProgressDrawableOwnershipTest.java | 2 +- .../RadioGroupAndFilteredAdapterTest.java | 2 +- .../android/widget/RowStateNotSavedTest.java | 2 +- .../widget/ScrollViewChildMarginsTest.java | 2 +- .../SeekBarInScrollingContainerTest.java | 2 +- .../widget/SimpleAdapterBindingHooksTest.java | 2 +- .../widget/SimpleAdapterFilterTest.java | 2 +- .../widget/SpinnerSelectedRowIdTest.java | 2 +- .../widget/TextViewAppendSpansTest.java | 2 +- .../widget/TextViewDefaultAppearanceTest.java | 2 +- .../widget/TextViewEllipsizeModeTest.java | 2 +- .../widget/TextViewFilterAndWatcherTest.java | 2 +- .../widget/TextViewNormalSnapshotTest.java | 2 +- .../widget/TextViewRelativeDrawablesTest.java | 2 +- .../widget/TextViewSpannableBufferTest.java | 2 +- .../widget/TextViewTransitionTest.java | 2 +- .../widget/TimePickerNullLegacyTimeTest.java | 2 +- .../widget/ViewAnimatorRemovalTest.java | 2 +- .../ComponentActivitySaveLifecycleTest.java | 2 +- .../GalleryLauncherUnregisterTest.java | 4 +- .../GalleryResultAfterRecreateTest.java | 2 +- .../activity/OnBackPressedDispatcherTest.java | 2 +- .../activity/SynchronousResultPostedTest.java | 2 +- .../widget/ConstraintLayoutTest.java | 2 +- .../lifecycle/LifecycleRegistryTest.java | 2 +- .../java/androidx/lifecycle/LiveDataTest.java | 2 +- .../lifecycle/MediatorReactivationTest.java | 2 +- .../recyclerview/widget/DiffUtilTest.java | 2 +- .../recyclerview/widget/GridRowSizeTest.java | 2 +- .../runtime/DrawableForDensityTest.java | 4 +- .../androidcompat/runtime/FontCacheTest.java | 2 +- .../runtime/LocaleQualifierTest.java | 2 +- .../runtime/MenuXmlOnClickTest.java | 2 +- .../runtime/ResTableRoundTripTest.java | 2 +- .../ResourceSelectionRegressionTest.java | 2 +- .../runtime/ScriptQualifierTest.java | 2 +- .../runtime/ShareStreamTest.java | 2 +- .../SharedPreferencesApplyDeferredTest.java | 2 +- .../SharedPreferencesClearListenerTest.java | 2 +- .../SharedPreferencesCommitFailureTest.java | 2 +- .../SharedPreferencesEditOrderTest.java | 2 +- .../SharedPreferencesEditorReuseTest.java | 2 +- .../SharedPreferencesStringTypeTest.java | 2 +- .../SharedPreferencesUnchangedValueTest.java | 2 +- .../SharedPreferencesWeakListenerTest.java | 2 +- .../runtime/XmlPullParserTest.java | 2 +- .../testing/AndroidTestSupport.java | 1 + .../jdk/BufferedReaderMarkTest.java | 2 +- .../jdk/BufferedStreamsClosedTest.java | 2 +- .../jdk/BufferedWriterClosedTest.java | 2 +- .../jdk/FileCanonicalPathTest.java | 4 +- .../jdk/FileRenameFailedDeleteTest.java | 6 +- .../jdk/FileRenameOntoItselfTest.java | 6 +- .../jdk/FileTypeContractTest.java | 4 +- .../jdk/PrintWriterClosedTest.java | 2 +- .../material/MaterialComponentsTest.java | 2 +- .../com/codename1/maven/AndroidRemapper.java | 354 +++-------------- .../codename1/maven/BytecodeCompliance.java | 6 +- .../com/codename1/maven/ClassRelocator.java | 376 ++++++++++++++++++ .../com/codename1/maven/CompatLayers.java | 83 ++++ .../java/com/codename1/maven/Relocation.java | 150 +++++++ .../codename1/maven/AndroidRemapperTest.java | 6 +- .../gradle/tasks/RemapAndroidAction.java | 2 + .../com/codename1/maven/RemapAndroidMojo.java | 9 +- maven/compat-jdk/pom.xml | 73 ++++ maven/compat-jdk/spotbugs-exclude.xml | 27 ++ .../compat}/jdk/BufferedInputStream.java | 2 +- .../compat}/jdk/BufferedOutputStream.java | 2 +- .../codename1/compat}/jdk/BufferedReader.java | 2 +- .../codename1/compat}/jdk/BufferedWriter.java | 2 +- .../java/com/codename1/compat}/jdk/File.java | 2 +- .../com/codename1/compat}/jdk/FileFilter.java | 2 +- .../compat}/jdk/FileInputStream.java | 2 +- .../compat}/jdk/FileOutputStream.java | 2 +- .../com/codename1/compat}/jdk/FileReader.java | 2 +- .../com/codename1/compat}/jdk/FileWriter.java | 2 +- .../codename1/compat}/jdk/FilenameFilter.java | 2 +- .../compat}/jdk/FilterInputStream.java | 2 +- .../compat}/jdk/FilterOutputStream.java | 2 +- .../codename1/compat}/jdk/PrintWriter.java | 2 +- maven/compat-testing/pom.xml | 56 +++ .../testing/HeadlessImplementation.java | 6 +- .../compat}/testing/MainThreadRule.java | 6 +- .../codename1/impl/ImplementationFactory.java | 2 +- maven/pom.xml | 2 + scripts/check-cast-semantics.sh | 4 +- 182 files changed, 1067 insertions(+), 516 deletions(-) rename maven/android-compat/src/test/java/com/codename1/{androidcompat => compat}/jdk/BufferedReaderMarkTest.java (98%) rename maven/android-compat/src/test/java/com/codename1/{androidcompat => compat}/jdk/BufferedStreamsClosedTest.java (99%) rename maven/android-compat/src/test/java/com/codename1/{androidcompat => compat}/jdk/BufferedWriterClosedTest.java (98%) rename maven/android-compat/src/test/java/com/codename1/{androidcompat => compat}/jdk/FileCanonicalPathTest.java (95%) rename maven/android-compat/src/test/java/com/codename1/{androidcompat => compat}/jdk/FileRenameFailedDeleteTest.java (94%) rename maven/android-compat/src/test/java/com/codename1/{androidcompat => compat}/jdk/FileRenameOntoItselfTest.java (94%) rename maven/android-compat/src/test/java/com/codename1/{androidcompat => compat}/jdk/FileTypeContractTest.java (95%) rename maven/android-compat/src/test/java/com/codename1/{androidcompat => compat}/jdk/PrintWriterClosedTest.java (98%) create mode 100644 maven/build-engine/src/main/java/com/codename1/maven/ClassRelocator.java create mode 100644 maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java create mode 100644 maven/build-engine/src/main/java/com/codename1/maven/Relocation.java create mode 100644 maven/compat-jdk/pom.xml create mode 100644 maven/compat-jdk/spotbugs-exclude.xml rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/BufferedInputStream.java (99%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/BufferedOutputStream.java (98%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/BufferedReader.java (99%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/BufferedWriter.java (98%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/File.java (99%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/FileFilter.java (96%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/FileInputStream.java (98%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/FileOutputStream.java (98%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/FileReader.java (97%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/FileWriter.java (97%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/FilenameFilter.java (96%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/FilterInputStream.java (98%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/FilterOutputStream.java (98%) rename maven/{android-compat/src/main/java/com/codename1/androidcompat => compat-jdk/src/main/java/com/codename1/compat}/jdk/PrintWriter.java (99%) create mode 100644 maven/compat-testing/pom.xml rename maven/{android-compat/src/test/java/com/codename1/androidcompat => compat-testing/src/main/java/com/codename1/compat}/testing/HeadlessImplementation.java (99%) rename maven/{android-compat/src/test/java/com/codename1/androidcompat => compat-testing/src/main/java/com/codename1/compat}/testing/MainThreadRule.java (93%) rename maven/{android-compat/src/test => compat-testing/src/main}/java/com/codename1/impl/ImplementationFactory.java (96%) diff --git a/.github/workflows/android-compat.yml b/.github/workflows/android-compat.yml index 05f704d2a51..b314a9f31e5 100644 --- a/.github/workflows/android-compat.yml +++ b/.github/workflows/android-compat.yml @@ -18,6 +18,7 @@ on: branches: [ master ] paths: - 'maven/android-compat/**' + - 'maven/compat-jdk/**' - 'maven/android-res-compiler/**' - 'maven/build-engine/**' - 'maven/codenameone-maven-plugin/**' @@ -34,6 +35,7 @@ on: branches: [ master ] paths: - 'maven/android-compat/**' + - 'maven/compat-jdk/**' - 'maven/android-res-compiler/**' - 'maven/build-engine/**' - 'maven/codenameone-maven-plugin/**' @@ -85,7 +87,7 @@ jobs: bash ../scripts/ci/retry.sh xvfb-run -a mvn install -Plocal-dev-javase -DskipTests -Darchetype.test.skip=true -Dmaven.javadoc.skip=true - name: Unit tests working-directory: maven - run: xvfb-run -a mvn -B -Plocal-dev-javase -pl android-res-compiler,android-compat,build-engine test + run: xvfb-run -a mvn -B -Plocal-dev-javase -pl compat-jdk,android-res-compiler,android-compat,build-engine test - name: Refresh archetype catalog run: | cd maven diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a8906d677c2..233cc47ed1d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -391,7 +391,7 @@ jobs: -Dcn1.binaries="${CN1_BINARIES}" \ -Pcompile-android \ -DbackendAnalysis \ - -pl android,ios,project-model,build-engine,codenameone-maven-plugin,codenameone-gradle-plugin,backend,backend-test,backend-parparvm,javac,android-res-compiler,android-compat -am verify + -pl android,ios,project-model,build-engine,codenameone-maven-plugin,codenameone-gradle-plugin,backend,backend-test,backend-parparvm,javac,android-res-compiler,android-compat,compat-jdk -am verify - name: Type-check the SQLCipher-backed database package if: ${{ matrix.java-version == 8 }} env: @@ -606,7 +606,7 @@ jobs: - name: Generate static analysis HTML summaries if: ${{ !cancelled() && matrix.java-version == 8 }} env: - QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target + QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target:maven/compat-jdk/target QUALITY_REPORT_SERVER_URL: ${{ github.server_url }} QUALITY_REPORT_REPOSITORY: ${{ github.repository }} QUALITY_REPORT_REF: ${{ github.event.pull_request.head.sha || github.sha }} @@ -628,6 +628,7 @@ jobs: "build-engine:maven/build-engine/target" \ "android-res-compiler:maven/android-res-compiler/target" \ "android-compat:maven/android-compat/target" \ + "compat-jdk:maven/compat-jdk/target" \ "project-model:maven/project-model/target" \ "gradle-plugin:maven/codenameone-gradle-plugin/target" \ "backend:maven/backend/target" \ @@ -758,11 +759,11 @@ jobs: - name: Generate quality report summary if: ${{ !cancelled() && matrix.java-version == 8 }} env: - QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target + QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target:maven/compat-jdk/target # Every project listed here must produce a SpotBugs report and that # report must be empty. A missing report fails the gate too, so a # SpotBugs run that silently stops running is not mistaken for clean. - QUALITY_REPORT_REQUIRED_SPOTBUGS: core-unittests:android:ios:ByteCodeTranslator:codenameone-maven-plugin:build-engine:project-model:codenameone-gradle-plugin:build-hint-catalog:build-hint-tools:backend:backend-test:javac:android-res-compiler:android-compat + QUALITY_REPORT_REQUIRED_SPOTBUGS: core-unittests:android:ios:ByteCodeTranslator:codenameone-maven-plugin:build-engine:project-model:codenameone-gradle-plugin:build-hint-catalog:build-hint-tools:backend:backend-test:javac:android-res-compiler:android-compat:compat-jdk # PMD and Checkstyle run on the core and on the backend runtime, and each # must produce its report: without one, the forbidden-rule gate below it # has nothing to read and passes. diff --git a/maven/android-compat/pom.xml b/maven/android-compat/pom.xml index 39bd17dddff..ad2151251e0 100644 --- a/maven/android-compat/pom.xml +++ b/maven/android-compat/pom.xml @@ -153,6 +153,14 @@ codenameone-core provided + + + com.codenameone + codenameone-compat-jdk + ${project.version} + @@ -167,6 +175,13 @@ junit test + + + com.codenameone + codenameone-compat-testing + ${project.version} + test + diff --git a/maven/android-compat/src/main/java/android/net/Uri.java b/maven/android-compat/src/main/java/android/net/Uri.java index a331daf7bdb..755ba7e3112 100644 --- a/maven/android-compat/src/main/java/android/net/Uri.java +++ b/maven/android-compat/src/main/java/android/net/Uri.java @@ -55,8 +55,8 @@ public static Uri withAppendedPath(Uri baseUri, String pathSegment) { public static Uri fromFile(Object file) { // A shim directory (`getFilesDir()`) is already a `file:` path; // prefixing another scheme gave `file://file:///...`. - String s = file instanceof com.codename1.androidcompat.jdk.File - ? ((com.codename1.androidcompat.jdk.File) file).storagePath() : String.valueOf(file); + String s = file instanceof com.codename1.compat.jdk.File + ? ((com.codename1.compat.jdk.File) file).storagePath() : String.valueOf(file); // The path is percent-encoded (everything but `/`) as Android's // fromFile does: a raw `#` or `?` in a file name started a fragment // or query, so `getPath()` named another file. The authority a diff --git a/maven/android-compat/src/test/java/android/animation/AnimatorMathTest.java b/maven/android-compat/src/test/java/android/animation/AnimatorMathTest.java index 6b1103c4478..ce74d0d802f 100644 --- a/maven/android-compat/src/test/java/android/animation/AnimatorMathTest.java +++ b/maven/android-compat/src/test/java/android/animation/AnimatorMathTest.java @@ -29,7 +29,7 @@ import android.view.animation.AccelerateInterpolator; import android.view.animation.LinearInterpolator; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/animation/AutoCancelReleaseTest.java b/maven/android-compat/src/test/java/android/animation/AutoCancelReleaseTest.java index 64da6c7c1af..9d1a4b21603 100644 --- a/maven/android-compat/src/test/java/android/animation/AutoCancelReleaseTest.java +++ b/maven/android-compat/src/test/java/android/animation/AutoCancelReleaseTest.java @@ -22,7 +22,7 @@ */ package android.animation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/animation/ObjectAnimatorSetPropertyTest.java b/maven/android-compat/src/test/java/android/animation/ObjectAnimatorSetPropertyTest.java index 70662c4d5db..5113ff1165f 100644 --- a/maven/android-compat/src/test/java/android/animation/ObjectAnimatorSetPropertyTest.java +++ b/maven/android-compat/src/test/java/android/animation/ObjectAnimatorSetPropertyTest.java @@ -24,7 +24,7 @@ import android.view.animation.LinearInterpolator; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/animation/ReverseRepeatedAnimatorTest.java b/maven/android-compat/src/test/java/android/animation/ReverseRepeatedAnimatorTest.java index 6d91b01f83b..f47c5e0c6ab 100644 --- a/maven/android-compat/src/test/java/android/animation/ReverseRepeatedAnimatorTest.java +++ b/maven/android-compat/src/test/java/android/animation/ReverseRepeatedAnimatorTest.java @@ -25,7 +25,7 @@ import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/app/ActivityBoundaryTest.java b/maven/android-compat/src/test/java/android/app/ActivityBoundaryTest.java index 6a5cc08c9d1..5c475ba12bc 100644 --- a/maven/android-compat/src/test/java/android/app/ActivityBoundaryTest.java +++ b/maven/android-compat/src/test/java/android/app/ActivityBoundaryTest.java @@ -26,7 +26,7 @@ import android.content.Intent; import android.os.Bundle; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.*; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/app/DialogDefaultTouchTest.java b/maven/android-compat/src/test/java/android/app/DialogDefaultTouchTest.java index 13506dd3641..ff55d7420be 100644 --- a/maven/android-compat/src/test/java/android/app/DialogDefaultTouchTest.java +++ b/maven/android-compat/src/test/java/android/app/DialogDefaultTouchTest.java @@ -22,7 +22,7 @@ */ package android.app; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/app/FragmentLauncherRecreationTest.java b/maven/android-compat/src/test/java/android/app/FragmentLauncherRecreationTest.java index 9d707687c4e..7d4558d1d84 100644 --- a/maven/android-compat/src/test/java/android/app/FragmentLauncherRecreationTest.java +++ b/maven/android-compat/src/test/java/android/app/FragmentLauncherRecreationTest.java @@ -37,7 +37,7 @@ import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentFactory; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/app/FragmentManagerTest.java b/maven/android-compat/src/test/java/android/app/FragmentManagerTest.java index 41e5060bdfe..05fa932117d 100644 --- a/maven/android-compat/src/test/java/android/app/FragmentManagerTest.java +++ b/maven/android-compat/src/test/java/android/app/FragmentManagerTest.java @@ -29,7 +29,7 @@ import com.codename1.androidcompat.runtime.MenuImpl; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/app/FragmentViewStateTest.java b/maven/android-compat/src/test/java/android/app/FragmentViewStateTest.java index cec65c164d1..cfde8aa55eb 100644 --- a/maven/android-compat/src/test/java/android/app/FragmentViewStateTest.java +++ b/maven/android-compat/src/test/java/android/app/FragmentViewStateTest.java @@ -31,7 +31,7 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/app/PauseSelfFinishTest.java b/maven/android-compat/src/test/java/android/app/PauseSelfFinishTest.java index 8b5c196f2c2..a64f0f1fa13 100644 --- a/maven/android-compat/src/test/java/android/app/PauseSelfFinishTest.java +++ b/maven/android-compat/src/test/java/android/app/PauseSelfFinishTest.java @@ -24,7 +24,7 @@ import android.content.Context; import android.content.Intent; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/app/ResultAfterRelaunchTest.java b/maven/android-compat/src/test/java/android/app/ResultAfterRelaunchTest.java index 730248fea1e..24716ab6c38 100644 --- a/maven/android-compat/src/test/java/android/app/ResultAfterRelaunchTest.java +++ b/maven/android-compat/src/test/java/android/app/ResultAfterRelaunchTest.java @@ -27,7 +27,7 @@ import com.codename1.androidcompat.runtime.ResourceManager; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; +import com.codename1.compat.testing.HeadlessImplementation; import com.codename1.ui.Display; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/app/ResumeConfigurationTest.java b/maven/android-compat/src/test/java/android/app/ResumeConfigurationTest.java index 5cdae4a9955..2b71d4c582f 100644 --- a/maven/android-compat/src/test/java/android/app/ResumeConfigurationTest.java +++ b/maven/android-compat/src/test/java/android/app/ResumeConfigurationTest.java @@ -26,7 +26,7 @@ import android.content.Intent; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; +import com.codename1.compat.testing.HeadlessImplementation; import com.codename1.ui.Display; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/app/SupportFragmentsTest.java b/maven/android-compat/src/test/java/android/app/SupportFragmentsTest.java index e19bd8f29ab..823bbbba545 100644 --- a/maven/android-compat/src/test/java/android/app/SupportFragmentsTest.java +++ b/maven/android-compat/src/test/java/android/app/SupportFragmentsTest.java @@ -36,7 +36,7 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/app/WindowKeepScreenOnTest.java b/maven/android-compat/src/test/java/android/app/WindowKeepScreenOnTest.java index b46827ee87e..ee29af997b6 100644 --- a/maven/android-compat/src/test/java/android/app/WindowKeepScreenOnTest.java +++ b/maven/android-compat/src/test/java/android/app/WindowKeepScreenOnTest.java @@ -25,8 +25,8 @@ import android.content.Intent; import android.view.WindowManager; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/content/BroadcastFilterMatchTest.java b/maven/android-compat/src/test/java/android/content/BroadcastFilterMatchTest.java index 956640fd142..2722df75f35 100644 --- a/maven/android-compat/src/test/java/android/content/BroadcastFilterMatchTest.java +++ b/maven/android-compat/src/test/java/android/content/BroadcastFilterMatchTest.java @@ -26,7 +26,7 @@ import android.os.Bundle; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/content/ContentResolverFileUriTest.java b/maven/android-compat/src/test/java/android/content/ContentResolverFileUriTest.java index a163eb2bf10..7d3f6ecb72b 100644 --- a/maven/android-compat/src/test/java/android/content/ContentResolverFileUriTest.java +++ b/maven/android-compat/src/test/java/android/content/ContentResolverFileUriTest.java @@ -25,8 +25,8 @@ import android.net.Uri; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/content/ContentValuesTest.java b/maven/android-compat/src/test/java/android/content/ContentValuesTest.java index 333ccdec458..0b788b61023 100644 --- a/maven/android-compat/src/test/java/android/content/ContentValuesTest.java +++ b/maven/android-compat/src/test/java/android/content/ContentValuesTest.java @@ -31,7 +31,7 @@ import java.util.ArrayList; import java.util.List; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/content/ContextFileNameTest.java b/maven/android-compat/src/test/java/android/content/ContextFileNameTest.java index edec7b76d71..1e0ab7cc6d6 100644 --- a/maven/android-compat/src/test/java/android/content/ContextFileNameTest.java +++ b/maven/android-compat/src/test/java/android/content/ContextFileNameTest.java @@ -23,7 +23,7 @@ package android.content; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/content/IntentCloneFilterTest.java b/maven/android-compat/src/test/java/android/content/IntentCloneFilterTest.java index b1575de4746..95f2e4b0e23 100644 --- a/maven/android-compat/src/test/java/android/content/IntentCloneFilterTest.java +++ b/maven/android-compat/src/test/java/android/content/IntentCloneFilterTest.java @@ -22,7 +22,7 @@ */ package android.content; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/content/IntentResolveActivityTest.java b/maven/android-compat/src/test/java/android/content/IntentResolveActivityTest.java index a42c0b56bd9..1d1c2a4c721 100644 --- a/maven/android-compat/src/test/java/android/content/IntentResolveActivityTest.java +++ b/maven/android-compat/src/test/java/android/content/IntentResolveActivityTest.java @@ -26,7 +26,7 @@ import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/database/CursorsTest.java b/maven/android-compat/src/test/java/android/database/CursorsTest.java index 5447872cd79..fcdd9c0ee36 100644 --- a/maven/android-compat/src/test/java/android/database/CursorsTest.java +++ b/maven/android-compat/src/test/java/android/database/CursorsTest.java @@ -30,7 +30,7 @@ import java.util.Arrays; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/database/sqlite/SQLiteDatabaseTest.java b/maven/android-compat/src/test/java/android/database/sqlite/SQLiteDatabaseTest.java index 986eb66130c..7ece2c8fefb 100644 --- a/maven/android-compat/src/test/java/android/database/sqlite/SQLiteDatabaseTest.java +++ b/maven/android-compat/src/test/java/android/database/sqlite/SQLiteDatabaseTest.java @@ -39,7 +39,7 @@ import java.util.ArrayList; import java.util.List; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.After; import org.junit.Before; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/database/sqlite/SQLiteQueryBuilderTest.java b/maven/android-compat/src/test/java/android/database/sqlite/SQLiteQueryBuilderTest.java index ee8cc23eae4..15a2d099ba1 100644 --- a/maven/android-compat/src/test/java/android/database/sqlite/SQLiteQueryBuilderTest.java +++ b/maven/android-compat/src/test/java/android/database/sqlite/SQLiteQueryBuilderTest.java @@ -28,7 +28,7 @@ import java.util.LinkedHashMap; import java.util.Map; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapCompressFormatTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapCompressFormatTest.java index 68e955fd7a7..aeba30331f0 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapCompressFormatTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapCompressFormatTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import com.codename1.ui.util.ImageIO; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapDerivedDensityTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapDerivedDensityTest.java index c220192f44d..7cf76dafcdf 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapDerivedDensityTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapDerivedDensityTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.After; import org.junit.Before; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapDisplayMetricsDensityTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapDisplayMetricsDensityTest.java index 6d8fe12ff37..3cf77a10665 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapDisplayMetricsDensityTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapDisplayMetricsDensityTest.java @@ -25,7 +25,7 @@ import android.util.DisplayMetrics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapFactoryBoundsTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapFactoryBoundsTest.java index 052df841c43..1b2c1af4307 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapFactoryBoundsTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapFactoryBoundsTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.After; import org.junit.Before; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapFactoryHeaderBoundsTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapFactoryHeaderBoundsTest.java index eead128d5cf..eeea2455638 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapFactoryHeaderBoundsTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapFactoryHeaderBoundsTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.After; import org.junit.Before; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapFactoryMimeTypeTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapFactoryMimeTypeTest.java index f500b92cc79..c8f19cbaa02 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapFactoryMimeTypeTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapFactoryMimeTypeTest.java @@ -23,7 +23,7 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Before; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapFactoryUnscaledDensityTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapFactoryUnscaledDensityTest.java index d8d830e2971..e13ec64adb6 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapFactoryUnscaledDensityTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapFactoryUnscaledDensityTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.After; import org.junit.Before; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapImmutableWriteTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapImmutableWriteTest.java index 7d14e71f210..824f8f3fb60 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapImmutableWriteTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapImmutableWriteTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import org.junit.After; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapMatrixTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapMatrixTest.java index 2ef074d80b9..78483b0e4fa 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapMatrixTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapMatrixTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.After; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapMutableSnapshotTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapMutableSnapshotTest.java index b9bb3828141..65640cd7c29 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapMutableSnapshotTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapMutableSnapshotTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import org.junit.After; diff --git a/maven/android-compat/src/test/java/android/graphics/BitmapPixelBoundsTest.java b/maven/android-compat/src/test/java/android/graphics/BitmapPixelBoundsTest.java index eea0ac80ae6..f260fab4ece 100644 --- a/maven/android-compat/src/test/java/android/graphics/BitmapPixelBoundsTest.java +++ b/maven/android-compat/src/test/java/android/graphics/BitmapPixelBoundsTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import org.junit.After; diff --git a/maven/android-compat/src/test/java/android/graphics/CanvasBitmapPixelCacheTest.java b/maven/android-compat/src/test/java/android/graphics/CanvasBitmapPixelCacheTest.java index 75f6c6c614b..33f65fa6546 100644 --- a/maven/android-compat/src/test/java/android/graphics/CanvasBitmapPixelCacheTest.java +++ b/maven/android-compat/src/test/java/android/graphics/CanvasBitmapPixelCacheTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import org.junit.After; diff --git a/maven/android-compat/src/test/java/android/graphics/CanvasClipPathTest.java b/maven/android-compat/src/test/java/android/graphics/CanvasClipPathTest.java index 3627afe5504..473ae646703 100644 --- a/maven/android-compat/src/test/java/android/graphics/CanvasClipPathTest.java +++ b/maven/android-compat/src/test/java/android/graphics/CanvasClipPathTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Graphics; import com.codename1.ui.Image; import com.codename1.ui.geom.Rectangle; diff --git a/maven/android-compat/src/test/java/android/graphics/CanvasClipRestoreAndClearTest.java b/maven/android-compat/src/test/java/android/graphics/CanvasClipRestoreAndClearTest.java index 120b15654dd..9dff20fc1af 100644 --- a/maven/android-compat/src/test/java/android/graphics/CanvasClipRestoreAndClearTest.java +++ b/maven/android-compat/src/test/java/android/graphics/CanvasClipRestoreAndClearTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Graphics; import com.codename1.ui.Image; diff --git a/maven/android-compat/src/test/java/android/graphics/CanvasDrawPaintShaderTest.java b/maven/android-compat/src/test/java/android/graphics/CanvasDrawPaintShaderTest.java index 83a14a4809b..b9ac446b9cb 100644 --- a/maven/android-compat/src/test/java/android/graphics/CanvasDrawPaintShaderTest.java +++ b/maven/android-compat/src/test/java/android/graphics/CanvasDrawPaintShaderTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Graphics; import com.codename1.ui.Image; diff --git a/maven/android-compat/src/test/java/android/graphics/CanvasImmutableBitmapTest.java b/maven/android-compat/src/test/java/android/graphics/CanvasImmutableBitmapTest.java index 14303275337..201587bca27 100644 --- a/maven/android-compat/src/test/java/android/graphics/CanvasImmutableBitmapTest.java +++ b/maven/android-compat/src/test/java/android/graphics/CanvasImmutableBitmapTest.java @@ -23,7 +23,7 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/graphics/CanvasRecycledBitmapTest.java b/maven/android-compat/src/test/java/android/graphics/CanvasRecycledBitmapTest.java index 713eede7a93..2433e520485 100644 --- a/maven/android-compat/src/test/java/android/graphics/CanvasRecycledBitmapTest.java +++ b/maven/android-compat/src/test/java/android/graphics/CanvasRecycledBitmapTest.java @@ -23,7 +23,7 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Before; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/graphics/CanvasRotatedClipRectTest.java b/maven/android-compat/src/test/java/android/graphics/CanvasRotatedClipRectTest.java index fa774c77ba1..d3b1f76008a 100644 --- a/maven/android-compat/src/test/java/android/graphics/CanvasRotatedClipRectTest.java +++ b/maven/android-compat/src/test/java/android/graphics/CanvasRotatedClipRectTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Graphics; import com.codename1.ui.Image; diff --git a/maven/android-compat/src/test/java/android/graphics/CanvasSaveLayerTest.java b/maven/android-compat/src/test/java/android/graphics/CanvasSaveLayerTest.java index fec47a1c2b7..e46b658b8bc 100644 --- a/maven/android-compat/src/test/java/android/graphics/CanvasSaveLayerTest.java +++ b/maven/android-compat/src/test/java/android/graphics/CanvasSaveLayerTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Graphics; import com.codename1.ui.Image; diff --git a/maven/android-compat/src/test/java/android/graphics/CanvasSpacedSurrogateTest.java b/maven/android-compat/src/test/java/android/graphics/CanvasSpacedSurrogateTest.java index b089d7f646e..fea418263a0 100644 --- a/maven/android-compat/src/test/java/android/graphics/CanvasSpacedSurrogateTest.java +++ b/maven/android-compat/src/test/java/android/graphics/CanvasSpacedSurrogateTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Graphics; import com.codename1.ui.Image; diff --git a/maven/android-compat/src/test/java/android/graphics/CanvasTextScaleXTest.java b/maven/android-compat/src/test/java/android/graphics/CanvasTextScaleXTest.java index fbca64f1e07..5e8b13e33ed 100644 --- a/maven/android-compat/src/test/java/android/graphics/CanvasTextScaleXTest.java +++ b/maven/android-compat/src/test/java/android/graphics/CanvasTextScaleXTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Graphics; import com.codename1.ui.Image; diff --git a/maven/android-compat/src/test/java/android/graphics/LetterSpacingMeasureTest.java b/maven/android-compat/src/test/java/android/graphics/LetterSpacingMeasureTest.java index 30fef53d5f2..5d2aea4a718 100644 --- a/maven/android-compat/src/test/java/android/graphics/LetterSpacingMeasureTest.java +++ b/maven/android-compat/src/test/java/android/graphics/LetterSpacingMeasureTest.java @@ -23,8 +23,8 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/graphics/LetterSpacingWidthsTest.java b/maven/android-compat/src/test/java/android/graphics/LetterSpacingWidthsTest.java index 59c8c9b8594..8cf87a0486e 100644 --- a/maven/android-compat/src/test/java/android/graphics/LetterSpacingWidthsTest.java +++ b/maven/android-compat/src/test/java/android/graphics/LetterSpacingWidthsTest.java @@ -23,7 +23,7 @@ package android.graphics; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/graphics/drawable/BitmapDrawableTargetDensityTest.java b/maven/android-compat/src/test/java/android/graphics/drawable/BitmapDrawableTargetDensityTest.java index 38b8affe473..ef3673a0520 100644 --- a/maven/android-compat/src/test/java/android/graphics/drawable/BitmapDrawableTargetDensityTest.java +++ b/maven/android-compat/src/test/java/android/graphics/drawable/BitmapDrawableTargetDensityTest.java @@ -26,8 +26,8 @@ import android.graphics.Bitmap; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/graphics/drawable/BitmapDrawableTileModeTest.java b/maven/android-compat/src/test/java/android/graphics/drawable/BitmapDrawableTileModeTest.java index c9bd951fdd4..d93ec2d7a3a 100644 --- a/maven/android-compat/src/test/java/android/graphics/drawable/BitmapDrawableTileModeTest.java +++ b/maven/android-compat/src/test/java/android/graphics/drawable/BitmapDrawableTileModeTest.java @@ -30,8 +30,8 @@ import android.graphics.Shader; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import java.util.ArrayList; diff --git a/maven/android-compat/src/test/java/android/graphics/drawable/GradientDrawableOrientationTest.java b/maven/android-compat/src/test/java/android/graphics/drawable/GradientDrawableOrientationTest.java index d8316b803e1..2fa89708efd 100644 --- a/maven/android-compat/src/test/java/android/graphics/drawable/GradientDrawableOrientationTest.java +++ b/maven/android-compat/src/test/java/android/graphics/drawable/GradientDrawableOrientationTest.java @@ -25,8 +25,8 @@ import android.graphics.Canvas; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import com.codename1.ui.LinearGradient; diff --git a/maven/android-compat/src/test/java/android/graphics/drawable/StateListTintModeTest.java b/maven/android-compat/src/test/java/android/graphics/drawable/StateListTintModeTest.java index d3537db90f5..9581c195278 100644 --- a/maven/android-compat/src/test/java/android/graphics/drawable/StateListTintModeTest.java +++ b/maven/android-compat/src/test/java/android/graphics/drawable/StateListTintModeTest.java @@ -26,7 +26,7 @@ import android.graphics.PorterDuff; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Before; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/graphics/drawable/WrapperChildStateTest.java b/maven/android-compat/src/test/java/android/graphics/drawable/WrapperChildStateTest.java index ff0b99b893d..09e47c251ca 100644 --- a/maven/android-compat/src/test/java/android/graphics/drawable/WrapperChildStateTest.java +++ b/maven/android-compat/src/test/java/android/graphics/drawable/WrapperChildStateTest.java @@ -29,7 +29,7 @@ import android.graphics.Rect; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import org.junit.Before; diff --git a/maven/android-compat/src/test/java/android/net/UriFromFileTest.java b/maven/android-compat/src/test/java/android/net/UriFromFileTest.java index 5007efeab78..f7ba02563aa 100644 --- a/maven/android-compat/src/test/java/android/net/UriFromFileTest.java +++ b/maven/android-compat/src/test/java/android/net/UriFromFileTest.java @@ -22,8 +22,8 @@ */ package android.net; -import com.codename1.androidcompat.jdk.File; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.jdk.File; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/net/UriNormalizeSchemeTest.java b/maven/android-compat/src/test/java/android/net/UriNormalizeSchemeTest.java index b6144d8b5f2..bb8225464bf 100644 --- a/maven/android-compat/src/test/java/android/net/UriNormalizeSchemeTest.java +++ b/maven/android-compat/src/test/java/android/net/UriNormalizeSchemeTest.java @@ -22,7 +22,7 @@ */ package android.net; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/os/BundleDeepCopyTest.java b/maven/android-compat/src/test/java/android/os/BundleDeepCopyTest.java index 77a29ef9d69..aa334c90126 100644 --- a/maven/android-compat/src/test/java/android/os/BundleDeepCopyTest.java +++ b/maven/android-compat/src/test/java/android/os/BundleDeepCopyTest.java @@ -24,7 +24,7 @@ import android.util.SparseArray; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import java.util.ArrayList; diff --git a/maven/android-compat/src/test/java/android/os/BundleEmptyImmutableTest.java b/maven/android-compat/src/test/java/android/os/BundleEmptyImmutableTest.java index 74f80640063..3fe553ed892 100644 --- a/maven/android-compat/src/test/java/android/os/BundleEmptyImmutableTest.java +++ b/maven/android-compat/src/test/java/android/os/BundleEmptyImmutableTest.java @@ -22,7 +22,7 @@ */ package android.os; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/os/HandlerFrontOfQueueTest.java b/maven/android-compat/src/test/java/android/os/HandlerFrontOfQueueTest.java index b3c06ca1ad3..5829b007fe1 100644 --- a/maven/android-compat/src/test/java/android/os/HandlerFrontOfQueueTest.java +++ b/maven/android-compat/src/test/java/android/os/HandlerFrontOfQueueTest.java @@ -23,7 +23,7 @@ package android.os; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/text/AllCapsSpansTest.java b/maven/android-compat/src/test/java/android/text/AllCapsSpansTest.java index 4940fb4d5a2..4258445935d 100644 --- a/maven/android-compat/src/test/java/android/text/AllCapsSpansTest.java +++ b/maven/android-compat/src/test/java/android/text/AllCapsSpansTest.java @@ -22,7 +22,7 @@ */ package android.text; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/text/LengthFilterSurrogateTest.java b/maven/android-compat/src/test/java/android/text/LengthFilterSurrogateTest.java index 4ddfa134989..99b4d08ceef 100644 --- a/maven/android-compat/src/test/java/android/text/LengthFilterSurrogateTest.java +++ b/maven/android-compat/src/test/java/android/text/LengthFilterSurrogateTest.java @@ -22,7 +22,7 @@ */ package android.text; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/text/TextUtilsCommaEllipsizeTest.java b/maven/android-compat/src/test/java/android/text/TextUtilsCommaEllipsizeTest.java index 3262ee9d567..11a2423106b 100644 --- a/maven/android-compat/src/test/java/android/text/TextUtilsCommaEllipsizeTest.java +++ b/maven/android-compat/src/test/java/android/text/TextUtilsCommaEllipsizeTest.java @@ -23,7 +23,7 @@ package android.text; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/text/TextUtilsEllipsizeSpansTest.java b/maven/android-compat/src/test/java/android/text/TextUtilsEllipsizeSpansTest.java index c1725b37a0d..641a7dc43df 100644 --- a/maven/android-compat/src/test/java/android/text/TextUtilsEllipsizeSpansTest.java +++ b/maven/android-compat/src/test/java/android/text/TextUtilsEllipsizeSpansTest.java @@ -23,7 +23,7 @@ package android.text; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/text/TextUtilsSplitRegexTest.java b/maven/android-compat/src/test/java/android/text/TextUtilsSplitRegexTest.java index 93bec284971..c7ed5d3f22b 100644 --- a/maven/android-compat/src/test/java/android/text/TextUtilsSplitRegexTest.java +++ b/maven/android-compat/src/test/java/android/text/TextUtilsSplitRegexTest.java @@ -22,7 +22,7 @@ */ package android.text; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import java.util.Arrays; diff --git a/maven/android-compat/src/test/java/android/view/AncestorChangeDispatchTest.java b/maven/android-compat/src/test/java/android/view/AncestorChangeDispatchTest.java index 9cc1fd29164..613b1b9c997 100644 --- a/maven/android-compat/src/test/java/android/view/AncestorChangeDispatchTest.java +++ b/maven/android-compat/src/test/java/android/view/AncestorChangeDispatchTest.java @@ -25,7 +25,7 @@ import android.widget.FrameLayout; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/view/BackgroundTintModeTest.java b/maven/android-compat/src/test/java/android/view/BackgroundTintModeTest.java index edda59e4650..c946dd02a5a 100644 --- a/maven/android-compat/src/test/java/android/view/BackgroundTintModeTest.java +++ b/maven/android-compat/src/test/java/android/view/BackgroundTintModeTest.java @@ -27,7 +27,7 @@ import android.graphics.drawable.ColorDrawable; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/view/DrawableLayoutDirectionTest.java b/maven/android-compat/src/test/java/android/view/DrawableLayoutDirectionTest.java index e0014988756..0794ff31bcf 100644 --- a/maven/android-compat/src/test/java/android/view/DrawableLayoutDirectionTest.java +++ b/maven/android-compat/src/test/java/android/view/DrawableLayoutDirectionTest.java @@ -26,7 +26,7 @@ import android.widget.FrameLayout; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/view/HiddenViewLosesFocusTest.java b/maven/android-compat/src/test/java/android/view/HiddenViewLosesFocusTest.java index 93e81aeb78d..448047cc662 100644 --- a/maven/android-compat/src/test/java/android/view/HiddenViewLosesFocusTest.java +++ b/maven/android-compat/src/test/java/android/view/HiddenViewLosesFocusTest.java @@ -25,7 +25,7 @@ import android.widget.FrameLayout; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/view/IncludeThemeTest.java b/maven/android-compat/src/test/java/android/view/IncludeThemeTest.java index 9e333583934..52c5903b242 100644 --- a/maven/android-compat/src/test/java/android/view/IncludeThemeTest.java +++ b/maven/android-compat/src/test/java/android/view/IncludeThemeTest.java @@ -27,7 +27,7 @@ import com.codename1.androidcompat.runtime.ResValue; import com.codename1.androidcompat.runtime.XmlNode; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/view/MergedIncludeFinishInflateTest.java b/maven/android-compat/src/test/java/android/view/MergedIncludeFinishInflateTest.java index 20aff7b491f..9deba5b9f49 100644 --- a/maven/android-compat/src/test/java/android/view/MergedIncludeFinishInflateTest.java +++ b/maven/android-compat/src/test/java/android/view/MergedIncludeFinishInflateTest.java @@ -31,7 +31,7 @@ import com.codename1.androidcompat.runtime.ResValue; import com.codename1.androidcompat.runtime.XmlNode; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/view/RemovedTouchTargetTest.java b/maven/android-compat/src/test/java/android/view/RemovedTouchTargetTest.java index b8bfc05e767..0f3fbb09c40 100644 --- a/maven/android-compat/src/test/java/android/view/RemovedTouchTargetTest.java +++ b/maven/android-compat/src/test/java/android/view/RemovedTouchTargetTest.java @@ -26,7 +26,7 @@ import android.widget.FrameLayout; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import java.util.ArrayList; import java.util.List; diff --git a/maven/android-compat/src/test/java/android/view/TransformedChildTouchTest.java b/maven/android-compat/src/test/java/android/view/TransformedChildTouchTest.java index c2276483cf5..f402a2acab4 100644 --- a/maven/android-compat/src/test/java/android/view/TransformedChildTouchTest.java +++ b/maven/android-compat/src/test/java/android/view/TransformedChildTouchTest.java @@ -26,7 +26,7 @@ import android.widget.FrameLayout; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/view/ViewCallbacksTest.java b/maven/android-compat/src/test/java/android/view/ViewCallbacksTest.java index 5cb6d4948db..1c0b9f6b0c2 100644 --- a/maven/android-compat/src/test/java/android/view/ViewCallbacksTest.java +++ b/maven/android-compat/src/test/java/android/view/ViewCallbacksTest.java @@ -33,7 +33,7 @@ import com.codename1.androidcompat.runtime.ResValue; import com.codename1.androidcompat.runtime.XmlNode; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/view/ViewStubReferenceTest.java b/maven/android-compat/src/test/java/android/view/ViewStubReferenceTest.java index 3f66e8167bd..dfbc02b1011 100644 --- a/maven/android-compat/src/test/java/android/view/ViewStubReferenceTest.java +++ b/maven/android-compat/src/test/java/android/view/ViewStubReferenceTest.java @@ -24,7 +24,7 @@ import android.content.Context; import android.widget.FrameLayout; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/view/ViewTreeScrollTest.java b/maven/android-compat/src/test/java/android/view/ViewTreeScrollTest.java index b505ee3f95f..32016b44d53 100644 --- a/maven/android-compat/src/test/java/android/view/ViewTreeScrollTest.java +++ b/maven/android-compat/src/test/java/android/view/ViewTreeScrollTest.java @@ -24,7 +24,7 @@ import android.widget.FrameLayout; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/view/VisibleRectAndKeepScreenOnTest.java b/maven/android-compat/src/test/java/android/view/VisibleRectAndKeepScreenOnTest.java index 71a2b8c5402..96c78ce69be 100644 --- a/maven/android-compat/src/test/java/android/view/VisibleRectAndKeepScreenOnTest.java +++ b/maven/android-compat/src/test/java/android/view/VisibleRectAndKeepScreenOnTest.java @@ -26,8 +26,8 @@ import android.widget.FrameLayout; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/view/animation/AnimationCloneTest.java b/maven/android-compat/src/test/java/android/view/animation/AnimationCloneTest.java index d79f0b3d46e..0d5eae6a73a 100644 --- a/maven/android-compat/src/test/java/android/view/animation/AnimationCloneTest.java +++ b/maven/android-compat/src/test/java/android/view/animation/AnimationCloneTest.java @@ -31,7 +31,7 @@ import android.animation.TimeAnimator; import android.animation.ValueAnimator; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/view/animation/AnimationTimingTest.java b/maven/android-compat/src/test/java/android/view/animation/AnimationTimingTest.java index a0ddab1878e..cb9287d85bb 100644 --- a/maven/android-compat/src/test/java/android/view/animation/AnimationTimingTest.java +++ b/maven/android-compat/src/test/java/android/view/animation/AnimationTimingTest.java @@ -28,7 +28,7 @@ import android.graphics.Matrix; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/webkit/WebViewHistoryDistanceTest.java b/maven/android-compat/src/test/java/android/webkit/WebViewHistoryDistanceTest.java index 8a18d872564..044c8da936a 100644 --- a/maven/android-compat/src/test/java/android/webkit/WebViewHistoryDistanceTest.java +++ b/maven/android-compat/src/test/java/android/webkit/WebViewHistoryDistanceTest.java @@ -25,7 +25,7 @@ import android.content.Context; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/webkit/WebViewHistoryUrlTest.java b/maven/android-compat/src/test/java/android/webkit/WebViewHistoryUrlTest.java index 64bd42d6839..8744080d191 100644 --- a/maven/android-compat/src/test/java/android/webkit/WebViewHistoryUrlTest.java +++ b/maven/android-compat/src/test/java/android/webkit/WebViewHistoryUrlTest.java @@ -23,7 +23,7 @@ package android.webkit; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.BrowserComponent; import com.codename1.ui.events.ActionEvent; diff --git a/maven/android-compat/src/test/java/android/widget/AbsListViewSelectionAndStackTest.java b/maven/android-compat/src/test/java/android/widget/AbsListViewSelectionAndStackTest.java index 83110ec9734..eec691d1790 100644 --- a/maven/android-compat/src/test/java/android/widget/AbsListViewSelectionAndStackTest.java +++ b/maven/android-compat/src/test/java/android/widget/AbsListViewSelectionAndStackTest.java @@ -27,7 +27,7 @@ import android.view.View; import android.view.ViewGroup; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import java.lang.reflect.Field; diff --git a/maven/android-compat/src/test/java/android/widget/DatePickerRangeClampNotifiesTest.java b/maven/android-compat/src/test/java/android/widget/DatePickerRangeClampNotifiesTest.java index 2cf1e43e3c8..b6cf77f6feb 100644 --- a/maven/android-compat/src/test/java/android/widget/DatePickerRangeClampNotifiesTest.java +++ b/maven/android-compat/src/test/java/android/widget/DatePickerRangeClampNotifiesTest.java @@ -23,7 +23,7 @@ package android.widget; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/DatePickerSavedStateTest.java b/maven/android-compat/src/test/java/android/widget/DatePickerSavedStateTest.java index 003f32de146..dfe338d87cb 100644 --- a/maven/android-compat/src/test/java/android/widget/DatePickerSavedStateTest.java +++ b/maven/android-compat/src/test/java/android/widget/DatePickerSavedStateTest.java @@ -24,7 +24,7 @@ import android.os.Parcelable; import android.util.SparseArray; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/widget/EditTextEditableWatcherTest.java b/maven/android-compat/src/test/java/android/widget/EditTextEditableWatcherTest.java index c7b6ca6a6da..38a884aaabc 100644 --- a/maven/android-compat/src/test/java/android/widget/EditTextEditableWatcherTest.java +++ b/maven/android-compat/src/test/java/android/widget/EditTextEditableWatcherTest.java @@ -26,7 +26,7 @@ import android.text.TextWatcher; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Before; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/widget/EditTextSelectionTest.java b/maven/android-compat/src/test/java/android/widget/EditTextSelectionTest.java index 8a8109aab77..ed0f131fb11 100644 --- a/maven/android-compat/src/test/java/android/widget/EditTextSelectionTest.java +++ b/maven/android-compat/src/test/java/android/widget/EditTextSelectionTest.java @@ -23,7 +23,7 @@ package android.widget; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/ForegroundGravityTest.java b/maven/android-compat/src/test/java/android/widget/ForegroundGravityTest.java index d00dfccaffe..68abba59b85 100644 --- a/maven/android-compat/src/test/java/android/widget/ForegroundGravityTest.java +++ b/maven/android-compat/src/test/java/android/widget/ForegroundGravityTest.java @@ -27,7 +27,7 @@ import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/FormWidgetSavedStateTest.java b/maven/android-compat/src/test/java/android/widget/FormWidgetSavedStateTest.java index fbd0e3c8d21..4bfc88abf87 100644 --- a/maven/android-compat/src/test/java/android/widget/FormWidgetSavedStateTest.java +++ b/maven/android-compat/src/test/java/android/widget/FormWidgetSavedStateTest.java @@ -25,7 +25,7 @@ import android.util.SparseArray; import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/widget/GridViewUniformSpacingTest.java b/maven/android-compat/src/test/java/android/widget/GridViewUniformSpacingTest.java index 938fd356c4d..c51dd74c112 100644 --- a/maven/android-compat/src/test/java/android/widget/GridViewUniformSpacingTest.java +++ b/maven/android-compat/src/test/java/android/widget/GridViewUniformSpacingTest.java @@ -25,7 +25,7 @@ import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/ImageViewClearColorModTest.java b/maven/android-compat/src/test/java/android/widget/ImageViewClearColorModTest.java index f6372455b8b..9c4abb319f6 100644 --- a/maven/android-compat/src/test/java/android/widget/ImageViewClearColorModTest.java +++ b/maven/android-compat/src/test/java/android/widget/ImageViewClearColorModTest.java @@ -31,7 +31,7 @@ import android.graphics.drawable.Drawable; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Before; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/widget/ImageViewMeasureInputsRelayoutTest.java b/maven/android-compat/src/test/java/android/widget/ImageViewMeasureInputsRelayoutTest.java index 1cafce39123..eafdf7f49c3 100644 --- a/maven/android-compat/src/test/java/android/widget/ImageViewMeasureInputsRelayoutTest.java +++ b/maven/android-compat/src/test/java/android/widget/ImageViewMeasureInputsRelayoutTest.java @@ -25,7 +25,7 @@ import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Before; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/widget/LinearLayoutDividersTest.java b/maven/android-compat/src/test/java/android/widget/LinearLayoutDividersTest.java index 72071b046e4..73a3237cc24 100644 --- a/maven/android-compat/src/test/java/android/widget/LinearLayoutDividersTest.java +++ b/maven/android-compat/src/test/java/android/widget/LinearLayoutDividersTest.java @@ -26,7 +26,7 @@ import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/ListCheckedRowsAfterDataChangeTest.java b/maven/android-compat/src/test/java/android/widget/ListCheckedRowsAfterDataChangeTest.java index 4997b0eb282..b756c613ea5 100644 --- a/maven/android-compat/src/test/java/android/widget/ListCheckedRowsAfterDataChangeTest.java +++ b/maven/android-compat/src/test/java/android/widget/ListCheckedRowsAfterDataChangeTest.java @@ -26,7 +26,7 @@ import android.view.ViewGroup; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/ListViewDragOffClickTest.java b/maven/android-compat/src/test/java/android/widget/ListViewDragOffClickTest.java index b1186cfce76..733f3e52a25 100644 --- a/maven/android-compat/src/test/java/android/widget/ListViewDragOffClickTest.java +++ b/maven/android-compat/src/test/java/android/widget/ListViewDragOffClickTest.java @@ -27,7 +27,7 @@ import android.view.ViewGroup; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/ProgressBarIndeterminateDrawableTest.java b/maven/android-compat/src/test/java/android/widget/ProgressBarIndeterminateDrawableTest.java index 381bcb0c029..611b6db643c 100644 --- a/maven/android-compat/src/test/java/android/widget/ProgressBarIndeterminateDrawableTest.java +++ b/maven/android-compat/src/test/java/android/widget/ProgressBarIndeterminateDrawableTest.java @@ -29,7 +29,7 @@ import android.graphics.drawable.Drawable; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.Image; import org.junit.Before; diff --git a/maven/android-compat/src/test/java/android/widget/ProgressBarRangeClampTest.java b/maven/android-compat/src/test/java/android/widget/ProgressBarRangeClampTest.java index fc6d8fad659..c4b2bce89e8 100644 --- a/maven/android-compat/src/test/java/android/widget/ProgressBarRangeClampTest.java +++ b/maven/android-compat/src/test/java/android/widget/ProgressBarRangeClampTest.java @@ -23,7 +23,7 @@ package android.widget; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/ProgressDrawableOwnershipTest.java b/maven/android-compat/src/test/java/android/widget/ProgressDrawableOwnershipTest.java index edd4aebd750..01f108c3ed5 100644 --- a/maven/android-compat/src/test/java/android/widget/ProgressDrawableOwnershipTest.java +++ b/maven/android-compat/src/test/java/android/widget/ProgressDrawableOwnershipTest.java @@ -25,7 +25,7 @@ import android.util.SparseArray; import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/widget/RadioGroupAndFilteredAdapterTest.java b/maven/android-compat/src/test/java/android/widget/RadioGroupAndFilteredAdapterTest.java index 03291c5189a..5239c64a603 100644 --- a/maven/android-compat/src/test/java/android/widget/RadioGroupAndFilteredAdapterTest.java +++ b/maven/android-compat/src/test/java/android/widget/RadioGroupAndFilteredAdapterTest.java @@ -26,7 +26,7 @@ import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/RowStateNotSavedTest.java b/maven/android-compat/src/test/java/android/widget/RowStateNotSavedTest.java index 9cfaeaae8aa..9daf60f95da 100644 --- a/maven/android-compat/src/test/java/android/widget/RowStateNotSavedTest.java +++ b/maven/android-compat/src/test/java/android/widget/RowStateNotSavedTest.java @@ -28,7 +28,7 @@ import android.view.ViewGroup; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/ScrollViewChildMarginsTest.java b/maven/android-compat/src/test/java/android/widget/ScrollViewChildMarginsTest.java index e9dcc07e29a..d1fb853fdf7 100644 --- a/maven/android-compat/src/test/java/android/widget/ScrollViewChildMarginsTest.java +++ b/maven/android-compat/src/test/java/android/widget/ScrollViewChildMarginsTest.java @@ -25,7 +25,7 @@ import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/SeekBarInScrollingContainerTest.java b/maven/android-compat/src/test/java/android/widget/SeekBarInScrollingContainerTest.java index 2872b0e92ee..c15394abe5e 100644 --- a/maven/android-compat/src/test/java/android/widget/SeekBarInScrollingContainerTest.java +++ b/maven/android-compat/src/test/java/android/widget/SeekBarInScrollingContainerTest.java @@ -27,7 +27,7 @@ import android.view.ViewGroup; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/SimpleAdapterBindingHooksTest.java b/maven/android-compat/src/test/java/android/widget/SimpleAdapterBindingHooksTest.java index 8fb88ecd390..a3d5c4f06df 100644 --- a/maven/android-compat/src/test/java/android/widget/SimpleAdapterBindingHooksTest.java +++ b/maven/android-compat/src/test/java/android/widget/SimpleAdapterBindingHooksTest.java @@ -26,7 +26,7 @@ import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/SimpleAdapterFilterTest.java b/maven/android-compat/src/test/java/android/widget/SimpleAdapterFilterTest.java index 607ff60ac7d..b39b3438dbc 100644 --- a/maven/android-compat/src/test/java/android/widget/SimpleAdapterFilterTest.java +++ b/maven/android-compat/src/test/java/android/widget/SimpleAdapterFilterTest.java @@ -23,7 +23,7 @@ package android.widget; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.BeforeClass; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/widget/SpinnerSelectedRowIdTest.java b/maven/android-compat/src/test/java/android/widget/SpinnerSelectedRowIdTest.java index d24b6cc0ff5..941328aea4e 100644 --- a/maven/android-compat/src/test/java/android/widget/SpinnerSelectedRowIdTest.java +++ b/maven/android-compat/src/test/java/android/widget/SpinnerSelectedRowIdTest.java @@ -26,7 +26,7 @@ import android.view.ViewGroup; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/TextViewAppendSpansTest.java b/maven/android-compat/src/test/java/android/widget/TextViewAppendSpansTest.java index 1a9d8f94cc0..bf142d9511c 100644 --- a/maven/android-compat/src/test/java/android/widget/TextViewAppendSpansTest.java +++ b/maven/android-compat/src/test/java/android/widget/TextViewAppendSpansTest.java @@ -24,7 +24,7 @@ import android.text.SpannableString; import android.text.Spanned; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/widget/TextViewDefaultAppearanceTest.java b/maven/android-compat/src/test/java/android/widget/TextViewDefaultAppearanceTest.java index 1aabbd84ed6..54c1323080a 100644 --- a/maven/android-compat/src/test/java/android/widget/TextViewDefaultAppearanceTest.java +++ b/maven/android-compat/src/test/java/android/widget/TextViewDefaultAppearanceTest.java @@ -28,7 +28,7 @@ import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/TextViewEllipsizeModeTest.java b/maven/android-compat/src/test/java/android/widget/TextViewEllipsizeModeTest.java index c8a3e804664..28bb6628491 100644 --- a/maven/android-compat/src/test/java/android/widget/TextViewEllipsizeModeTest.java +++ b/maven/android-compat/src/test/java/android/widget/TextViewEllipsizeModeTest.java @@ -25,7 +25,7 @@ import android.text.TextUtils; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/TextViewFilterAndWatcherTest.java b/maven/android-compat/src/test/java/android/widget/TextViewFilterAndWatcherTest.java index a5b62cdd415..b5b4aedb07e 100644 --- a/maven/android-compat/src/test/java/android/widget/TextViewFilterAndWatcherTest.java +++ b/maven/android-compat/src/test/java/android/widget/TextViewFilterAndWatcherTest.java @@ -27,7 +27,7 @@ import android.text.TextWatcher; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Before; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/android/widget/TextViewNormalSnapshotTest.java b/maven/android-compat/src/test/java/android/widget/TextViewNormalSnapshotTest.java index 2bc1c87ca39..41598d12959 100644 --- a/maven/android-compat/src/test/java/android/widget/TextViewNormalSnapshotTest.java +++ b/maven/android-compat/src/test/java/android/widget/TextViewNormalSnapshotTest.java @@ -28,7 +28,7 @@ import android.text.style.ForegroundColorSpan; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/TextViewRelativeDrawablesTest.java b/maven/android-compat/src/test/java/android/widget/TextViewRelativeDrawablesTest.java index 402c147a697..f25b2f2abb2 100644 --- a/maven/android-compat/src/test/java/android/widget/TextViewRelativeDrawablesTest.java +++ b/maven/android-compat/src/test/java/android/widget/TextViewRelativeDrawablesTest.java @@ -27,7 +27,7 @@ import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/TextViewSpannableBufferTest.java b/maven/android-compat/src/test/java/android/widget/TextViewSpannableBufferTest.java index 2fc10d10635..4baea4ced6a 100644 --- a/maven/android-compat/src/test/java/android/widget/TextViewSpannableBufferTest.java +++ b/maven/android-compat/src/test/java/android/widget/TextViewSpannableBufferTest.java @@ -27,7 +27,7 @@ import android.text.style.ForegroundColorSpan; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/TextViewTransitionTest.java b/maven/android-compat/src/test/java/android/widget/TextViewTransitionTest.java index 7c54e844bfa..63ba1cc2ba8 100644 --- a/maven/android-compat/src/test/java/android/widget/TextViewTransitionTest.java +++ b/maven/android-compat/src/test/java/android/widget/TextViewTransitionTest.java @@ -25,7 +25,7 @@ import android.graphics.drawable.ColorDrawable; import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/android/widget/TimePickerNullLegacyTimeTest.java b/maven/android-compat/src/test/java/android/widget/TimePickerNullLegacyTimeTest.java index 1f77a6ba0ec..c701cd6e410 100644 --- a/maven/android-compat/src/test/java/android/widget/TimePickerNullLegacyTimeTest.java +++ b/maven/android-compat/src/test/java/android/widget/TimePickerNullLegacyTimeTest.java @@ -23,7 +23,7 @@ package android.widget; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/android/widget/ViewAnimatorRemovalTest.java b/maven/android-compat/src/test/java/android/widget/ViewAnimatorRemovalTest.java index c9af48d36c8..adec8146aa4 100644 --- a/maven/android-compat/src/test/java/android/widget/ViewAnimatorRemovalTest.java +++ b/maven/android-compat/src/test/java/android/widget/ViewAnimatorRemovalTest.java @@ -25,7 +25,7 @@ import android.util.SparseArray; import android.view.View; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/androidx/activity/ComponentActivitySaveLifecycleTest.java b/maven/android-compat/src/test/java/androidx/activity/ComponentActivitySaveLifecycleTest.java index 12c325a50ec..669ed1ebb72 100644 --- a/maven/android-compat/src/test/java/androidx/activity/ComponentActivitySaveLifecycleTest.java +++ b/maven/android-compat/src/test/java/androidx/activity/ComponentActivitySaveLifecycleTest.java @@ -26,7 +26,7 @@ import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleRegistry; import androidx.savedstate.SavedStateRegistry; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/androidx/activity/GalleryLauncherUnregisterTest.java b/maven/android-compat/src/test/java/androidx/activity/GalleryLauncherUnregisterTest.java index bda78424041..e99fb353e08 100644 --- a/maven/android-compat/src/test/java/androidx/activity/GalleryLauncherUnregisterTest.java +++ b/maven/android-compat/src/test/java/androidx/activity/GalleryLauncherUnregisterTest.java @@ -31,8 +31,8 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.ui.events.ActionEvent; import com.codename1.ui.events.ActionListener; diff --git a/maven/android-compat/src/test/java/androidx/activity/GalleryResultAfterRecreateTest.java b/maven/android-compat/src/test/java/androidx/activity/GalleryResultAfterRecreateTest.java index c69284c6e55..bbee6318884 100644 --- a/maven/android-compat/src/test/java/androidx/activity/GalleryResultAfterRecreateTest.java +++ b/maven/android-compat/src/test/java/androidx/activity/GalleryResultAfterRecreateTest.java @@ -32,7 +32,7 @@ import android.content.Intent; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; +import com.codename1.compat.testing.HeadlessImplementation; import com.codename1.ui.Display; import com.codename1.ui.events.ActionEvent; import com.codename1.ui.events.ActionListener; diff --git a/maven/android-compat/src/test/java/androidx/activity/OnBackPressedDispatcherTest.java b/maven/android-compat/src/test/java/androidx/activity/OnBackPressedDispatcherTest.java index 0b38cce84a3..29902c060fd 100644 --- a/maven/android-compat/src/test/java/androidx/activity/OnBackPressedDispatcherTest.java +++ b/maven/android-compat/src/test/java/androidx/activity/OnBackPressedDispatcherTest.java @@ -29,7 +29,7 @@ import java.util.ArrayList; import java.util.List; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/androidx/activity/SynchronousResultPostedTest.java b/maven/android-compat/src/test/java/androidx/activity/SynchronousResultPostedTest.java index 7e7c3a566a0..0290750c1cb 100644 --- a/maven/android-compat/src/test/java/androidx/activity/SynchronousResultPostedTest.java +++ b/maven/android-compat/src/test/java/androidx/activity/SynchronousResultPostedTest.java @@ -34,7 +34,7 @@ import androidx.activity.result.contract.ActivityResultContract; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.BeforeClass; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/androidx/constraintlayout/widget/ConstraintLayoutTest.java b/maven/android-compat/src/test/java/androidx/constraintlayout/widget/ConstraintLayoutTest.java index 88ad6e15c7a..cfce97e534f 100644 --- a/maven/android-compat/src/test/java/androidx/constraintlayout/widget/ConstraintLayoutTest.java +++ b/maven/android-compat/src/test/java/androidx/constraintlayout/widget/ConstraintLayoutTest.java @@ -29,7 +29,7 @@ import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Before; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java b/maven/android-compat/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java index 6a9ba8fddd5..899a7922145 100644 --- a/maven/android-compat/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java +++ b/maven/android-compat/src/test/java/androidx/lifecycle/LifecycleRegistryTest.java @@ -27,7 +27,7 @@ import java.util.ArrayList; import java.util.List; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/androidx/lifecycle/LiveDataTest.java b/maven/android-compat/src/test/java/androidx/lifecycle/LiveDataTest.java index 42a1be26b3b..61dfb9b2c41 100644 --- a/maven/android-compat/src/test/java/androidx/lifecycle/LiveDataTest.java +++ b/maven/android-compat/src/test/java/androidx/lifecycle/LiveDataTest.java @@ -26,7 +26,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/androidx/lifecycle/MediatorReactivationTest.java b/maven/android-compat/src/test/java/androidx/lifecycle/MediatorReactivationTest.java index af7572e5535..abed612750a 100644 --- a/maven/android-compat/src/test/java/androidx/lifecycle/MediatorReactivationTest.java +++ b/maven/android-compat/src/test/java/androidx/lifecycle/MediatorReactivationTest.java @@ -21,7 +21,7 @@ * need additional information or have any questions. */ package androidx.lifecycle; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import androidx.arch.core.util.Function; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/androidx/recyclerview/widget/DiffUtilTest.java b/maven/android-compat/src/test/java/androidx/recyclerview/widget/DiffUtilTest.java index 7ccb16d5321..bef48d00a33 100644 --- a/maven/android-compat/src/test/java/androidx/recyclerview/widget/DiffUtilTest.java +++ b/maven/android-compat/src/test/java/androidx/recyclerview/widget/DiffUtilTest.java @@ -25,7 +25,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/androidx/recyclerview/widget/GridRowSizeTest.java b/maven/android-compat/src/test/java/androidx/recyclerview/widget/GridRowSizeTest.java index 4069effb9cc..ca8b5b26b2e 100644 --- a/maven/android-compat/src/test/java/androidx/recyclerview/widget/GridRowSizeTest.java +++ b/maven/android-compat/src/test/java/androidx/recyclerview/widget/GridRowSizeTest.java @@ -24,7 +24,7 @@ import android.view.View; import android.view.ViewGroup; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/DrawableForDensityTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/DrawableForDensityTest.java index b3dbf9f06a3..92c961845d6 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/DrawableForDensityTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/DrawableForDensityTest.java @@ -27,8 +27,8 @@ import com.codename1.android.rescompiler.ResourceCompiler; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/FontCacheTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/FontCacheTest.java index fcabdee781e..bb1891176c5 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/FontCacheTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/FontCacheTest.java @@ -24,7 +24,7 @@ import android.graphics.Typeface; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/LocaleQualifierTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/LocaleQualifierTest.java index 2a247a9968a..54b7549f4f3 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/LocaleQualifierTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/LocaleQualifierTest.java @@ -22,7 +22,7 @@ */ package com.codename1.androidcompat.runtime; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/MenuXmlOnClickTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/MenuXmlOnClickTest.java index 6703130cfdf..3e31dbd416b 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/MenuXmlOnClickTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/MenuXmlOnClickTest.java @@ -26,7 +26,7 @@ import android.view.MenuItem; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ResTableRoundTripTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ResTableRoundTripTest.java index d758d5342c1..127106b52a3 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ResTableRoundTripTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ResTableRoundTripTest.java @@ -23,7 +23,7 @@ package com.codename1.androidcompat.runtime; import com.codename1.android.rescompiler.ResourceCompiler; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ResourceSelectionRegressionTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ResourceSelectionRegressionTest.java index e6c67d3e42d..5c0fdff3fb2 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ResourceSelectionRegressionTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ResourceSelectionRegressionTest.java @@ -23,7 +23,7 @@ package com.codename1.androidcompat.runtime; import android.content.res.Resources; import android.util.TypedValue; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.*; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ScriptQualifierTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ScriptQualifierTest.java index 21152d7a5ef..59084ec7086 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ScriptQualifierTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ScriptQualifierTest.java @@ -22,7 +22,7 @@ */ package com.codename1.androidcompat.runtime; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ShareStreamTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ShareStreamTest.java index c9a9001285b..d47a3278fdc 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ShareStreamTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/ShareStreamTest.java @@ -27,7 +27,7 @@ import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesApplyDeferredTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesApplyDeferredTest.java index 0dea6c78e23..2c1e7069791 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesApplyDeferredTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesApplyDeferredTest.java @@ -26,7 +26,7 @@ import android.content.SharedPreferences; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.io.Storage; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesClearListenerTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesClearListenerTest.java index b9b2438837b..cb78a4f3d15 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesClearListenerTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesClearListenerTest.java @@ -26,7 +26,7 @@ import android.content.SharedPreferences; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import java.util.ArrayList; import java.util.Collections; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesCommitFailureTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesCommitFailureTest.java index 322b2b04f71..890747c2db6 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesCommitFailureTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesCommitFailureTest.java @@ -26,7 +26,7 @@ import android.content.SharedPreferences; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import com.codename1.io.Storage; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesEditOrderTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesEditOrderTest.java index e0b7569aa6d..f6cb9fe33ec 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesEditOrderTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesEditOrderTest.java @@ -27,7 +27,7 @@ import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesEditorReuseTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesEditorReuseTest.java index bde5737819d..fe237cfd531 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesEditorReuseTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesEditorReuseTest.java @@ -26,7 +26,7 @@ import android.content.SharedPreferences; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesStringTypeTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesStringTypeTest.java index ac94a949922..767da845f47 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesStringTypeTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesStringTypeTest.java @@ -26,7 +26,7 @@ import android.content.SharedPreferences; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesUnchangedValueTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesUnchangedValueTest.java index c99130b29b2..89dcc828f03 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesUnchangedValueTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesUnchangedValueTest.java @@ -26,7 +26,7 @@ import android.content.SharedPreferences; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import java.util.ArrayList; import java.util.Arrays; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesWeakListenerTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesWeakListenerTest.java index e75298e0e3c..c2de3a4fc7b 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesWeakListenerTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/SharedPreferencesWeakListenerTest.java @@ -26,7 +26,7 @@ import android.content.SharedPreferences; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import java.lang.ref.WeakReference; import java.util.ArrayList; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/XmlPullParserTest.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/XmlPullParserTest.java index 7a65f7e296a..5abd15a6c9d 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/XmlPullParserTest.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/runtime/XmlPullParserTest.java @@ -31,7 +31,7 @@ import android.util.TypedValue; import android.util.Xml; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; import org.xmlpull.v1.XmlPullParser; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/testing/AndroidTestSupport.java b/maven/android-compat/src/test/java/com/codename1/androidcompat/testing/AndroidTestSupport.java index 23ba3150518..061826182ae 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/testing/AndroidTestSupport.java +++ b/maven/android-compat/src/test/java/com/codename1/androidcompat/testing/AndroidTestSupport.java @@ -30,6 +30,7 @@ import com.codename1.androidcompat.runtime.AndroidApp; import com.codename1.androidcompat.runtime.AndroidRuntime; +import com.codename1.compat.testing.HeadlessImplementation; /// The Android runtime started on [HeadlessImplementation] with an /// application that has no resources of its own, so tests can construct, diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/BufferedReaderMarkTest.java b/maven/android-compat/src/test/java/com/codename1/compat/jdk/BufferedReaderMarkTest.java similarity index 98% rename from maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/BufferedReaderMarkTest.java rename to maven/android-compat/src/test/java/com/codename1/compat/jdk/BufferedReaderMarkTest.java index 8dad31566a7..dd529209088 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/BufferedReaderMarkTest.java +++ b/maven/android-compat/src/test/java/com/codename1/compat/jdk/BufferedReaderMarkTest.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/BufferedStreamsClosedTest.java b/maven/android-compat/src/test/java/com/codename1/compat/jdk/BufferedStreamsClosedTest.java similarity index 99% rename from maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/BufferedStreamsClosedTest.java rename to maven/android-compat/src/test/java/com/codename1/compat/jdk/BufferedStreamsClosedTest.java index f93466f1bc6..f9ae395b46c 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/BufferedStreamsClosedTest.java +++ b/maven/android-compat/src/test/java/com/codename1/compat/jdk/BufferedStreamsClosedTest.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/BufferedWriterClosedTest.java b/maven/android-compat/src/test/java/com/codename1/compat/jdk/BufferedWriterClosedTest.java similarity index 98% rename from maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/BufferedWriterClosedTest.java rename to maven/android-compat/src/test/java/com/codename1/compat/jdk/BufferedWriterClosedTest.java index 8f0be466350..f7d1e1746b6 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/BufferedWriterClosedTest.java +++ b/maven/android-compat/src/test/java/com/codename1/compat/jdk/BufferedWriterClosedTest.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.IOException; import java.io.Writer; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileCanonicalPathTest.java b/maven/android-compat/src/test/java/com/codename1/compat/jdk/FileCanonicalPathTest.java similarity index 95% rename from maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileCanonicalPathTest.java rename to maven/android-compat/src/test/java/com/codename1/compat/jdk/FileCanonicalPathTest.java index cf47edef676..faf28f35440 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileCanonicalPathTest.java +++ b/maven/android-compat/src/test/java/com/codename1/compat/jdk/FileCanonicalPathTest.java @@ -20,9 +20,9 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileRenameFailedDeleteTest.java b/maven/android-compat/src/test/java/com/codename1/compat/jdk/FileRenameFailedDeleteTest.java similarity index 94% rename from maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileRenameFailedDeleteTest.java rename to maven/android-compat/src/test/java/com/codename1/compat/jdk/FileRenameFailedDeleteTest.java index cc3daffd3c7..796ce38d1ba 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileRenameFailedDeleteTest.java +++ b/maven/android-compat/src/test/java/com/codename1/compat/jdk/FileRenameFailedDeleteTest.java @@ -20,11 +20,11 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.After; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileRenameOntoItselfTest.java b/maven/android-compat/src/test/java/com/codename1/compat/jdk/FileRenameOntoItselfTest.java similarity index 94% rename from maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileRenameOntoItselfTest.java rename to maven/android-compat/src/test/java/com/codename1/compat/jdk/FileRenameOntoItselfTest.java index 3916d6599b0..e13e08d7df7 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileRenameOntoItselfTest.java +++ b/maven/android-compat/src/test/java/com/codename1/compat/jdk/FileRenameOntoItselfTest.java @@ -20,11 +20,11 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import com.codename1.androidcompat.testing.AndroidTestSupport; -import com.codename1.androidcompat.testing.HeadlessImplementation; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; import org.junit.After; import org.junit.Rule; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileTypeContractTest.java b/maven/android-compat/src/test/java/com/codename1/compat/jdk/FileTypeContractTest.java similarity index 95% rename from maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileTypeContractTest.java rename to maven/android-compat/src/test/java/com/codename1/compat/jdk/FileTypeContractTest.java index 6f2cd7459d4..e50e6e716d1 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/FileTypeContractTest.java +++ b/maven/android-compat/src/test/java/com/codename1/compat/jdk/FileTypeContractTest.java @@ -20,11 +20,11 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import android.content.Intent; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/PrintWriterClosedTest.java b/maven/android-compat/src/test/java/com/codename1/compat/jdk/PrintWriterClosedTest.java similarity index 98% rename from maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/PrintWriterClosedTest.java rename to maven/android-compat/src/test/java/com/codename1/compat/jdk/PrintWriterClosedTest.java index 89dfc59adad..ddd28192c3e 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/jdk/PrintWriterClosedTest.java +++ b/maven/android-compat/src/test/java/com/codename1/compat/jdk/PrintWriterClosedTest.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.Writer; diff --git a/maven/android-compat/src/test/java/com/google/android/material/MaterialComponentsTest.java b/maven/android-compat/src/test/java/com/google/android/material/MaterialComponentsTest.java index 861278c55e6..3a9d7277830 100644 --- a/maven/android-compat/src/test/java/com/google/android/material/MaterialComponentsTest.java +++ b/maven/android-compat/src/test/java/com/google/android/material/MaterialComponentsTest.java @@ -53,7 +53,7 @@ import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.theme.MaterialComponentsViewInflater; -import com.codename1.androidcompat.testing.MainThreadRule; +import com.codename1.compat.testing.MainThreadRule; import org.junit.Rule; import org.junit.Test; diff --git a/maven/build-engine/src/main/java/com/codename1/maven/AndroidRemapper.java b/maven/build-engine/src/main/java/com/codename1/maven/AndroidRemapper.java index 8ddd62b2e87..cfa37dabd3f 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/AndroidRemapper.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/AndroidRemapper.java @@ -30,27 +30,17 @@ import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; -import org.objectweb.asm.commons.ClassRemapper; -import org.objectweb.asm.commons.Remapper; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.nio.charset.Charset; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; /// Relocates an application's compiled Android code onto the compatibility /// runtime, and ships the runtime with it. @@ -101,12 +91,19 @@ public final class AndroidRemapper { "(L" + APPLICATION + ";L" + SAVED_STATE_HANDLE + ";)V", "(L" + SAVED_STATE_HANDLE + ";)V", "(L" + APPLICATION + ";)V", "()V"}; + /// The Android layer's naming rules. + public static final Relocation RELOCATION = new Relocation("Android", AndroidResourceRunner.COMPAT_ARTIFACT, + TARGET, PREFIXES, Relocation.JDK_SHIMS, RUNTIME_PACKAGE, RUNTIME_TARGET); + private static final ClassRelocator ANDROID = new ClassRelocator(RELOCATION); + private final File classesDir; private final File compatJar; private final File onClickNames; private final Log log; private boolean shipRuntime = true; private final List handlerDirs = new ArrayList(); + private final List supportJars = new ArrayList(); + private ClassRelocator relocator = ANDROID; public AndroidRemapper(File classesDir, File compatJar, File onClickNames, Log log) { this.classesDir = classesDir; @@ -135,158 +132,48 @@ public AndroidRemapper withHandlerDirectories(List dirs) { return this; } - /// JDK classes Android code uses that the Codename One runtime lacks, - /// redirected to the compatibility runtime's implementations. - static final String[][] JDK_SHIMS = { - {"java/io/BufferedReader", "com/codename1/androidcompat/jdk/BufferedReader"}, - {"java/io/BufferedWriter", "com/codename1/androidcompat/jdk/BufferedWriter"}, - {"java/io/BufferedInputStream", "com/codename1/androidcompat/jdk/BufferedInputStream"}, - {"java/io/BufferedOutputStream", "com/codename1/androidcompat/jdk/BufferedOutputStream"}, - {"java/io/File", "com/codename1/androidcompat/jdk/File"}, - {"java/io/FileFilter", "com/codename1/androidcompat/jdk/FileFilter"}, - {"java/io/FilenameFilter", "com/codename1/androidcompat/jdk/FilenameFilter"}, - {"java/io/FileInputStream", "com/codename1/androidcompat/jdk/FileInputStream"}, - {"java/io/FileOutputStream", "com/codename1/androidcompat/jdk/FileOutputStream"}, - {"java/io/FileReader", "com/codename1/androidcompat/jdk/FileReader"}, - {"java/io/FileWriter", "com/codename1/androidcompat/jdk/FileWriter"}, - {"java/io/FilterInputStream", "com/codename1/androidcompat/jdk/FilterInputStream"}, - {"java/io/FilterOutputStream", "com/codename1/androidcompat/jdk/FilterOutputStream"}, - {"java/io/PrintWriter", "com/codename1/androidcompat/jdk/PrintWriter"}, - // Every Codename One stream, reader and writer is AutoCloseable, and - // close() is the interface's only method, so code holding a Closeable - // runs unchanged against it. - {"java/io/Closeable", "java/lang/AutoCloseable"}, - }; - - /// The relocated internal name, or `name` unchanged. - public static String map(String name) { - if (name == null) { - return null; - } - for (String[] shim : JDK_SHIMS) { - if (shim[0].equals(name)) { - return shim[1]; - } - } - if (name.startsWith(RUNTIME_PACKAGE)) { - return RUNTIME_TARGET + name.substring(RUNTIME_PACKAGE.length()); - } - for (String p : PREFIXES) { - if (name.startsWith(p)) { - return TARGET + name; + /// Jars the runtime needs beside it in the application -- the shared JDK + /// classes ([Relocation#JDK_ARTIFACT]) -- extracted the way the runtime is. + public AndroidRemapper withSupportJars(List jars) { + if (jars != null) { + for (File j : jars) { + if (j != null && j.isFile()) { + supportJars.add(j); + } } } - return name; + return this; } - static final Remapper REMAPPER = new Remapper() { - @Override - public String map(String internalName) { - return AndroidRemapper.map(internalName); - } - }; - - static byte[] remap(byte[] in) { - ClassReader cr = new ClassReader(in); - ClassWriter cw = new ClassWriter(0); - cr.accept(new ClassRemapper(new PostRemapFixes(cw), REMAPPER), 0); - return cw.toByteArray(); + /// Relocates by every active layer's rules at once, when the application + /// has more than Android sources; see [ClassRelocator]. + public AndroidRemapper withRelocator(ClassRelocator composed) { + relocator = composed; + return this; } - /// What the remapper itself does not cover: two source interfaces can map - /// to one target (`Closeable` and `AutoCloseable` both become - /// `AutoCloseable`), and a class file that names an interface twice is - /// rejected by the JVM; and Kotlin's metadata strings (below). - private static final class PostRemapFixes extends ClassVisitor { - private final Set methods = new LinkedHashSet(); - private String className; - - PostRemapFixes(ClassVisitor next) { - super(Opcodes.ASM9, next); - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, - String[] interfaces) { - className = name; - String[] distinct = interfaces; - if (interfaces != null && interfaces.length > 1) { - Set seen = new LinkedHashSet(Arrays.asList(interfaces)); - if (seen.size() != interfaces.length) { - distinct = seen.toArray(new String[seen.size()]); - } - } - super.visit(version, access, name, signature, superName, distinct); - } - - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, - String signature, String[] exceptions) { - if (!methods.add(name + descriptor)) { - throw new IllegalArgumentException("Android remapping collapses overloads in " - + className + ": " + name + descriptor - + ". Rename the overload or use one AutoCloseable signature."); - } - return super.visitMethod(access, name, descriptor, signature, exceptions); - } - - /// Kotlin records the JVM descriptors of a class's members as plain - /// strings in `@kotlin.Metadata`'s `d2` array. The remapper never sees - /// those, so without this they would keep naming android/... types. - @Override - public org.objectweb.asm.AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - org.objectweb.asm.AnnotationVisitor av = super.visitAnnotation(descriptor, visible); - if (av == null || !"Lkotlin/Metadata;".equals(descriptor)) { - return av; - } - return new org.objectweb.asm.AnnotationVisitor(Opcodes.ASM9, av) { - @Override - public org.objectweb.asm.AnnotationVisitor visitArray(String name) { - org.objectweb.asm.AnnotationVisitor array = super.visitArray(name); - if (array == null || !"d2".equals(name)) { - return array; - } - return new org.objectweb.asm.AnnotationVisitor(Opcodes.ASM9, array) { - @Override - public void visit(String n, Object value) { - super.visit(n, value instanceof String ? mapMetadataString((String) value) : value); - } - }; - } - }; - } + /// The relocated internal name by the Android layer's rules alone, or + /// `name` unchanged. + public static String map(String name) { + return ANDROID.map(name); } - /// A `kotlin.Metadata` string: a method or field descriptor, or a name - /// that is left alone. - static String mapMetadataString(String s) { - if (s.length() == 0) { - return s; - } - try { - if (s.charAt(0) == '(') { - return REMAPPER.mapMethodDesc(s); - } - if ((s.charAt(0) == 'L' && s.endsWith(";")) || s.charAt(0) == '[') { - return REMAPPER.mapDesc(s); - } - } catch (RuntimeException e) { - // Not a descriptor after all (a name that happens to start with - // L or a bracket): keep it as written. - return s; - } - return s; + static byte[] remap(byte[] in) { + return ANDROID.remap(in); } public void run() throws BuildException { try { List appClasses = new ArrayList(); - remapDirectory(classesDir, appClasses); + relocator.remapDirectory(classesDir, appClasses, log); if (!shipRuntime) { log.info("Relocated " + appClasses.size() + " application classes"); return; } - int runtime = extractRuntime(); + int runtime = relocator.extractRuntime(compatJar, classesDir); + for (File jar : supportJars) { + runtime += relocator.extractRuntime(jar, classesDir); + } int handlers = writeOnClickDispatch(appClasses); int fragments = writeFragmentFactory(); int viewModels = writeViewModelFactory(); @@ -307,104 +194,6 @@ public void run() throws BuildException { } } - private void remapDirectory(File dir, List out) throws IOException { - File[] files = dir.listFiles(); - if (files == null) { - return; - } - for (File f : files) { - if (f.isDirectory()) { - String rel = relative(f) + "/"; - // The relocated runtime is extracted, not application code. - if (rel.equals(TARGET) || rel.startsWith(TARGET)) { - continue; - } - remapDirectory(f, out); - } else if (f.getName().endsWith(".class")) { - byte[] in = Files.readAllBytes(f.toPath()); - byte[] remapped = remap(in); - String name = relative(f); - name = name.substring(0, name.length() - ".class".length()); - String mapped = map(name); - if (!mapped.equals(name)) { - // Application code inside android.* (a support shim - // the app carries itself) moves with the rest. - File dest = new File(classesDir, mapped + ".class"); - dest.getParentFile().mkdirs(); - write(dest, remapped); - if (!f.delete()) { - log.warn("Could not delete " + f); - } - out.add(mapped); - } else { - if (!Arrays.equals(in, remapped)) { - write(f, remapped); - } - out.add(name); - } - } - } - } - - private String relative(File f) { - String base = classesDir.getAbsolutePath(); - String p = f.getAbsolutePath(); - return p.substring(base.length() + 1).replace(File.separatorChar, '/'); - } - - /// `dest` when it lies inside the classes directory. An entry name with - /// `..` (or an absolute one) would write elsewhere; the runtime jar never - /// has one, so meeting one means the artifact is not what it should be. - private File inside(File dest, String entry) throws IOException { - String root = classesDir.getCanonicalPath() + File.separator; - if (!dest.getCanonicalPath().startsWith(root)) { - throw new IOException(compatJar + " entry " + entry + " resolves outside " + classesDir); - } - return dest; - } - - private int extractRuntime() throws IOException { - int count = 0; - ZipFile zip = new ZipFile(compatJar); - try { - Enumeration en = zip.entries(); - while (en.hasMoreElements()) { - ZipEntry e = en.nextElement(); - String name = e.getName(); - if (e.isDirectory() || name.startsWith("META-INF/")) { - continue; - } - byte[] data = read(zip.getInputStream(e)); - if (name.endsWith(".class")) { - String cls = name.substring(0, name.length() - ".class".length()); - File dest = inside(new File(classesDir, map(cls) + ".class"), name); - writeIfDifferent(dest, remap(data)); - count++; - } else { - // Resource tables and assets are compared byte for byte - // too: a rebuilt table routinely keeps its length while - // its contents change, and a length check alone kept the - // previous runtime's copy. - writeIfDifferent(inside(new File(classesDir, name), name), data); - } - } - } finally { - zip.close(); - } - return count; - } - - /// Writes `data` to `dest` unless the file already holds exactly those - /// bytes, so an incremental build leaves unchanged outputs (and their - /// timestamps) alone. - private static void writeIfDifferent(File dest, byte[] data) throws IOException { - if (dest.isFile() && dest.length() == data.length && Arrays.equals(Files.readAllBytes(dest.toPath()), data)) { - return; - } - dest.getParentFile().mkdirs(); - write(dest, data); - } - /// Writes `OnClickDispatch.dispatch(Object, String, View)`: for each layout /// `android:onClick` name and each class declaring a public instance /// `name(View)` method, a type test and a direct call. Menu items' @@ -442,14 +231,14 @@ private int writeOnClickDispatch(List appClasses) throws IOException { classFiles.add(new File(classesDir, cls + ".class")); } for (File dir : handlerDirs) { - collectClassFiles(dir, classFiles); + ClassRelocator.collectClassFiles(dir, classFiles); } for (final File f : classFiles) { final ClassReader cr = new ClassReader(Files.readAllBytes(f.toPath())); if ((cr.getAccess() & Opcodes.ACC_INTERFACE) != 0) { continue; } - final String cls = map(cr.getClassName()); + final String cls = relocator.map(cr.getClassName()); final Set wanted = names; cr.accept(new ClassVisitor(Opcodes.ASM9) { @Override @@ -459,7 +248,7 @@ public MethodVisitor visitMethod(int access, String name, String desc, String si } // Mapped first: a handler directory's classes may not // be relocated yet (Gradle relocates Kotlin's after javac). - String mapped = REMAPPER.mapMethodDesc(desc); + String mapped = relocator.remapper().mapMethodDesc(desc); if (mapped.equals(viewDesc) && wanted.contains(name)) { handlers.add(new String[] {cls, name}); } else if ((mapped.equals(menuVoidDesc) || mapped.equals(menuBoolDesc)) @@ -534,7 +323,7 @@ public MethodVisitor visitMethod(int access, String name, String desc, String si cw.visitEnd(); File out = new File(classesDir, ON_CLICK_DISPATCH + ".class"); out.getParentFile().mkdirs(); - write(out, cw.toByteArray()); + ClassRelocator.write(out, cw.toByteArray()); for (String n : menuNames) { boolean found = false; for (String[] h : menuHandlers) { @@ -575,16 +364,16 @@ public MethodVisitor visitMethod(int access, String name, String desc, String si private int writeFragmentFactory() throws IOException { final Map supers = new HashMap(); List all = new ArrayList(); - collectClassFiles(classesDir, all); + ClassRelocator.collectClassFiles(classesDir, all); for (File dir : handlerDirs) { - collectClassFiles(dir, all); + ClassRelocator.collectClassFiles(dir, all); } final Map candidates = new HashMap(); for (File f : all) { ClassReader cr = new ClassReader(Files.readAllBytes(f.toPath())); - final String cls = map(cr.getClassName()); + final String cls = relocator.map(cr.getClassName()); if (cr.getSuperName() != null) { - supers.put(cls, map(cr.getSuperName())); + supers.put(cls, relocator.map(cr.getSuperName())); } int access = cr.getAccess(); if ((access & (Opcodes.ACC_INTERFACE | Opcodes.ACC_ABSTRACT)) != 0 || (access & Opcodes.ACC_PUBLIC) == 0 @@ -644,7 +433,7 @@ public MethodVisitor visitMethod(int acc, String name, String desc, String sig, cw.visitEnd(); File out = new File(classesDir, FRAGMENT_FACTORY + ".class"); out.getParentFile().mkdirs(); - write(out, cw.toByteArray()); + ClassRelocator.write(out, cw.toByteArray()); return fragments.size(); } @@ -660,16 +449,16 @@ public MethodVisitor visitMethod(int acc, String name, String desc, String sig, private int writeViewModelFactory() throws IOException { final Map supers = new HashMap(); List all = new ArrayList(); - collectClassFiles(classesDir, all); + ClassRelocator.collectClassFiles(classesDir, all); for (File dir : handlerDirs) { - collectClassFiles(dir, all); + ClassRelocator.collectClassFiles(dir, all); } final Map> ctorOf = new HashMap>(); for (File f : all) { ClassReader cr = new ClassReader(Files.readAllBytes(f.toPath())); - final String cls = map(cr.getClassName()); + final String cls = relocator.map(cr.getClassName()); if (cr.getSuperName() != null) { - supers.put(cls, map(cr.getSuperName())); + supers.put(cls, relocator.map(cr.getSuperName())); } int access = cr.getAccess(); if ((access & (Opcodes.ACC_INTERFACE | Opcodes.ACC_ABSTRACT)) != 0 || (access & Opcodes.ACC_PUBLIC) == 0 @@ -681,7 +470,7 @@ private int writeViewModelFactory() throws IOException { @Override public MethodVisitor visitMethod(int acc, String name, String desc, String sig, String[] ex) { if (name.equals("") && (acc & Opcodes.ACC_PUBLIC) != 0) { - ctors.add(REMAPPER.mapMethodDesc(desc)); + ctors.add(relocator.remapper().mapMethodDesc(desc)); } return null; } @@ -760,7 +549,7 @@ public MethodVisitor visitMethod(int acc, String name, String desc, String sig, cw.visitEnd(); File out = new File(classesDir, VIEW_MODEL_FACTORY + ".class"); out.getParentFile().mkdirs(); - write(out, cw.toByteArray()); + ClassRelocator.write(out, cw.toByteArray()); return models.size(); } @@ -802,7 +591,7 @@ private int writeJsInterfaceDispatch(List appClasses) throws IOException classFiles.add(new File(classesDir, cls + ".class")); } for (File dir : handlerDirs) { - collectClassFiles(dir, classFiles); + ClassRelocator.collectClassFiles(dir, classFiles); } final List methods = new ArrayList(); // Every scanned class's superclass, and the public instance methods @@ -817,9 +606,9 @@ private int writeJsInterfaceDispatch(List appClasses) throws IOException if ((cr.getAccess() & Opcodes.ACC_INTERFACE) != 0) { continue; } - final String cls = map(cr.getClassName()); + final String cls = relocator.map(cr.getClassName()); if (cr.getSuperName() != null) { - supers.put(cls, map(cr.getSuperName())); + supers.put(cls, relocator.map(cr.getSuperName())); } final List plain = new ArrayList(); unannotated.put(cls, plain); @@ -837,7 +626,7 @@ public MethodVisitor visitMethod(final int access, final String name, final Stri @Override public void visitEnd() { if (!annotated) { - plain.add(name + REMAPPER.mapMethodDesc(desc)); + plain.add(name + relocator.remapper().mapMethodDesc(desc)); } } @@ -846,7 +635,7 @@ public org.objectweb.asm.AnnotationVisitor visitAnnotation(String annotation, bo for (String a : JS_INTERFACE_ANNOTATIONS) { if (a.equals(annotation)) { annotated = true; - String mapped = REMAPPER.mapMethodDesc(desc); + String mapped = relocator.remapper().mapMethodDesc(desc); if (jsCallable(mapped)) { methods.add(new String[] {cls, name, mapped}); } else { @@ -994,7 +783,7 @@ public org.objectweb.asm.AnnotationVisitor visitAnnotation(String annotation, bo cw.visitEnd(); File out = new File(classesDir, JS_INTERFACE_DISPATCH + ".class"); out.getParentFile().mkdirs(); - write(out, cw.toByteArray()); + ClassRelocator.write(out, cw.toByteArray()); return methods.size(); } @@ -1047,41 +836,4 @@ private static String jsConversion(org.objectweb.asm.Type t) { return null; } } - - private static void collectClassFiles(File dir, List out) { - File[] files = dir.listFiles(); - if (files == null) { - return; - } - for (File f : files) { - if (f.isDirectory()) { - collectClassFiles(f, out); - } else if (f.getName().endsWith(".class")) { - out.add(f); - } - } - } - - private static byte[] read(InputStream in) throws IOException { - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - byte[] buf = new byte[16384]; - int n; - while ((n = in.read(buf)) > 0) { - out.write(buf, 0, n); - } - return out.toByteArray(); - } finally { - in.close(); - } - } - - private static void write(File f, byte[] data) throws IOException { - OutputStream out = new FileOutputStream(f); - try { - out.write(data); - } finally { - out.close(); - } - } } diff --git a/maven/build-engine/src/main/java/com/codename1/maven/BytecodeCompliance.java b/maven/build-engine/src/main/java/com/codename1/maven/BytecodeCompliance.java index bf1746a41e0..113c695fe10 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/BytecodeCompliance.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/BytecodeCompliance.java @@ -978,7 +978,7 @@ private void indexArchive(File archive, Map index) throws InputStream fis = new BufferedInputStream(new FileInputStream(archive)); try { indexArchiveStream(fis, archive.getAbsolutePath(), index, - archive.getName().startsWith(AndroidResourceRunner.COMPAT_ARTIFACT + "-")); + CompatLayers.EVERY.layerOf(archive) != null); } finally { fis.close(); } @@ -988,7 +988,7 @@ private void indexArchiveStream(InputStream archiveStream, String sourcePrefix, indexArchiveStream(archiveStream, sourcePrefix, index, false); } - /// `relocated`: the Android compatibility runtime, which the remap step + /// `relocated`: a compatibility layer's runtime, which the remap step /// ships relocated; its classes are indexed under those names too, so a /// pass checked before the runtime is copied in (Gradle's Kotlin pass) /// resolves the relocated references. @@ -1012,7 +1012,7 @@ private void indexArchiveStream(InputStream archiveStream, String sourcePrefix, index.put(metadata.name, metadata); } if (relocated) { - ClassMetadata moved = readClassMetadata(new ByteArrayInputStream(AndroidRemapper.remap(bytes)), + ClassMetadata moved = readClassMetadata(new ByteArrayInputStream(CompatLayers.EVERY.remap(bytes)), sourcePrefix + "!" + entryName); if (moved != null) { index.put(moved.name, moved); diff --git a/maven/build-engine/src/main/java/com/codename1/maven/ClassRelocator.java b/maven/build-engine/src/main/java/com/codename1/maven/ClassRelocator.java new file mode 100644 index 00000000000..17fdfc53d5b --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/maven/ClassRelocator.java @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.build.Log; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.Remapper; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/// Rewrites compiled classes by the rules of one or more [Relocation]s. +/// +/// Several layers can be active in one application -- Android sources beside +/// Swing ones -- and they are applied as ONE pass with the union of their +/// rules. Running a pass per layer would have each walk the other's relocated +/// runtime, and two layers redirect the same JDK classes. +public final class ClassRelocator { + + private final List relocations; + private final Remapper remapper = new Remapper() { + @Override + public String map(String internalName) { + return ClassRelocator.this.map(internalName); + } + }; + + public ClassRelocator(List relocations) { + this.relocations = Collections.unmodifiableList(new ArrayList(relocations)); + } + + public ClassRelocator(Relocation... relocations) { + this(Arrays.asList(relocations)); + } + + public List relocations() { + return relocations; + } + + /// The relocated internal name, or `name` unchanged. + public String map(String name) { + if (name == null) { + return null; + } + for (Relocation r : relocations) { + String shim = r.shim(name); + if (shim != null) { + return shim; + } + } + for (Relocation r : relocations) { + String moved = r.relocate(name); + if (moved != null) { + return moved; + } + } + return name; + } + + /// The name the application was compiled against, or `relocated` + /// unchanged when no layer produced it. + public String original(String relocated) { + for (Relocation r : relocations) { + String o = r.original(relocated); + if (o != null) { + return o; + } + } + return relocated; + } + + /// The layer whose runtime `jar` is, or null. + public Relocation layerOf(File jar) { + for (Relocation r : relocations) { + if (r.isRuntimeJar(jar.getName())) { + return r; + } + } + return null; + } + + public Remapper remapper() { + return remapper; + } + + public byte[] remap(byte[] in) { + ClassReader cr = new ClassReader(in); + ClassWriter cw = new ClassWriter(0); + cr.accept(new ClassRemapper(new PostRemapFixes(cw), remapper), 0); + return cw.toByteArray(); + } + + /// What the remapper itself does not cover: two source interfaces can map + /// to one target (`Closeable` and `AutoCloseable` both become + /// `AutoCloseable`), and a class file that names an interface twice is + /// rejected by the JVM; and Kotlin's metadata strings (below). + private final class PostRemapFixes extends ClassVisitor { + private final Set methods = new LinkedHashSet(); + private String className; + + PostRemapFixes(ClassVisitor next) { + super(Opcodes.ASM9, next); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, + String[] interfaces) { + className = name; + String[] distinct = interfaces; + if (interfaces != null && interfaces.length > 1) { + Set seen = new LinkedHashSet(Arrays.asList(interfaces)); + if (seen.size() != interfaces.length) { + distinct = seen.toArray(new String[seen.size()]); + } + } + super.visit(version, access, name, signature, superName, distinct); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + if (!methods.add(name + descriptor)) { + // The wording predates the other layers and is what an + // Android build has always printed; Closeable is the only + // rule in any layer that maps two names onto one. + throw new IllegalArgumentException("Android remapping collapses overloads in " + + className + ": " + name + descriptor + + ". Rename the overload or use one AutoCloseable signature."); + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + /// Kotlin records the JVM descriptors of a class's members as plain + /// strings in `@kotlin.Metadata`'s `d2` array. The remapper never sees + /// those, so without this they would keep naming android/... types. + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + AnnotationVisitor av = super.visitAnnotation(descriptor, visible); + if (av == null || !"Lkotlin/Metadata;".equals(descriptor)) { + return av; + } + return new AnnotationVisitor(Opcodes.ASM9, av) { + @Override + public AnnotationVisitor visitArray(String name) { + AnnotationVisitor array = super.visitArray(name); + if (array == null || !"d2".equals(name)) { + return array; + } + return new AnnotationVisitor(Opcodes.ASM9, array) { + @Override + public void visit(String n, Object value) { + super.visit(n, value instanceof String ? mapMetadataString((String) value) : value); + } + }; + } + }; + } + } + + /// A `kotlin.Metadata` string: a method or field descriptor, or a name + /// that is left alone. + String mapMetadataString(String s) { + if (s.length() == 0) { + return s; + } + try { + if (s.charAt(0) == '(') { + return remapper.mapMethodDesc(s); + } + if ((s.charAt(0) == 'L' && s.endsWith(";")) || s.charAt(0) == '[') { + return remapper.mapDesc(s); + } + } catch (RuntimeException e) { + // Not a descriptor after all (a name that happens to start with + // L or a bracket): keep it as written. + return s; + } + return s; + } + + /// Relocates every class under `classesDir` in place, adding each one's + /// final internal name to `out`. Directories holding a runtime this + /// relocator already extracted are not application code and are skipped. + public void remapDirectory(File classesDir, List out, Log log) throws IOException { + remapDirectory(classesDir, classesDir, out, log); + } + + private void remapDirectory(File classesDir, File dir, List out, Log log) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + if (isShipped(relative(classesDir, f) + "/")) { + continue; + } + remapDirectory(classesDir, f, out, log); + } else if (f.getName().endsWith(".class")) { + byte[] in = Files.readAllBytes(f.toPath()); + byte[] remapped = remap(in); + String name = relative(classesDir, f); + name = name.substring(0, name.length() - ".class".length()); + String mapped = map(name); + if (!mapped.equals(name)) { + // Application code inside a relocated package (a support + // shim the app carries itself) moves with the rest. + File dest = new File(classesDir, mapped + ".class"); + dest.getParentFile().mkdirs(); + write(dest, remapped); + if (!f.delete()) { + log.warn("Could not delete " + f); + } + out.add(mapped); + } else { + if (!Arrays.equals(in, remapped)) { + write(f, remapped); + } + out.add(name); + } + } + } + } + + /// Whether `rel` (a directory, with its trailing slash) is where some + /// active layer's runtime, or the shared JDK classes, were extracted. + private boolean isShipped(String rel) { + if (rel.startsWith(Relocation.JDK_PACKAGE)) { + return true; + } + for (Relocation r : relocations) { + if (rel.startsWith(r.target())) { + return true; + } + } + return false; + } + + private static String relative(File classesDir, File f) { + String base = classesDir.getAbsolutePath(); + String p = f.getAbsolutePath(); + return p.substring(base.length() + 1).replace(File.separatorChar, '/'); + } + + /// `dest` when it lies inside the classes directory. An entry name with + /// `..` (or an absolute one) would write elsewhere; a runtime jar never + /// has one, so meeting one means the artifact is not what it should be. + private static File inside(File classesDir, File jar, File dest, String entry) throws IOException { + String root = classesDir.getCanonicalPath() + File.separator; + if (!dest.getCanonicalPath().startsWith(root)) { + throw new IOException(jar + " entry " + entry + " resolves outside " + classesDir); + } + return dest; + } + + /// Copies a runtime jar's classes into `classesDir`, relocated, and its + /// other entries as they are. Answers the number of classes. + public int extractRuntime(File jar, File classesDir) throws IOException { + int count = 0; + ZipFile zip = new ZipFile(jar); + try { + Enumeration en = zip.entries(); + while (en.hasMoreElements()) { + ZipEntry e = en.nextElement(); + String name = e.getName(); + if (e.isDirectory() || name.startsWith("META-INF/")) { + continue; + } + byte[] data = read(zip.getInputStream(e)); + if (name.endsWith(".class")) { + String cls = name.substring(0, name.length() - ".class".length()); + File dest = inside(classesDir, jar, new File(classesDir, map(cls) + ".class"), name); + writeIfDifferent(dest, remap(data)); + count++; + } else { + // Resource tables and assets are compared byte for byte + // too: a rebuilt table routinely keeps its length while + // its contents change, and a length check alone kept the + // previous runtime's copy. + writeIfDifferent(inside(classesDir, jar, new File(classesDir, name), name), data); + } + } + } finally { + zip.close(); + } + return count; + } + + /// Writes `data` to `dest` unless the file already holds exactly those + /// bytes, so an incremental build leaves unchanged outputs (and their + /// timestamps) alone. + static void writeIfDifferent(File dest, byte[] data) throws IOException { + if (dest.isFile() && dest.length() == data.length && Arrays.equals(Files.readAllBytes(dest.toPath()), data)) { + return; + } + dest.getParentFile().mkdirs(); + write(dest, data); + } + + static void collectClassFiles(File dir, List out) { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + collectClassFiles(f, out); + } else if (f.getName().endsWith(".class")) { + out.add(f); + } + } + } + + static byte[] read(InputStream in) throws IOException { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[16384]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } finally { + in.close(); + } + } + + static void write(File f, byte[] data) throws IOException { + OutputStream out = new FileOutputStream(f); + try { + out.write(data); + } finally { + out.close(); + } + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java b/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java new file mode 100644 index 00000000000..b91c12d43e3 --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/// Every compatibility layer the build knows, and which of them an +/// application has switched on. +/// +/// A layer is switched on by its runtime jar being on the compile classpath: +/// the project templates add that dependency when the layer's source directory +/// exists, so nothing else has to be configured. +public final class CompatLayers { + + /// Every layer, in the order their rules are tried. + public static final List ALL = Collections.unmodifiableList( + Arrays.asList(AndroidRemapper.RELOCATION)); + + /// A relocator applying every layer's rules. Their packages are disjoint, + /// so it is correct for any class; what it cannot say is which layers an + /// application actually ships -- [#active] does. + public static final ClassRelocator EVERY = new ClassRelocator(ALL); + + private CompatLayers() { + } + + /// The layers whose runtime jar is among `classpath`. + public static List active(Iterable classpath) { + List out = new ArrayList(); + for (Relocation r : ALL) { + if (runtimeJar(r, classpath) != null) { + out.add(r); + } + } + return out; + } + + /// `layer`'s runtime jar among `classpath`, or null. + public static File runtimeJar(Relocation layer, Iterable classpath) { + File found = null; + for (File f : classpath) { + if (f != null && layer.isRuntimeJar(f.getName())) { + found = f; + } + } + return found; + } + + /// The jar of shared JDK classes among `classpath`, or null. + public static File jdkJar(Iterable classpath) { + File found = null; + for (File f : classpath) { + if (f != null && f.getName().startsWith(Relocation.JDK_ARTIFACT + "-") && f.getName().endsWith(".jar")) { + found = f; + } + } + return found; + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java b/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java new file mode 100644 index 00000000000..0264f1bfbfc --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +/// One compatibility layer's naming rules: which packages an application +/// compiles against, and where they live once the application ships. +/// +/// A layer exists because its API names cannot ship as they are -- `android.*` +/// would collide with the real framework on an Android build, and `java.awt` +/// belongs to the JDK and can be defined by nobody else. The application keeps +/// its sources and compiles against the original names; [ClassRelocator] +/// rewrites the compiled classes by these rules. +public final class Relocation { + + /// The artifact carrying the JDK classes the device runtime lacks. Every + /// layer ships it, so its names are the same whichever layers are active. + public static final String JDK_ARTIFACT = "codenameone-compat-jdk"; + public static final String JDK_PACKAGE = "com/codename1/compat/jdk/"; + + /// JDK classes the Codename One runtime lacks, redirected to the + /// implementations in [#JDK_ARTIFACT]. + static final String[][] JDK_SHIMS = { + {"java/io/BufferedReader", JDK_PACKAGE + "BufferedReader"}, + {"java/io/BufferedWriter", JDK_PACKAGE + "BufferedWriter"}, + {"java/io/BufferedInputStream", JDK_PACKAGE + "BufferedInputStream"}, + {"java/io/BufferedOutputStream", JDK_PACKAGE + "BufferedOutputStream"}, + {"java/io/File", JDK_PACKAGE + "File"}, + {"java/io/FileFilter", JDK_PACKAGE + "FileFilter"}, + {"java/io/FilenameFilter", JDK_PACKAGE + "FilenameFilter"}, + {"java/io/FileInputStream", JDK_PACKAGE + "FileInputStream"}, + {"java/io/FileOutputStream", JDK_PACKAGE + "FileOutputStream"}, + {"java/io/FileReader", JDK_PACKAGE + "FileReader"}, + {"java/io/FileWriter", JDK_PACKAGE + "FileWriter"}, + {"java/io/FilterInputStream", JDK_PACKAGE + "FilterInputStream"}, + {"java/io/FilterOutputStream", JDK_PACKAGE + "FilterOutputStream"}, + {"java/io/PrintWriter", JDK_PACKAGE + "PrintWriter"}, + // Every Codename One stream, reader and writer is AutoCloseable, and + // close() is the interface's only method, so code holding a Closeable + // runs unchanged against it. + {"java/io/Closeable", "java/lang/AutoCloseable"}, + }; + + private final String name; + private final String artifactId; + private final String target; + private final String[] prefixes; + private final String[][] shims; + private final String runtimePackage; + private final String runtimeTarget; + + /// `prefixes` are prepended with `target`, not replaced by it, so + /// `android/view/View` becomes `android/view/View`. `shims` are + /// exact names, tried before any prefix. `runtimePackage`, when the layer + /// has one, is its bridge package inside a jar that is authored under the + /// original names: it moves to `runtimeTarget` so the copy that ships + /// never shares a name with the jar's unrelocated original. + public Relocation(String name, String artifactId, String target, String[] prefixes, String[][] shims, + String runtimePackage, String runtimeTarget) { + this.name = name; + this.artifactId = artifactId; + this.target = target; + this.prefixes = prefixes.clone(); + this.shims = new String[shims.length][]; + for (int i = 0; i < shims.length; i++) { + this.shims[i] = shims[i].clone(); + } + this.runtimePackage = runtimePackage; + this.runtimeTarget = runtimeTarget; + } + + /// What a build message calls the layer ("Android", "Swing"). + public String name() { + return name; + } + + /// The artifact whose jar holds the layer's runtime. + public String artifactId() { + return artifactId; + } + + /// The package everything relocated by this layer ends up under. + public String target() { + return target; + } + + /// Whether `jarName` is a build of this layer's runtime artifact. + public boolean isRuntimeJar(String jarName) { + return jarName.startsWith(artifactId + "-") && jarName.endsWith(".jar"); + } + + /// The name `internalName` ships under when it is one of this layer's + /// exact shims, else null. + String shim(String internalName) { + for (String[] shim : shims) { + if (shim[0].equals(internalName)) { + return shim[1]; + } + } + return null; + } + + /// The relocated name when `internalName` falls under this layer's bridge + /// package or one of its prefixes, else null. + String relocate(String internalName) { + if (runtimePackage != null && internalName.startsWith(runtimePackage)) { + return runtimeTarget + internalName.substring(runtimePackage.length()); + } + for (String p : prefixes) { + if (internalName.startsWith(p)) { + return target + internalName; + } + } + return null; + } + + /// The name the application was compiled against, when `relocated` is one + /// this layer produced from a prefix, else null. Build messages use it: + /// a developer knows `javax.swing.JTable`, not where it ships. + String original(String relocated) { + if (relocated.startsWith(target)) { + String rest = relocated.substring(target.length()); + for (String p : prefixes) { + if (rest.startsWith(p)) { + return rest; + } + } + } + return null; + } +} diff --git a/maven/build-engine/src/test/java/com/codename1/maven/AndroidRemapperTest.java b/maven/build-engine/src/test/java/com/codename1/maven/AndroidRemapperTest.java index 555cbf85646..86bc7059e87 100644 --- a/maven/build-engine/src/test/java/com/codename1/maven/AndroidRemapperTest.java +++ b/maven/build-engine/src/test/java/com/codename1/maven/AndroidRemapperTest.java @@ -143,7 +143,7 @@ public void relocatesApplicationAndRuntimeAndWritesTheDispatcher() throws Except List app = refs(Files.readAllBytes(new File(classes, "com/x/Main.class").toPath())); assertTrue(app.toString(), app.contains("com/codename1/androidcompat/android/app/Activity")); assertTrue(app.toString(), app.contains("(Lcom/codename1/androidcompat/android/view/View;)V")); - assertTrue(app.toString(), app.contains("com/codename1/androidcompat/jdk/BufferedReader")); + assertTrue(app.toString(), app.contains("com/codename1/compat/jdk/BufferedReader")); for (String r : app) { assertFalse(r, r != null && (r.startsWith("android/") || r.contains("Landroid/"))); } @@ -488,8 +488,8 @@ public void visit(int v, int a, String name, String sig, String sup, String[] in assertEquals(1, itf.get(0).length); assertEquals("java/lang/AutoCloseable", itf.get(0)[0]); List r = refs(out); - assertTrue(r.toString(), r.contains("(Lcom/codename1/androidcompat/jdk/File;)Ljava/io/InputStream;")); - assertTrue(r.toString(), r.contains("com/codename1/androidcompat/jdk/FileInputStream")); + assertTrue(r.toString(), r.contains("(Lcom/codename1/compat/jdk/File;)Ljava/io/InputStream;")); + assertTrue(r.toString(), r.contains("com/codename1/compat/jdk/FileInputStream")); } /// A public class `name` extending `superName` with public constructors diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapAndroidAction.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapAndroidAction.java index 1d6ed0e5c4b..3934f13b92a 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapAndroidAction.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapAndroidAction.java @@ -70,6 +70,8 @@ public void execute(Task task) { } Log log = new GradleLog(task.getLogger()); AndroidRemapper r = new AndroidRemapper(classesDir, jar, onClickNames, log); + r.withSupportJars(java.util.Collections.singletonList( + com.codename1.maven.CompatLayers.jdkJar(compileClasspath.getFiles()))); if (relocateOnly) { r.relocateOnly(); } else if (handlerDirs != null) { diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapAndroidMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapAndroidMojo.java index 6ad50d6c2ce..63c4a497263 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapAndroidMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapAndroidMojo.java @@ -54,7 +54,14 @@ protected void executeImpl() throws MojoExecutionException, MojoFailureException copyKotlinIncrementalCompileOutputToOutputDir(); File onClick = AndroidResourceRunner.onClickNamesFile(new File(project.getBuild().getDirectory())); try { - new AndroidRemapper(classes, jar, onClick, MavenLog.of(getLog())).run(); + java.util.List classpath = new java.util.ArrayList(); + for (org.apache.maven.artifact.Artifact artifact : project.getArtifacts()) { + if (artifact.getFile() != null) { + classpath.add(artifact.getFile()); + } + } + new AndroidRemapper(classes, jar, onClick, MavenLog.of(getLog())) + .withSupportJars(java.util.Collections.singletonList(CompatLayers.jdkJar(classpath))).run(); } catch (com.codename1.builders.BuildException ex) { throw new MojoFailureException(ex.getMessage(), ex); } diff --git a/maven/compat-jdk/pom.xml b/maven/compat-jdk/pom.xml new file mode 100644 index 00000000000..f8da98308fb --- /dev/null +++ b/maven/compat-jdk/pom.xml @@ -0,0 +1,73 @@ + + + + + com.codenameone + codenameone + 8.0-SNAPSHOT + + 4.0.0 + + codenameone-compat-jdk + 8.0-SNAPSHOT + jar + codenameone-compat-jdk + + JDK classes that code written for another platform expects and the + Codename One device runtime does not have (java.io.File and the file + streams, the buffered readers and writers), implemented over Codename + One. The compatibility layers share them: the remap step redirects an + application's references to these classes and copies them into the + application, whichever layers it uses. + + + + UTF-8 + 1.8 + 1.8 + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.7.3.4 + + Max + Low + false + true + ${project.build.directory} + ${project.basedir}/spotbugs-exclude.xml + + + + spotbugs + verify + + spotbugs + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + + com.codenameone + codenameone-core + provided + + + diff --git a/maven/compat-jdk/spotbugs-exclude.xml b/maven/compat-jdk/spotbugs-exclude.xml new file mode 100644 index 00000000000..647e0f718ef --- /dev/null +++ b/maven/compat-jdk/spotbugs-exclude.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedInputStream.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedInputStream.java similarity index 99% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedInputStream.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedInputStream.java index c054bb0d052..08b231b839c 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedInputStream.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedInputStream.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.IOException; import java.io.InputStream; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedOutputStream.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedOutputStream.java similarity index 98% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedOutputStream.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedOutputStream.java index 02494565279..403a3ba853b 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedOutputStream.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedOutputStream.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.IOException; import java.io.OutputStream; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedReader.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedReader.java similarity index 99% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedReader.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedReader.java index dd5b62fdb4c..9c197d71df7 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedReader.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedReader.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.IOException; import java.io.Reader; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedWriter.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedWriter.java similarity index 98% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedWriter.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedWriter.java index 6402631b82f..07a43dd5d3f 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/BufferedWriter.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/BufferedWriter.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.IOException; import java.io.Writer; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/File.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/File.java similarity index 99% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/File.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/File.java index 59b41be5c60..4af413921f3 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/File.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/File.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import com.codename1.io.FileSystemStorage; import com.codename1.io.Util; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileFilter.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileFilter.java similarity index 96% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileFilter.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileFilter.java index 327b03d7df0..2901ed25138 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileFilter.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileFilter.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; /// `java.io.FileFilter` for the Codename One runtime. public interface FileFilter { diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileInputStream.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileInputStream.java similarity index 98% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileInputStream.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileInputStream.java index afc79341676..c22293d8a34 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileInputStream.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileInputStream.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import com.codename1.io.FileSystemStorage; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileOutputStream.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileOutputStream.java similarity index 98% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileOutputStream.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileOutputStream.java index 93ffcd3b620..e52f422af3e 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileOutputStream.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileOutputStream.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import com.codename1.io.FileSystemStorage; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileReader.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileReader.java similarity index 97% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileReader.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileReader.java index 7a1eb72a7ee..edcb1b729e7 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileReader.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileReader.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.FileNotFoundException; import java.io.InputStreamReader; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileWriter.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileWriter.java similarity index 97% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileWriter.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileWriter.java index 351a499d050..22d9ec76d99 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FileWriter.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FileWriter.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.IOException; import java.io.OutputStreamWriter; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilenameFilter.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FilenameFilter.java similarity index 96% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilenameFilter.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FilenameFilter.java index 2a9deb7efa7..885ea00cf05 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilenameFilter.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FilenameFilter.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; /// `java.io.FilenameFilter` for the Codename One runtime. public interface FilenameFilter { diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilterInputStream.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FilterInputStream.java similarity index 98% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilterInputStream.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FilterInputStream.java index c12c509e14a..07af44f6420 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilterInputStream.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FilterInputStream.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.IOException; import java.io.InputStream; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilterOutputStream.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FilterOutputStream.java similarity index 98% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilterOutputStream.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FilterOutputStream.java index 968a0f5ef05..fa04f7e0fae 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/FilterOutputStream.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FilterOutputStream.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.IOException; import java.io.OutputStream; diff --git a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/PrintWriter.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/PrintWriter.java similarity index 99% rename from maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/PrintWriter.java rename to maven/compat-jdk/src/main/java/com/codename1/compat/jdk/PrintWriter.java index 3d7a4ab82e7..62a5914ea6f 100644 --- a/maven/android-compat/src/main/java/com/codename1/androidcompat/jdk/PrintWriter.java +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/PrintWriter.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.jdk; +package com.codename1.compat.jdk; import java.io.IOException; import java.io.OutputStream; diff --git a/maven/compat-testing/pom.xml b/maven/compat-testing/pom.xml new file mode 100644 index 00000000000..6ee2da027cb --- /dev/null +++ b/maven/compat-testing/pom.xml @@ -0,0 +1,56 @@ + + + + + com.codenameone + codenameone + 8.0-SNAPSHOT + + 4.0.0 + + codenameone-compat-testing + 8.0-SNAPSHOT + jar + codenameone-compat-testing + + Test support shared by the compatibility layers: a headless Codename + One implementation with fixed metrics, the factory that selects it, and + a JUnit rule that runs a test on the EDT. A test-scope dependency only; + an ordinary module rather than a test-jar so a build that skips test + compilation still resolves it. + + + + UTF-8 + 1.8 + 1.8 + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + + com.codenameone + codenameone-core + provided + + + junit + junit + + compile + + + diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/testing/HeadlessImplementation.java b/maven/compat-testing/src/main/java/com/codename1/compat/testing/HeadlessImplementation.java similarity index 99% rename from maven/android-compat/src/test/java/com/codename1/androidcompat/testing/HeadlessImplementation.java rename to maven/compat-testing/src/main/java/com/codename1/compat/testing/HeadlessImplementation.java index 80b53e49d78..0ae9d654960 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/testing/HeadlessImplementation.java +++ b/maven/compat-testing/src/main/java/com/codename1/compat/testing/HeadlessImplementation.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.testing; +package com.codename1.compat.testing; import com.codename1.impl.CodenameOneImplementation; import com.codename1.l10n.L10NManager; @@ -37,8 +37,8 @@ public class HeadlessImplementation extends CodenameOneImplementation { public static final int WIDTH = 1080; public static final int HEIGHT = 1920; public static final int DPI = 320; - static final int CHAR_WIDTH = 16; - static final int FONT_HEIGHT = 32; + public static final int CHAR_WIDTH = 16; + public static final int FONT_HEIGHT = 32; private static final Object FONT = new Object(); private static final L10NManager L10N = new L10NManager("en", "US") { }; diff --git a/maven/android-compat/src/test/java/com/codename1/androidcompat/testing/MainThreadRule.java b/maven/compat-testing/src/main/java/com/codename1/compat/testing/MainThreadRule.java similarity index 93% rename from maven/android-compat/src/test/java/com/codename1/androidcompat/testing/MainThreadRule.java rename to maven/compat-testing/src/main/java/com/codename1/compat/testing/MainThreadRule.java index 7cba00f8c82..bdf39fdcda4 100644 --- a/maven/android-compat/src/test/java/com/codename1/androidcompat/testing/MainThreadRule.java +++ b/maven/compat-testing/src/main/java/com/codename1/compat/testing/MainThreadRule.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.androidcompat.testing; +package com.codename1.compat.testing; import com.codename1.ui.Display; @@ -28,7 +28,9 @@ import org.junit.runner.Description; import org.junit.runners.model.Statement; -/// Runs each test on the Android main thread, which is Codename One's EDT. +/// Runs each test on the UI thread every compatibility layer maps onto -- +/// Android's main thread, Swing's event dispatch thread, the JavaFX +/// application thread -- which is Codename One's EDT. /// /// Until a display exists every thread counts as the main one, so a test /// that never starts one passes on the JUnit thread. Tests that run diff --git a/maven/android-compat/src/test/java/com/codename1/impl/ImplementationFactory.java b/maven/compat-testing/src/main/java/com/codename1/impl/ImplementationFactory.java similarity index 96% rename from maven/android-compat/src/test/java/com/codename1/impl/ImplementationFactory.java rename to maven/compat-testing/src/main/java/com/codename1/impl/ImplementationFactory.java index 4aea501aede..7a6a9aff1bb 100644 --- a/maven/android-compat/src/test/java/com/codename1/impl/ImplementationFactory.java +++ b/maven/compat-testing/src/main/java/com/codename1/impl/ImplementationFactory.java @@ -22,7 +22,7 @@ */ package com.codename1.impl; -import com.codename1.androidcompat.testing.HeadlessImplementation; +import com.codename1.compat.testing.HeadlessImplementation; /// The port hook Codename One's Display uses to create its implementation. /// Every port ships its own; the runtime's tests run on no port, so this one diff --git a/maven/pom.xml b/maven/pom.xml index d165caacd07..fd44b53aab4 100644 --- a/maven/pom.xml +++ b/maven/pom.xml @@ -91,6 +91,8 @@ svg-transcoder + compat-testing + compat-jdk android-res-compiler android-compat lottie-transcoder diff --git a/scripts/check-cast-semantics.sh b/scripts/check-cast-semantics.sh index 028627337be..2e177c685ff 100755 --- a/scripts/check-cast-semantics.sh +++ b/scripts/check-cast-semantics.sh @@ -24,7 +24,8 @@ TRANSLATOR="$REPO_ROOT/vm/ByteCodeTranslator/target/classes" # The modules whose bytecode ParparVM translates and that we own. A translation # sees maven/core, maven/ios and vm/JavaAPI, and -- in an application with # Android sources -- maven/android-compat, whose runtime the build relocates into -# the application's own classes. Nothing else of ours. +# the application's own classes, with the JDK classes of maven/compat-jdk beside +# it. Nothing else of ours. # Deliberately NOT covered, for the one reason: the code runs on a VM whose # CHECKCAST does throw, so its catch(ClassCastException) handlers are live and # correct and demanding an instanceof there buys nothing. @@ -37,6 +38,7 @@ DEFAULT_ROOTS=( "maven/core/target/classes" "maven/ios/target/classes" "maven/android-compat/target/classes" + "maven/compat-jdk/target/classes" ) write_baseline=0 From 54ca1d4b231642e57f168975d905d6263d8fef83 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:56:42 +0300 Subject: [PATCH 002/257] Add the swing-compat and javafx-compat module skeletons Co-Authored-By: Claude Opus 5.5 --- maven/javafx-compat/pom.xml | 91 +++++++++++++++++++++ maven/javafx-compat/spotbugs-exclude.xml | 5 ++ maven/javafx-compat/src/main/java/.gitkeep | 0 maven/javafx-compat/src/test/java/.gitkeep | 0 maven/pom.xml | 2 + maven/swing-compat/pom.xml | 95 ++++++++++++++++++++++ maven/swing-compat/spotbugs-exclude.xml | 5 ++ maven/swing-compat/src/main/java/.gitkeep | 0 maven/swing-compat/src/test/java/.gitkeep | 0 9 files changed, 198 insertions(+) create mode 100644 maven/javafx-compat/pom.xml create mode 100644 maven/javafx-compat/spotbugs-exclude.xml create mode 100644 maven/javafx-compat/src/main/java/.gitkeep create mode 100644 maven/javafx-compat/src/test/java/.gitkeep create mode 100644 maven/swing-compat/pom.xml create mode 100644 maven/swing-compat/spotbugs-exclude.xml create mode 100644 maven/swing-compat/src/main/java/.gitkeep create mode 100644 maven/swing-compat/src/test/java/.gitkeep diff --git a/maven/javafx-compat/pom.xml b/maven/javafx-compat/pom.xml new file mode 100644 index 00000000000..f7e9f916fc0 --- /dev/null +++ b/maven/javafx-compat/pom.xml @@ -0,0 +1,91 @@ + + + + + com.codenameone + codenameone + 8.0-SNAPSHOT + + 4.0.0 + + codenameone-javafx-compat + 8.0-SNAPSHOT + jar + codenameone-javafx-compat + + A subset of the javafx.* API implemented on top of Codename + One components. An application compiles against this jar under + the real names, and the remap step of the build plugins + relocates both to com.codename1.fxcompat.*. + + + + UTF-8 + 1.8 + 1.8 + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.7.3.4 + + Max + Low + false + true + ${project.build.directory} + ${project.basedir}/spotbugs-exclude.xml + + + + spotbugs + verify + + spotbugs + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + + com.codenameone + codenameone-core + provided + + + + com.codenameone + codenameone-compat-jdk + ${project.version} + + + junit + junit + test + + + + com.codenameone + codenameone-compat-testing + ${project.version} + test + + + diff --git a/maven/javafx-compat/spotbugs-exclude.xml b/maven/javafx-compat/spotbugs-exclude.xml new file mode 100644 index 00000000000..f4c69d2e2a6 --- /dev/null +++ b/maven/javafx-compat/spotbugs-exclude.xml @@ -0,0 +1,5 @@ + + + + diff --git a/maven/javafx-compat/src/main/java/.gitkeep b/maven/javafx-compat/src/main/java/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/maven/javafx-compat/src/test/java/.gitkeep b/maven/javafx-compat/src/test/java/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/maven/pom.xml b/maven/pom.xml index fd44b53aab4..066438e670f 100644 --- a/maven/pom.xml +++ b/maven/pom.xml @@ -95,6 +95,8 @@ compat-jdk android-res-compiler android-compat + swing-compat + javafx-compat lottie-transcoder sqlite-jdbc diff --git a/maven/swing-compat/pom.xml b/maven/swing-compat/pom.xml new file mode 100644 index 00000000000..316ca146dff --- /dev/null +++ b/maven/swing-compat/pom.xml @@ -0,0 +1,95 @@ + + + + + com.codenameone + codenameone + 8.0-SNAPSHOT + + 4.0.0 + + codenameone-swing-compat + 8.0-SNAPSHOT + jar + codenameone-swing-compat + + The java.awt, javax.swing, java.beans and SwingX APIs + implemented on top of Codename One components, so desktop + Swing code runs on devices. The JDK owns those package names, + so an application keeps compiling against the JDK's own + classes and this module is authored directly under the names + it ships as, com.codename1.desktopcompat.*; the remap step of + the build plugins rewrites the application's references to + match. + + + + UTF-8 + 1.8 + 1.8 + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.7.3.4 + + Max + Low + false + true + ${project.build.directory} + ${project.basedir}/spotbugs-exclude.xml + + + + spotbugs + verify + + spotbugs + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + + com.codenameone + codenameone-core + provided + + + + com.codenameone + codenameone-compat-jdk + ${project.version} + + + junit + junit + test + + + + com.codenameone + codenameone-compat-testing + ${project.version} + test + + + diff --git a/maven/swing-compat/spotbugs-exclude.xml b/maven/swing-compat/spotbugs-exclude.xml new file mode 100644 index 00000000000..f4c69d2e2a6 --- /dev/null +++ b/maven/swing-compat/spotbugs-exclude.xml @@ -0,0 +1,5 @@ + + + + diff --git a/maven/swing-compat/src/main/java/.gitkeep b/maven/swing-compat/src/main/java/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/maven/swing-compat/src/test/java/.gitkeep b/maven/swing-compat/src/test/java/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d From 48ea72cbe3df25b2c02125ff000cb1b1763e31f5 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:18:24 +0300 Subject: [PATCH 003/257] Swing and JavaFX layers: one remap entry point, and unsupported API as a build error Registers CompatLayers.SWING and CompatLayers.JAVAFX. The Swing runtime is authored under the names it ships with, so relocating its jar is an identity: the compliance index now keeps one entry for such a class, the relocated reading. CompatRemapper is the remap step for any set of active layers: one composed ClassRelocator, the Android step when Android is active, every other runtime and the shared JDK classes extracted once, a no-op without a layer, and idempotent. Hooks are left for the Swing and JavaFX generators. The compliance check is the only compiler a Swing application has for the layer, so a reference into an active layer's packages is now also looked for in superclasses, interfaces, declared field and method types, throws, catch types, class constants, array types and invokedynamic, and is reported under the name the developer wrote with its source file and line. Projects with no layer are scanned exactly as before; a raw java.awt or javafx reference now says how to enable the layer. Co-Authored-By: Claude Opus 5.5 --- .../com/codename1/maven/AndroidRemapper.java | 8 + .../codename1/maven/BytecodeCompliance.java | 473 ++++++++++++- .../com/codename1/maven/CompatLayers.java | 47 +- .../com/codename1/maven/CompatRemapper.java | 230 +++++++ .../java/com/codename1/maven/Relocation.java | 12 + .../maven/CompatLayerComplianceTest.java | 647 ++++++++++++++++++ .../codename1/maven/CompatRemapperTest.java | 409 +++++++++++ 7 files changed, 1805 insertions(+), 21 deletions(-) create mode 100644 maven/build-engine/src/main/java/com/codename1/maven/CompatRemapper.java create mode 100644 maven/build-engine/src/test/java/com/codename1/maven/CompatLayerComplianceTest.java create mode 100644 maven/build-engine/src/test/java/com/codename1/maven/CompatRemapperTest.java diff --git a/maven/build-engine/src/main/java/com/codename1/maven/AndroidRemapper.java b/maven/build-engine/src/main/java/com/codename1/maven/AndroidRemapper.java index cfa37dabd3f..81d5a965ca4 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/AndroidRemapper.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/AndroidRemapper.java @@ -104,6 +104,7 @@ public final class AndroidRemapper { private final List handlerDirs = new ArrayList(); private final List supportJars = new ArrayList(); private ClassRelocator relocator = ANDROID; + private List applicationClasses = new ArrayList(); public AndroidRemapper(File classesDir, File compatJar, File onClickNames, Log log) { this.classesDir = classesDir; @@ -162,9 +163,16 @@ static byte[] remap(byte[] in) { return ANDROID.remap(in); } + /// The internal names of the application's classes as the last [#run] + /// left them, for a caller that goes on to generate for other layers. + List applicationClasses() { + return applicationClasses; + } + public void run() throws BuildException { try { List appClasses = new ArrayList(); + applicationClasses = appClasses; relocator.remapDirectory(classesDir, appClasses, log); if (!shipRuntime) { log.info("Relocated " + appClasses.size() + " application classes"); diff --git a/maven/build-engine/src/main/java/com/codename1/maven/BytecodeCompliance.java b/maven/build-engine/src/main/java/com/codename1/maven/BytecodeCompliance.java index 113c695fe10..0acdd613b1b 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/BytecodeCompliance.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/BytecodeCompliance.java @@ -31,7 +31,10 @@ import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.ConstantDynamic; import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -97,6 +100,8 @@ public class BytecodeCompliance { private Set pendingProjectClasses = Collections.emptySet(); + private List activeLayers; + /// A check of the project `host` describes. public BytecodeCompliance(ProjectHost host) { this.host = host; @@ -126,6 +131,22 @@ public BytecodeCompliance pendingProjectClasses(Set internalNames) { return this; } + /// The compatibility layers the checked classes were relocated by. Left + /// unset, [#execute] works them out from the project's dependencies + /// ([CompatLayers#active]), which is right for every build; a caller that + /// already knows them can say so. They decide two things: a reference + /// into a layer's packages is reported under the name the application was + /// compiled against, and it is looked for in more places (see + /// `ComplianceScanner`). + public BytecodeCompliance activeLayers(List layers) { + this.activeLayers = layers == null ? null : new ArrayList(layers); + return this; + } + + private List layers() { + return activeLayers == null ? Collections.emptyList() : activeLayers; + } + private boolean isPendingProjectClass(String owner) { if (pendingProjectClasses.isEmpty() || owner == null) { return false; @@ -319,6 +340,9 @@ public void execute() throws BuildExecutionException { Map allowedIndex = buildClassIndex(Arrays.asList(getJavaRuntimeJar(), getCodenameOneJar())); Map projectAndDependencyIndex = buildClassIndexWithOutput(outputDir, dependencyJars); projectAndDependencyIndex.putAll(buildClassIndex(siblingClassRoots)); + if (activeLayers == null) { + activeLayers = CompatLayers.active(dependencyJars); + } List violations = scanProjectClasses(outputDir, allowedIndex, projectAndDependencyIndex); if (!violations.isEmpty()) { @@ -1008,15 +1032,21 @@ private void indexArchiveStream(InputStream archiveStream, String sourcePrefix, continue; } ClassMetadata metadata = readClassMetadata(new ByteArrayInputStream(bytes), sourcePrefix + "!" + entryName); - if (metadata != null) { - index.put(metadata.name, metadata); - } + ClassMetadata moved = null; if (relocated) { - ClassMetadata moved = readClassMetadata(new ByteArrayInputStream(CompatLayers.EVERY.remap(bytes)), + moved = readClassMetadata(new ByteArrayInputStream(CompatLayers.EVERY.remap(bytes)), sourcePrefix + "!" + entryName); - if (moved != null) { - index.put(moved.name, moved); - } + } + // A runtime authored under the names it ships with (the + // Swing layer's) keeps its name through relocation. It is + // then one class, not two, and the relocated reading is + // the one to keep: its members name the JDK classes the + // device has, as the application's relocated calls do. + if (metadata != null && (moved == null || !metadata.name.equals(moved.name))) { + index.put(metadata.name, metadata); + } + if (moved != null) { + index.put(moved.name, moved); } } else if (isClassArchiveName(entryName)) { indexArchiveStream(new ByteArrayInputStream(bytes), sourcePrefix + "!" + entryName, index); @@ -1222,13 +1252,58 @@ private static final class ClassMetadata { Set fields = new HashSet(); } + /// Reports every reference a class makes that the device could not link. + /// + /// A project with no compatibility layer is checked at its method, field + /// and type instructions, and each failing instruction is one violation. + /// + /// A reference into an ACTIVE layer's relocated packages is held to more, + /// because for the Swing layer this check is the only compiler there is: + /// the application compiled against the real JDK, so javac accepted every + /// call the JDK has, and nothing before this point knows which of them + /// the layer provides. Such a reference is therefore also looked for in + /// the superclass and interfaces, the types of declared fields and + /// methods, declared `throws`, `catch` types, class constants, array + /// types, and the three parts of an `invokedynamic` -- which is what every + /// lambda and method reference compiles to: the functional interface in + /// its descriptor, the bootstrap method types, and the method handle's + /// target. It is reported under the name the developer wrote + /// (`javax.swing.JTable.setAutoCreateRowSorter(boolean)`), once per source + /// line, with a missing class reported as the class rather than as each + /// of its members. + /// + /// None of the additional places is examined for any other reference, so + /// the result for a project with no layer is what it always was. + /// + /// #### What it cannot see + /// + /// An application class that overrides or implements a method the JDK + /// type declares but the layer's type does not -- `paintComponent` on a + /// component whose compat class never calls it, a listener method the + /// compat interface lacks -- passes. The override is an ordinary method + /// of the application's own class; nothing in the class file says which + /// inherited method it was written against, and telling would need the + /// JDK's own signatures to compare the layer with. The method is then + /// simply never called on a device. Only an explicit `super.method()` + /// call, which is a method instruction like any other, is caught. private final class ComplianceScanner extends ClassVisitor { private final File classFile; private final File outputDir; private final Map allowedIndex; private final Map projectAndDependencyIndex; private final List violations; + private final List layers = layers(); + private final ClassRelocator layerNames = new ClassRelocator(layers); + /// Layer symbols already reported for this class: `symbol` alone, and + /// `symbol@line` for each line it was reported at. + private final Set reportedLayerSymbols = new HashSet(); + /// Layer symbols missing from a declaration, which has no line of its + /// own. Reported at the end, and only when no instruction reported + /// the same symbol with a line. + private final Map declaredLayerSymbols = new LinkedHashMap(); private String className; + private String sourceFile; + private int currentLine; private ComplianceScanner(File classFile, File outputDir, @@ -1246,12 +1321,57 @@ private ComplianceScanner(File classFile, @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { className = name; + if (layers.isEmpty()) { + return; + } + List declared = new ArrayList(); + checkLayerType("(extends)", superName, declared); + if (interfaces != null) { + for (String iface : interfaces) { + checkLayerType("(implements)", iface, declared); + } + } + } + + @Override + public void visitSource(String source, String debug) { + sourceFile = source; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if (!layers.isEmpty()) { + checkLayerDescriptor("(field " + name + ")", descriptor, new ArrayList()); + } + return null; } @Override public MethodVisitor visitMethod(int access, final String name, final String descriptor, String signature, String[] exceptions) { final String sourceMethod = name + descriptor; + currentLine = 0; + // What the method's own declaration is missing. It has no line, so + // it is given the method's first one when the code turns up. + final List declared = new ArrayList(); + if (!layers.isEmpty()) { + checkLayerDescriptor(sourceMethod, descriptor, declared); + if (exceptions != null) { + for (String thrown : exceptions) { + checkLayerType(sourceMethod, thrown, declared); + } + } + } return new MethodVisitor(Opcodes.ASM9) { + @Override + public void visitLineNumber(int line, Label start) { + if (currentLine == 0) { + for (DeclaredSymbol d : declared) { + d.line = line; + } + } + currentLine = line; + } + @Override public void visitMethodInsn(int opcode, String owner, String memberName, String memberDescriptor, boolean isInterface) { checkMethodReference(className, sourceMethod, owner, memberName, memberDescriptor); @@ -1266,43 +1386,312 @@ public void visitFieldInsn(int opcode, String owner, String memberName, String m public void visitTypeInsn(int opcode, String type) { checkTypeReference(className, sourceMethod, type); } + + @Override + public void visitMultiANewArrayInsn(String arrayDescriptor, int dimensions) { + if (!layers.isEmpty()) { + checkLayerType(sourceMethod, arrayDescriptor, null); + } + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + // Visited before the code it covers, so before any line: + // it is a declaration of the method, like its throws. + if (!layers.isEmpty() && type != null) { + checkLayerType(sourceMethod, type, declared); + } + } + + @Override + public void visitLdcInsn(Object value) { + if (!layers.isEmpty()) { + checkLayerConstant(sourceMethod, value); + } + } + + @Override + public void visitInvokeDynamicInsn(String indyName, String indyDescriptor, Handle bootstrap, + Object... bootstrapArguments) { + if (layers.isEmpty()) { + return; + } + int before = violations.size(); + checkLayerDescriptor(sourceMethod, indyDescriptor, null); + for (Object argument : bootstrapArguments) { + checkLayerConstant(sourceMethod, argument); + } + if (violations.size() == before) { + // Only when every class it names exists: a missing + // parameter type already explains a missing method. + checkFunctionalInterfaceMethod(sourceMethod, indyName, indyDescriptor, bootstrap, + bootstrapArguments); + } + } }; } + @Override + public void visitEnd() { + for (Map.Entry e : declaredLayerSymbols.entrySet()) { + if (reportedLayerSymbols.add(e.getKey())) { + DeclaredSymbol d = e.getValue(); + violations.add(new Violation(className, d.sourceMethod, d.message, null, relativePath(), + sourceFile, d.line)); + } + } + } + private void checkMethodReference(String sourceClass, String sourceMethod, String owner, String memberName, String memberDescriptor) { if (shouldAllowMethod(owner, memberName, memberDescriptor)) { return; } - addViolation(sourceClass, sourceMethod, owner + "#" + memberName + memberDescriptor); + if (reportLayerMember(sourceMethod, owner, memberName, memberDescriptor, true)) { + return; + } + addViolation(sourceClass, sourceMethod, owner, owner + "#" + memberName + memberDescriptor); } private void checkFieldReference(String sourceClass, String sourceMethod, String owner, String memberName, String memberDescriptor) { if (shouldAllowField(owner, memberName, memberDescriptor)) { return; } - addViolation(sourceClass, sourceMethod, owner + "#" + memberName + ":" + memberDescriptor); + if (reportLayerMember(sourceMethod, owner, memberName, memberDescriptor, false)) { + return; + } + addViolation(sourceClass, sourceMethod, owner, owner + "#" + memberName + ":" + memberDescriptor); } private void checkTypeReference(String sourceClass, String sourceMethod, String owner) { if (isArrayDescriptor(owner)) { + // An array of a layer's class is still a reference to the + // class; of anything else it was never examined. + if (!layers.isEmpty()) { + checkLayerType(sourceMethod, owner, null); + } return; } if (isInternalRewriteHelper(owner)) { return; } - if (projectAndDependencyIndex.containsKey(owner) || allowedIndex.containsKey(owner) - || isPendingProjectClass(owner)) { + if (isKnownClass(owner)) { + return; + } + Relocation layer = layerOf(owner); + if (layer != null) { + reportLayerSymbol(sourceMethod, layer, owner, javaName(owner), null); return; } - addViolation(sourceClass, sourceMethod, owner + " (type)"); + addViolation(sourceClass, sourceMethod, owner, owner + " (type)"); } - private void addViolation(String sourceClass, String sourceMethod, String referencedMember) { + private boolean isKnownClass(String internalName) { + return projectAndDependencyIndex.containsKey(internalName) || allowedIndex.containsKey(internalName) + || isPendingProjectClass(internalName); + } + + private String relativePath() { String relativePath = classFile.getAbsolutePath().replace(outputDir.getAbsolutePath(), ""); if (relativePath.startsWith(File.separator)) { relativePath = relativePath.substring(1); } - violations.add(new Violation(sourceClass, sourceMethod, referencedMember, replacementFor(referencedMember), relativePath)); + return relativePath; + } + + private void addViolation(String sourceClass, String sourceMethod, String owner, String referencedMember) { + violations.add(new Violation(sourceClass, sourceMethod, referencedMember, + replacementFor(referencedMember, owner, layers), relativePath(), sourceFile, currentLine)); + } + + /// The active layer whose relocated packages `internalName` is in, or + /// null: the references the wider scan applies to. + private Relocation layerOf(String internalName) { + if (internalName == null) { + return null; + } + for (Relocation layer : layers) { + if (layer.original(internalName) != null) { + return layer; + } + } + return null; + } + + /// Records that `api` is missing from `layer`. `declared` is non-null + /// for a reference made by a declaration rather than by an + /// instruction; such a reference is held in `declaredLayerSymbols`. + private void reportLayerSymbol(String sourceMethod, Relocation layer, String symbol, String api, + List declared) { + String message = api + " is not supported by the Codename One " + layer.name() + " compatibility layer"; + if (declared != null) { + if (!declaredLayerSymbols.containsKey(symbol)) { + DeclaredSymbol d = new DeclaredSymbol(sourceMethod, message); + declaredLayerSymbols.put(symbol, d); + declared.add(d); + } + return; + } + if (!reportedLayerSymbols.add(symbol + "@" + currentLine)) { + return; + } + reportedLayerSymbols.add(symbol); + violations.add(new Violation(className, sourceMethod, message, null, relativePath(), sourceFile, + currentLine)); + } + + /// Reports an unresolved member whose owner is a layer's class, and + /// answers whether it was one. A class the layer lacks altogether is + /// reported as the class: its constructor and every method called on + /// it would otherwise each say the same thing. + private boolean reportLayerMember(String sourceMethod, String owner, String memberName, String memberDescriptor, + boolean method) { + Relocation layer = layerOf(owner); + if (layer == null) { + return false; + } + if (!isKnownClass(owner)) { + reportLayerSymbol(sourceMethod, layer, owner, javaName(owner), null); + } else { + reportLayerSymbol(sourceMethod, layer, owner + "#" + memberName + (method ? "" : ":") + memberDescriptor, + javaMember(owner, memberName, memberDescriptor, method), null); + } + return true; + } + + /// `type` is an internal name or an array descriptor. + private void checkLayerType(String sourceMethod, String type, List declared) { + if (type == null) { + return; + } + String name = type; + if (isArrayDescriptor(type)) { + Type element = Type.getType(type).getElementType(); + if (element.getSort() != Type.OBJECT) { + return; + } + name = element.getInternalName(); + } + Relocation layer = layerOf(name); + if (layer != null && !isKnownClass(name)) { + reportLayerSymbol(sourceMethod, layer, name, javaName(name), declared); + } + } + + /// Every class a field or method descriptor names. + private void checkLayerDescriptor(String sourceMethod, String descriptor, List declared) { + Type type = Type.getType(descriptor); + if (type.getSort() == Type.METHOD) { + for (Type argument : type.getArgumentTypes()) { + checkLayerValueType(sourceMethod, argument, declared); + } + checkLayerValueType(sourceMethod, type.getReturnType(), declared); + } else { + checkLayerValueType(sourceMethod, type, declared); + } + } + + private void checkLayerValueType(String sourceMethod, Type type, List declared) { + Type element = type.getSort() == Type.ARRAY ? type.getElementType() : type; + if (element.getSort() == Type.OBJECT) { + checkLayerType(sourceMethod, element.getInternalName(), declared); + } + } + + /// A constant-pool value an instruction loads: a class, a method type, + /// a method handle or a dynamic constant. Anything else names no class. + private void checkLayerConstant(String sourceMethod, Object value) { + if (value instanceof Type) { + checkLayerDescriptor(sourceMethod, ((Type) value).getDescriptor(), null); + } else if (value instanceof Handle) { + checkLayerHandle(sourceMethod, (Handle) value); + } else if (value instanceof ConstantDynamic) { + ConstantDynamic constant = (ConstantDynamic) value; + checkLayerDescriptor(sourceMethod, constant.getDescriptor(), null); + for (int i = 0; i < constant.getBootstrapMethodArgumentCount(); i++) { + checkLayerConstant(sourceMethod, constant.getBootstrapMethodArgument(i)); + } + } + } + + /// A method handle is a call or field access spelled as a constant: + /// `JTable::setAutoCreateRowSorter` reaches the method through one and + /// through no method instruction. + private void checkLayerHandle(String sourceMethod, Handle handle) { + String owner = handle.getOwner(); + boolean method = handle.getTag() >= Opcodes.H_INVOKEVIRTUAL; + if (!isArrayDescriptor(owner) && layerOf(owner) != null + && !resolveMember(owner, memberKey(handle.getName(), handle.getDesc()), method)) { + reportLayerMember(sourceMethod, owner, handle.getName(), handle.getDesc(), method); + } + checkLayerDescriptor(sourceMethod, handle.getDesc(), null); + } + + /// A lambda implements one method of the interface its `invokedynamic` + /// returns. The interface existing is not enough: the layer's version + /// has to declare that method, or the lambda can never be called. + private void checkFunctionalInterfaceMethod(String sourceMethod, String methodName, String indyDescriptor, + Handle bootstrap, Object[] bootstrapArguments) { + if (!"java/lang/invoke/LambdaMetafactory".equals(bootstrap.getOwner()) || bootstrapArguments.length == 0 + || !(bootstrapArguments[0] instanceof Type)) { + return; + } + Type functional = Type.getReturnType(indyDescriptor); + Type implemented = (Type) bootstrapArguments[0]; + if (functional.getSort() != Type.OBJECT || implemented.getSort() != Type.METHOD) { + return; + } + String owner = functional.getInternalName(); + if (layerOf(owner) != null && isKnownClass(owner) + && !resolveMember(owner, memberKey(methodName, implemented.getDescriptor()), true)) { + reportLayerMember(sourceMethod, owner, methodName, implemented.getDescriptor(), true); + } + } + + /// `javax.swing.JTable` for the internal name the class ships under. + private String javaName(String internalName) { + String original = layerNames.original(internalName); + if (original.startsWith(Relocation.JDK_PACKAGE)) { + for (String[] shim : Relocation.JDK_SHIMS) { + if (shim[1].equals(original)) { + original = shim[0]; + break; + } + } + } + return original.replace('/', '.').replace('$', '.'); + } + + private String javaTypeName(Type type) { + if (type.getSort() == Type.ARRAY) { + StringBuilder sb = new StringBuilder(javaTypeName(type.getElementType())); + for (int i = 0; i < type.getDimensions(); i++) { + sb.append("[]"); + } + return sb.toString(); + } + return type.getSort() == Type.OBJECT ? javaName(type.getInternalName()) : type.getClassName(); + } + + /// The member as its documentation spells it: + /// `javax.swing.JTable.setAutoCreateRowSorter(boolean)`, + /// `new javax.swing.JTable(int, int)`, `javax.swing.JTable.AUTO_RESIZE_OFF`. + private String javaMember(String owner, String memberName, String memberDescriptor, boolean method) { + String ownerName = javaName(owner); + if (!method) { + return ownerName + "." + memberName; + } + StringBuilder sb = new StringBuilder(); + if ("".equals(memberName)) { + sb.append("new ").append(ownerName); + } else { + sb.append(ownerName).append('.').append(memberName); + } + sb.append('('); + Type[] arguments = Type.getArgumentTypes(memberDescriptor); + for (int i = 0; i < arguments.length; i++) { + sb.append(i == 0 ? "" : ", ").append(javaTypeName(arguments[i])); + } + return sb.append(')').toString(); } private boolean shouldAllowMethod(String owner, String name, String descriptor) { @@ -1372,40 +1761,80 @@ private static boolean isInternalRewriteHelper(String owner) { return JDK_API_REWRITE_HELPER_INTERNAL_NAME.equals(owner); } - private static String replacementFor(String referencedMember) { + /// The advice printed beside a violation, or null. `owner` is the class + /// the reference names. A class of a compatibility layer's API that + /// arrives here unrelocated belongs to a project that has not switched + /// the layer on; the wording for that is [CompatLayers#enableHint]. + private static String replacementFor(String referencedMember, String owner, List active) { String direct = SUGGESTED_REPLACEMENTS.get(referencedMember); if (direct != null) { return direct; } - int hashPos = referencedMember.indexOf('#'); - if (hashPos > 0) { - String ownerOnly = referencedMember.substring(0, hashPos); - if (ownerOnly.startsWith("java/awt/") || ownerOnly.startsWith("javax/swing/")) { - return "Codename One does not support AWT/Swing APIs. Use com.codename1.ui components for UI logic."; + if (owner != null) { + Relocation layer = CompatLayers.owning(owner); + if (layer != null && !active.contains(layer)) { + return CompatLayers.enableHint(layer); } } return null; } + /// A reference missing from a declaration, held back until the class has + /// been read; see `ComplianceScanner`. + private static final class DeclaredSymbol { + private final String sourceMethod; + private final String message; + private int line; + + private DeclaredSymbol(String sourceMethod, String message) { + this.sourceMethod = sourceMethod; + this.message = message; + } + } + private static final class Violation { private final String sourceClass; private final String sourceMethod; private final String referencedMember; private final String suggestion; private final String sourcePath; + /// The source file the class was compiled from and the line of the + /// reference, when the class carries debug information: null and 0 + /// otherwise, each on its own. + private final String sourceFile; + private final int line; private Violation(String sourceClass, String sourceMethod, String referencedMember, String suggestion, String sourcePath) { + this(sourceClass, sourceMethod, referencedMember, suggestion, sourcePath, null, 0); + } + + private Violation(String sourceClass, String sourceMethod, String referencedMember, String suggestion, String sourcePath, + String sourceFile, int line) { this.sourceClass = sourceClass; this.sourceMethod = sourceMethod; this.referencedMember = referencedMember; this.suggestion = suggestion; this.sourcePath = sourcePath; + this.sourceFile = sourceFile; + this.line = line; + } + + /// `Foo.java:123`, `Foo.java` when only the file is known, else null. + private String location() { + if (sourceFile == null || sourceFile.isEmpty()) { + return null; + } + return line > 0 ? sourceFile + ":" + line : sourceFile; } private String render() { StringBuilder sb = new StringBuilder(); sb.append("Source class: ").append(sourceClass).append("\n"); sb.append("Source method: ").append(sourceMethod).append("\n"); + String location = location(); + if (location != null) { + sb.append("Source location: ").append(location).append("\n"); + } sb.append("Source bytecode file: ").append(sourcePath).append("\n"); sb.append("Forbidden reference: ").append(referencedMember); if (suggestion != null && !suggestion.isEmpty()) { @@ -1419,6 +1848,10 @@ private String renderInline() { sb.append(sourceClass).append("#").append(sourceMethod) .append(" -> ").append(referencedMember) .append(" (").append(sourcePath).append(")"); + String location = location(); + if (location != null) { + sb.append(" at ").append(location); + } if (suggestion != null && !suggestion.isEmpty()) { sb.append(" Suggestion: ").append(suggestion); } diff --git a/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java b/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java index b91c12d43e3..611574d201f 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java @@ -36,9 +36,26 @@ /// exists, so nothing else has to be configured. public final class CompatLayers { + /// The Swing layer. `java.awt` and `javax.swing` belong to the JDK, so + /// unlike the other layers its runtime cannot be authored under the names + /// an application compiles against: the application compiles against the + /// JDK's own classes, and the runtime jar is written directly under the + /// names it ships with. It therefore has no bridge package to move, and + /// relocating its jar changes no class name. + public static final Relocation SWING = new Relocation("Swing", "codenameone-swing-compat", + "com/codename1/desktopcompat/", + new String[] {"java/awt/", "javax/swing/", "java/beans/", "javax/accessibility/", "org/jdesktop/"}, + Relocation.JDK_SHIMS, null, null); + + /// The JavaFX layer, authored under `javafx.*` and relocated like the + /// Android one. + public static final Relocation JAVAFX = new Relocation("JavaFX", "codenameone-javafx-compat", + "com/codename1/fxcompat/", new String[] {"javafx/"}, Relocation.JDK_SHIMS, + "com/codename1/fxcompat/runtime/", "com/codename1/fxcompat/rt/"); + /// Every layer, in the order their rules are tried. public static final List ALL = Collections.unmodifiableList( - Arrays.asList(AndroidRemapper.RELOCATION)); + Arrays.asList(AndroidRemapper.RELOCATION, SWING, JAVAFX)); /// A relocator applying every layer's rules. Their packages are disjoint, /// so it is correct for any class; what it cannot say is which layers an @@ -70,6 +87,34 @@ public static File runtimeJar(Relocation layer, Iterable classpath) { return found; } + /// The layer whose API `internalName` belongs to, as an application is + /// compiled against it (`javax/swing/JTable`), or null. + public static Relocation owning(String internalName) { + for (Relocation r : ALL) { + if (r.owns(internalName)) { + return r; + } + } + return null; + } + + /// What a build tells a developer whose application uses `layer`'s API + /// without having switched the layer on, or null when the build has + /// nothing to suggest. The only place this wording lives. + public static String enableHint(Relocation layer) { + if (layer == SWING) { + return "Codename One runs AWT/Swing code through its Swing compatibility layer, which this project " + + "has not enabled. Move the Swing sources to src/main/desktop to enable it, or use " + + "com.codename1.ui components for UI logic."; + } + if (layer == JAVAFX) { + return "Codename One runs JavaFX code through its JavaFX compatibility layer, which this project " + + "has not enabled. Move the JavaFX sources to src/main/desktop to enable it, or use " + + "com.codename1.ui components for UI logic."; + } + return null; + } + /// The jar of shared JDK classes among `classpath`, or null. public static File jdkJar(Iterable classpath) { File found = null; diff --git a/maven/build-engine/src/main/java/com/codename1/maven/CompatRemapper.java b/maven/build-engine/src/main/java/com/codename1/maven/CompatRemapper.java new file mode 100644 index 00000000000..dadb5921abb --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/maven/CompatRemapper.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.build.Log; +import com.codename1.builders.BuildException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/// Relocates an application's compiled classes onto whichever compatibility +/// layers it has switched on, and ships their runtimes with it. +/// +/// The one entry point a build plugin needs: it works out the active layers +/// from the compile classpath ([CompatLayers#active]), relocates by all of +/// their rules in a single pass ([ClassRelocator]) and extracts each runtime +/// jar, and the shared JDK classes, exactly once. An application with no +/// layer is left untouched, so the step can run unconditionally. +/// +/// Only the layers that are active are applied, never [CompatLayers#EVERY]: +/// an application without the Swing layer keeps its `java/awt/` references as +/// written, and the compliance check then reports them with the instruction +/// to enable the layer. +/// +/// Running it again over the same directory changes nothing. The relocation +/// rules only match names an application is compiled against, extracted +/// runtimes are skipped by the directory walk, and every extracted file is +/// compared before it is written. +public final class CompatRemapper { + + private final File classesDir; + private final List classpath; + private final File onClickNames; + private final Log log; + private final List active; + private final ClassRelocator relocator; + private final List handlerDirs = new ArrayList(); + private boolean shipRuntime = true; + + /// `classesDir` is the application's output directory, rewritten in + /// place. `classpath` is its compile classpath, which decides the active + /// layers and supplies their jars. `onClickNames` is the Android resource + /// compiler's list of `android:onClick` names, or null when the + /// application has no Android sources. + public CompatRemapper(File classesDir, Iterable classpath, File onClickNames, Log log) { + this.classesDir = classesDir; + this.classpath = new ArrayList(); + if (classpath != null) { + for (File f : classpath) { + if (f != null) { + this.classpath.add(f); + } + } + } + this.onClickNames = onClickNames; + this.log = log; + this.active = Collections.unmodifiableList(CompatLayers.active(this.classpath)); + this.relocator = new ClassRelocator(active); + } + + /// Only relocates the classes, without copying in any runtime or + /// generating anything: for a second output directory of the same + /// application (Gradle's Kotlin classes), which the main pass covers. + public CompatRemapper relocateOnly() { + shipRuntime = false; + return this; + } + + /// Further class directories of the same application, whose classes the + /// generators consider beside those of the classes directory. + public CompatRemapper withHandlerDirectories(List dirs) { + if (dirs != null) { + for (File d : dirs) { + if (d != null && d.isDirectory() && !d.equals(classesDir)) { + handlerDirs.add(d); + } + } + } + return this; + } + + /// The layers this application has switched on, in the order their rules + /// are tried; empty when it has none. + public List activeLayers() { + return active; + } + + /// Whether `layer` is among the active ones. + public boolean isActive(Relocation layer) { + return active.contains(layer); + } + + /// The relocator composed of every active layer's rules. + public ClassRelocator relocator() { + return relocator; + } + + /// Relocates and ships. Answers false, having done nothing, when the + /// application has no compatibility layer. + public boolean run() throws BuildException { + if (active.isEmpty()) { + log.debug("No compatibility layer on the classpath; nothing to relocate"); + return false; + } + try { + File jdkJar = CompatLayers.jdkJar(classpath); + List appClasses; + int runtime = 0; + boolean android = isActive(AndroidRemapper.RELOCATION); + if (android) { + // The Android step relocates the directory itself, with the + // composed rules, and extracts its own runtime and the JDK + // classes before generating its dispatchers. + AndroidRemapper remapper = new AndroidRemapper(classesDir, + CompatLayers.runtimeJar(AndroidRemapper.RELOCATION, classpath), onClickNames, log) + .withRelocator(relocator) + .withHandlerDirectories(handlerDirs) + .withSupportJars(Collections.singletonList(jdkJar)); + if (!shipRuntime) { + remapper.relocateOnly(); + } + remapper.run(); + appClasses = remapper.applicationClasses(); + } else { + appClasses = new ArrayList(); + relocator.remapDirectory(classesDir, appClasses, log); + if (shipRuntime && jdkJar != null) { + runtime += relocator.extractRuntime(jdkJar, classesDir); + } + } + if (!shipRuntime) { + if (!android) { + log.info("Relocated " + appClasses.size() + " application classes"); + } + return true; + } + StringBuilder names = new StringBuilder(); + for (Relocation layer : active) { + if (layer == AndroidRemapper.RELOCATION) { + continue; + } + runtime += relocator.extractRuntime(CompatLayers.runtimeJar(layer, classpath), classesDir); + names.append(names.length() == 0 ? "" : ", ").append(layer.name()); + } + List unmodifiable = Collections.unmodifiableList(appClasses); + if (isActive(CompatLayers.SWING)) { + generateSwingEntryPoint(unmodifiable); + generateSwingResources(unmodifiable); + } + if (isActive(CompatLayers.JAVAFX)) { + generateJavaFxEntryPoint(unmodifiable); + generateFxmlDispatch(unmodifiable); + generateJavaFxResources(unmodifiable); + } + if (names.length() > 0) { + log.info("Relocated " + appClasses.size() + " application classes and " + runtime + " " + + names + " runtime classes"); + } + return true; + } catch (IOException e) { + throw new BuildException("Compatibility layer remapping failed: " + e.getMessage(), e); + } + } + + /// Hook, not implemented yet: generates the class that starts a Swing + /// application -- the Codename One lifecycle class that calls the + /// application's `main`, since nothing on a device runs a `main` method. + /// `appClasses` are the application's relocated internal names. Called + /// after every runtime is in place, on a full (not relocate-only) run with + /// the Swing layer active. Whatever it writes must be written with + /// [ClassRelocator#writeIfDifferent], to keep a second run a no-op. + void generateSwingEntryPoint(List appClasses) throws IOException { + // Deliberately empty: see the comment above. + } + + /// Hook, not implemented yet: ships what a Swing application loads by + /// name -- `Class.getResource` images, `ResourceBundle` properties -- in + /// the form the runtime looks them up. Same contract as + /// [#generateSwingEntryPoint]. + void generateSwingResources(List appClasses) throws IOException { + // Deliberately empty: see the comment above. + } + + /// Hook, not implemented yet: generates the class that starts a JavaFX + /// application, instantiating its `javafx.application.Application` + /// subclass with `new` rather than by reflection. Same contract as + /// [#generateSwingEntryPoint], for the JavaFX layer. + void generateJavaFxEntryPoint(List appClasses) throws IOException { + // Deliberately empty: see the comment above. + } + + /// Hook, not implemented yet: generates what FXML resolves by name at + /// run time on a desktop -- controller classes, `fx:id` fields and + /// `onAction="#handler"` methods -- as direct calls, the way + /// [AndroidRemapper] generates the `android:onClick` dispatcher. Same + /// contract as [#generateSwingEntryPoint], for the JavaFX layer. + void generateFxmlDispatch(List appClasses) throws IOException { + // Deliberately empty: see the comment above. + } + + /// Hook, not implemented yet: ships the FXML documents, stylesheets and + /// images a JavaFX application loads by name. Same contract as + /// [#generateSwingEntryPoint], for the JavaFX layer. + void generateJavaFxResources(List appClasses) throws IOException { + // Deliberately empty: see the comment above. + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java b/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java index 0264f1bfbfc..31b306ae4a6 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java @@ -133,6 +133,18 @@ String relocate(String internalName) { return null; } + /// Whether `internalName`, as an application is compiled against it, is + /// part of the API this layer provides. Unlike [#relocate] this leaves the + /// bridge package out: that one is the layer's own, never an API name. + boolean owns(String internalName) { + for (String p : prefixes) { + if (internalName.startsWith(p)) { + return true; + } + } + return false; + } + /// The name the application was compiled against, when `relocated` is one /// this layer produced from a prefix, else null. Build messages use it: /// a developer knows `javax.swing.JTable`, not where it ships. diff --git a/maven/build-engine/src/test/java/com/codename1/maven/CompatLayerComplianceTest.java b/maven/build-engine/src/test/java/com/codename1/maven/CompatLayerComplianceTest.java new file mode 100644 index 00000000000..9d1f61e35a8 --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/maven/CompatLayerComplianceTest.java @@ -0,0 +1,647 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.build.BuildArtifact; +import com.codename1.build.BuildFailureException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/// The compliance check as the compiler of a compatibility layer: an API the +/// layer lacks is reported under the name the developer wrote and with the +/// line it is on, wherever in the class file the reference sits -- and a +/// project with no layer is checked exactly as before. +public class CompatLayerComplianceTest { + + private static final String SWING = CompatRemapperTest.SWING; + private static final String NOT_SUPPORTED = " is not supported by the Codename One Swing compatibility layer"; + private static final Handle METAFACTORY = new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", + "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;" + + "Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)" + + "Ljava/lang/invoke/CallSite;", false); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + /// The code of one method, written against the names an application is + /// compiled against. + private interface Body { + void emit(MethodVisitor mv); + } + + private static void line(MethodVisitor mv, int line) { + Label label = new Label(); + mv.visitLabel(label); + mv.visitLineNumber(line, label); + } + + private static void pushDefault(MethodVisitor mv, Type type) { + switch (type.getSort()) { + case Type.OBJECT: + case Type.ARRAY: + mv.visitInsn(Opcodes.ACONST_NULL); + break; + case Type.LONG: + mv.visitInsn(Opcodes.LCONST_0); + break; + case Type.FLOAT: + mv.visitInsn(Opcodes.FCONST_0); + break; + case Type.DOUBLE: + mv.visitInsn(Opcodes.DCONST_0); + break; + default: + mv.visitInsn(Opcodes.ICONST_0); + } + } + + /// A call with its operands pushed and its result dropped, so the method + /// stays well formed: the check analyses every method's stack. A + /// constructor call expects the `NEW` before it. + private static void call(MethodVisitor mv, int opcode, String owner, String name, String descriptor) { + if (opcode != Opcodes.INVOKESTATIC && !"".equals(name)) { + mv.visitInsn(Opcodes.ACONST_NULL); + } + for (Type argument : Type.getArgumentTypes(descriptor)) { + pushDefault(mv, argument); + } + mv.visitMethodInsn(opcode, owner, name, descriptor, false); + if (Type.getReturnType(descriptor).getSort() != Type.VOID) { + mv.visitInsn(Opcodes.POP); + } + } + + private static void getStatic(MethodVisitor mv, String owner, String name, String descriptor) { + mv.visitFieldInsn(Opcodes.GETSTATIC, owner, name, descriptor); + mv.visitInsn(Opcodes.POP); + } + + private static void ldc(MethodVisitor mv, Object constant) { + mv.visitLdcInsn(constant); + mv.visitInsn(Opcodes.POP); + } + + private static void typeInsn(MethodVisitor mv, int opcode, String type) { + mv.visitInsn(opcode == Opcodes.ANEWARRAY ? Opcodes.ICONST_0 : Opcodes.ACONST_NULL); + mv.visitTypeInsn(opcode, type); + mv.visitInsn(Opcodes.POP); + } + + private static void multiArray(MethodVisitor mv, String descriptor) { + mv.visitInsn(Opcodes.ICONST_0); + mv.visitInsn(Opcodes.ICONST_0); + mv.visitMultiANewArrayInsn(descriptor, 2); + mv.visitInsn(Opcodes.POP); + } + + private static void lambda(MethodVisitor mv, String name, String descriptor, Object... bootstrapArguments) { + for (Type argument : Type.getArgumentTypes(descriptor)) { + pushDefault(mv, argument); + } + mv.visitInvokeDynamicInsn(name, descriptor, METAFACTORY, bootstrapArguments); + mv.visitInsn(Opcodes.POP); + } + + /// A `try` around one instruction, catching `type` (null: a `finally`). + private static void tryCatch(MethodVisitor mv, String type) { + Label start = new Label(); + Label end = new Label(); + Label handler = new Label(); + Label after = new Label(); + mv.visitTryCatchBlock(start, end, handler, type); + mv.visitLabel(start); + mv.visitInsn(Opcodes.NOP); + mv.visitLabel(end); + mv.visitJumpInsn(Opcodes.GOTO, after); + mv.visitLabel(handler); + mv.visitInsn(Opcodes.POP); + mv.visitLabel(after); + } + + private static ClassWriter appClass(String superName, String... interfaces) { + ClassWriter cw = new ClassWriter(0); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/x/Main", null, superName, interfaces); + cw.visitSource("Main.java", null); + return cw; + } + + private static void method(ClassWriter cw, String name, String descriptor, String[] thrown, Body body) { + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, name, descriptor, null, thrown); + mv.visitCode(); + body.emit(mv); + if (Type.getReturnType(descriptor).getSort() == Type.VOID) { + mv.visitInsn(Opcodes.RETURN); + } else { + mv.visitInsn(Opcodes.ACONST_NULL); + mv.visitInsn(Opcodes.ARETURN); + } + mv.visitMaxs(16, 16); + mv.visitEnd(); + } + + private static byte[] appWith(Body body) { + ClassWriter cw = appClass("java/lang/Object"); + method(cw, "run", "()V", null, body); + cw.visitEnd(); + return cw.toByteArray(); + } + + private static byte[] apiClass(String name, boolean iface, String superName, String[] fields, String... methods) { + ClassWriter cw = new ClassWriter(0); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | (iface ? Opcodes.ACC_INTERFACE | Opcodes.ACC_ABSTRACT : 0), name, + null, superName, null); + for (int i = 0; fields != null && i < fields.length; i += 2) { + cw.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, fields[i], fields[i + 1], null, null).visitEnd(); + } + for (int i = 0; i < methods.length; i += 2) { + cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, methods[i], methods[i + 1], null, null) + .visitEnd(); + } + cw.visitEnd(); + return cw.toByteArray(); + } + + /// A tiny Swing runtime, authored the way the real one is: under the names + /// it ships with, and against the JDK classes its sources name. + private File swingJar() throws Exception { + String table = SWING + "javax/swing/JTable"; + String listener = SWING + "java/awt/event/ActionListener"; + String event = SWING + "java/awt/event/ActionEvent"; + String component = SWING + "javax/swing/JComponent"; + return CompatRemapperTest.jar(new File(tmp.getRoot(), "codenameone-swing-compat-1.jar"), + table + ".class", apiClass(table, false, component, new String[] {"AUTO_RESIZE_OFF", "I"}, + "", "()V", "setRowHeight", "(I)V", "setFile", "(Ljava/io/File;)V"), + component + ".class", apiClass(component, false, "java/lang/Object", null, "", "()V", + "repaint", "()V"), + listener + ".class", apiClass(listener, true, "java/lang/Object", null, + "actionPerformed", "(L" + event + ";)V"), + event + ".class", apiClass(event, false, "java/lang/Object", null)); + } + + private File allowedDir() throws Exception { + File dir = new File(tmp.getRoot(), "allowed"); + if (!dir.isDirectory()) { + assertTrue(new File(dir, "java/lang").mkdirs()); + Files.write(new File(dir, "java/lang/Object.class").toPath(), + apiClass("java/lang/Object", false, null, null, "", "()V")); + Files.write(new File(dir, "java/lang/Runnable.class").toPath(), + apiClass("java/lang/Runnable", true, "java/lang/Object", null, "run", "()V")); + } + return dir; + } + + /// Scans `app` as the Swing layer's build does: relocated, against the + /// runtime jar, with the layer active. + private List scanWithSwing(byte[] app) throws Exception { + return scan(new ClassRelocator(CompatLayers.SWING).remap(app), Collections.singletonList(CompatLayers.SWING), + Collections.singletonList(swingJar())); + } + + private List scan(byte[] app, List layers, List dependencies) throws Exception { + File classes = tmp.newFolder(); + assertTrue(new File(classes, "com/x").mkdirs()); + Files.write(new File(classes, "com/x/Main.class").toPath(), app); + BytecodeCompliance check = new BytecodeCompliance(TestProjectHost.empty()).activeLayers(layers); + Map allowed = buildClassIndex(check, Collections.singletonList(allowedDir())); + Map project = buildClassIndex(check, dependencies); + Method scan = BytecodeCompliance.class.getDeclaredMethod("scanProjectClasses", File.class, Map.class, + Map.class); + scan.setAccessible(true); + return (List) scan.invoke(check, classes, allowed, project); + } + + @SuppressWarnings("unchecked") + private static Map buildClassIndex(BytecodeCompliance check, List roots) throws Exception { + Method method = BytecodeCompliance.class.getDeclaredMethod("buildClassIndex", List.class); + method.setAccessible(true); + return (Map) method.invoke(check, roots); + } + + private static Object field(Object target, String name) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(target); + } + + private static String render(Object violation, String method) throws Exception { + Method m = violation.getClass().getDeclaredMethod(method); + m.setAccessible(true); + return (String) m.invoke(violation); + } + + /// Each violation as `reference @line`. + private static List summary(List violations) throws Exception { + List out = new ArrayList(); + for (Object v : violations) { + out.add(field(v, "referencedMember") + " @" + field(v, "line")); + } + return out; + } + + private static String unsupported(String api, int line) { + return api + NOT_SUPPORTED + " @" + line; + } + + @Test + public void aSupportedReferencePassesAtEverySite() throws Exception { + ClassWriter cw = appClass("javax/swing/JComponent", "java/awt/event/ActionListener"); + cw.visitField(Opcodes.ACC_PRIVATE, "table", "Ljavax/swing/JTable;", null, null).visitEnd(); + method(cw, "lambda$run$0", "(Ljava/awt/event/ActionEvent;)V", null, mv -> line(mv, 5)); + method(cw, "run", "([Ljavax/swing/JTable;)Ljavax/swing/JComponent;", null, mv -> { + line(mv, 10); + tryCatch(mv, "java/lang/Object"); + mv.visitTypeInsn(Opcodes.NEW, "javax/swing/JTable"); + call(mv, Opcodes.INVOKESPECIAL, "javax/swing/JTable", "", "()V"); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTable", "setRowHeight", "(I)V"); + // Inherited from the layer's own superclass. + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTable", "repaint", "()V"); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTable", "setFile", "(Ljava/io/File;)V"); + getStatic(mv, "javax/swing/JTable", "AUTO_RESIZE_OFF", "I"); + line(mv, 11); + lambda(mv, "actionPerformed", "()Ljava/awt/event/ActionListener;", + Type.getMethodType("(Ljava/awt/event/ActionEvent;)V"), + new Handle(Opcodes.H_INVOKESTATIC, "com/x/Main", "lambda$run$0", "(Ljava/awt/event/ActionEvent;)V", + false), + Type.getMethodType("(Ljava/awt/event/ActionEvent;)V")); + lambda(mv, "run", "(Ljavax/swing/JTable;)Ljava/lang/Runnable;", + Type.getMethodType("()V"), + new Handle(Opcodes.H_INVOKEVIRTUAL, "javax/swing/JTable", "repaint", "()V", false), + Type.getMethodType("()V")); + ldc(mv, Type.getObjectType("javax/swing/JTable")); + typeInsn(mv, Opcodes.CHECKCAST, "[Ljavax/swing/JTable;"); + multiArray(mv, "[[Ljavax/swing/JComponent;"); + }); + cw.visitEnd(); + + List violations = scanWithSwing(cw.toByteArray()); + assertTrue(summary(violations).toString(), violations.isEmpty()); + } + + @Test + public void aMissingMemberIsNamedAsTheDeveloperWroteItWithItsLine() throws Exception { + List violations = scanWithSwing(appWith(mv -> { + line(mv, 12); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTable", "setAutoCreateRowSorter", "(Z)V"); + line(mv, 13); + mv.visitTypeInsn(Opcodes.NEW, "javax/swing/JTable"); + call(mv, Opcodes.INVOKESPECIAL, "javax/swing/JTable", "", + "(I[Ljava/lang/String;Ljava/io/File;)V"); + line(mv, 14); + getStatic(mv, "javax/swing/JTable", "AUTO_RESIZE_ALL_COLUMNS", "I"); + line(mv, 15); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTable", "add", + "(Ljavax/swing/JComponent;)Ljavax/swing/JComponent;"); + })); + + assertEquals(Arrays.asList( + unsupported("javax.swing.JTable.setAutoCreateRowSorter(boolean)", 12), + unsupported("new javax.swing.JTable(int, java.lang.String[], java.io.File)", 13), + unsupported("javax.swing.JTable.AUTO_RESIZE_ALL_COLUMNS", 14), + unsupported("javax.swing.JTable.add(javax.swing.JComponent)", 15)), summary(violations)); + Object first = violations.get(0); + assertEquals("Main.java", field(first, "sourceFile")); + assertEquals("com/x/Main", field(first, "sourceClass")); + assertEquals("run()V", field(first, "sourceMethod")); + assertNull(field(first, "suggestion")); + String inline = render(first, "renderInline"); + assertTrue(inline, inline.contains("javax.swing.JTable.setAutoCreateRowSorter(boolean)" + NOT_SUPPORTED)); + assertTrue(inline, inline.endsWith(" at Main.java:12")); + assertTrue(render(first, "render"), render(first, "render").contains("Source location: Main.java:12\n")); + } + + /// `new JTree()` is a type instruction and a constructor call, and every + /// method called on it is another instruction; a class the layer lacks is + /// one finding per line it is used on. + @Test + public void aMissingClassIsReportedOncePerLine() throws Exception { + List violations = scanWithSwing(appWith(mv -> { + line(mv, 20); + mv.visitTypeInsn(Opcodes.NEW, "javax/swing/JTree"); + call(mv, Opcodes.INVOKESPECIAL, "javax/swing/JTree", "", "()V"); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTree", "expandRow", "(I)V"); + getStatic(mv, "javax/swing/JTree", "ROOT", "I"); + line(mv, 21); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTree", "expandRow", "(I)V"); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTable", "selectAll", "()V"); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTable", "selectAll", "()V"); + })); + + assertEquals(Arrays.asList(unsupported("javax.swing.JTree", 20), unsupported("javax.swing.JTree", 21), + unsupported("javax.swing.JTable.selectAll()", 21)), summary(violations)); + } + + @Test + public void declarationsAreScanned() throws Exception { + ClassWriter cw = appClass("javax/swing/JTree", "javax/swing/Missing1", "java/awt/event/ActionListener"); + cw.visitField(Opcodes.ACC_PRIVATE, "a", "[Ljavax/swing/Missing2;", null, null).visitEnd(); + cw.visitField(Opcodes.ACC_PRIVATE, "b", "Ljavax/swing/JTable;", null, null).visitEnd(); + method(cw, "run", "(I[[Ljavax/swing/Missing3;Ljavax/swing/JTable;)Ljavax/swing/Missing4;", + new String[] {"javax/swing/Missing5", "java/lang/Missing"}, mv -> { + line(mv, 40); + tryCatch(mv, "javax/swing/Missing6"); + tryCatch(mv, null); + line(mv, 41); + mv.visitInsn(Opcodes.NOP); + }); + cw.visitEnd(); + + assertEquals(Arrays.asList( + unsupported("javax.swing.JTree", 0), + unsupported("javax.swing.Missing1", 0), + unsupported("javax.swing.Missing2", 0), + unsupported("javax.swing.Missing3", 40), + unsupported("javax.swing.Missing4", 40), + unsupported("javax.swing.Missing5", 40), + unsupported("javax.swing.Missing6", 40)), summary(scanWithSwing(cw.toByteArray()))); + Object superclass = scanWithSwing(cw.toByteArray()).get(0); + assertEquals("(extends)", field(superclass, "sourceMethod")); + assertTrue(render(superclass, "renderInline"), render(superclass, "renderInline").endsWith(" at Main.java")); + } + + /// A class missing from a method's signature and used in its body is one + /// problem; the report is the one that has a line. + @Test + public void aDeclarationIsNotReportedBesideTheInstructionThatUsesIt() throws Exception { + ClassWriter cw = appClass("java/lang/Object"); + cw.visitField(Opcodes.ACC_PRIVATE, "tree", "Ljavax/swing/JTree;", null, null).visitEnd(); + method(cw, "run", "(Ljavax/swing/JTree;)V", null, mv -> { + line(mv, 50); + mv.visitInsn(Opcodes.NOP); + line(mv, 51); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTree", "expandRow", "(I)V"); + }); + cw.visitEnd(); + + assertEquals(Collections.singletonList(unsupported("javax.swing.JTree", 51)), + summary(scanWithSwing(cw.toByteArray()))); + } + + @Test + public void typeInstructionsAndClassConstantsAreScanned() throws Exception { + List violations = scanWithSwing(appWith(mv -> { + line(mv, 60); + ldc(mv, Type.getObjectType("javax/swing/Missing1")); + line(mv, 61); + ldc(mv, Type.getType("[Ljavax/swing/Missing2;")); + line(mv, 62); + typeInsn(mv, Opcodes.INSTANCEOF, "[Ljavax/swing/Missing3;"); + line(mv, 63); + typeInsn(mv, Opcodes.CHECKCAST, "javax/swing/Missing4"); + line(mv, 64); + typeInsn(mv, Opcodes.ANEWARRAY, "javax/swing/Missing5"); + line(mv, 65); + multiArray(mv, "[[Ljavax/swing/Missing6;"); + line(mv, 66); + ldc(mv, "javax/swing/NotAReference"); + ldc(mv, Type.getType("[I")); + multiArray(mv, "[[I"); + })); + + assertEquals(Arrays.asList( + unsupported("javax.swing.Missing1", 60), + unsupported("javax.swing.Missing2", 61), + unsupported("javax.swing.Missing3", 62), + unsupported("javax.swing.Missing4", 63), + unsupported("javax.swing.Missing5", 64), + unsupported("javax.swing.Missing6", 65)), summary(violations)); + } + + /// Every lambda and method reference is an `invokedynamic`: no method + /// instruction names the listener interface or the referenced method. + @Test + public void lambdasAndMethodReferencesAreScanned() throws Exception { + final Handle body = new Handle(Opcodes.H_INVOKESTATIC, "com/x/Main", "lambda$run$0", "()V", false); + List violations = scanWithSwing(appWith(mv -> { + // A listener interface the layer lacks. + line(mv, 70); + lambda(mv, "mouseClicked", "()Ljava/awt/event/MouseListener;", + Type.getMethodType("()V"), body, Type.getMethodType("()V")); + // An interface it has, implemented with an event class it lacks. + line(mv, 71); + lambda(mv, "actionPerformed", "()Ljava/awt/event/ActionListener;", + Type.getMethodType("(Ljava/awt/event/KeyEvent;)V"), body, Type.getMethodType("()V")); + // An interface it has, without the method the lambda implements. + line(mv, 72); + lambda(mv, "actionCancelled", "()Ljava/awt/event/ActionListener;", + Type.getMethodType("(Ljava/awt/event/ActionEvent;)V"), body, Type.getMethodType("()V")); + // table::setAutoCreateRowSorter + line(mv, 73); + lambda(mv, "run", "(Ljavax/swing/JTable;)Ljava/lang/Runnable;", + Type.getMethodType("()V"), + new Handle(Opcodes.H_INVOKEVIRTUAL, "javax/swing/JTable", "setAutoCreateRowSorter", "(Z)V", false), + Type.getMethodType("()V")); + // JTree::new + line(mv, 74); + lambda(mv, "run", "()Ljava/lang/Runnable;", Type.getMethodType("()V"), + new Handle(Opcodes.H_NEWINVOKESPECIAL, "javax/swing/JTree", "", "()V", false), + Type.getMethodType("()V")); + // A field read through a handle, and a handle loaded as a constant. + line(mv, 75); + ldc(mv, new Handle(Opcodes.H_GETSTATIC, "javax/swing/JTable", "AUTO_RESIZE_LAST_COLUMN", "I", + false)); + line(mv, 76); + ldc(mv, Type.getMethodType("(Ljavax/swing/JList;)V")); + })); + + assertEquals(Arrays.asList( + unsupported("java.awt.event.MouseListener", 70), + unsupported("java.awt.event.KeyEvent", 71), + unsupported("java.awt.event.ActionListener.actionCancelled(java.awt.event.ActionEvent)", 72), + unsupported("javax.swing.JTable.setAutoCreateRowSorter(boolean)", 73), + unsupported("javax.swing.JTree", 74), + unsupported("javax.swing.JTable.AUTO_RESIZE_LAST_COLUMN", 75), + unsupported("javax.swing.JList", 76)), summary(violations)); + } + + /// The same application with no layer switched on: only method, field + /// and type instructions are examined, each failing one is reported, in + /// the form the check has always used -- and it says how to get the layer. + @Test + public void aProjectWithNoLayerIsCheckedAsBefore() throws Exception { + ClassWriter cw = appClass("javax/swing/JTree", "javax/swing/Missing1"); + cw.visitField(Opcodes.ACC_PRIVATE, "a", "Ljavax/swing/Missing2;", null, null).visitEnd(); + method(cw, "run", "(Ljavax/swing/Missing3;)V", new String[] {"javax/swing/Missing4"}, mv -> { + line(mv, 80); + tryCatch(mv, "javax/swing/Missing5"); + mv.visitTypeInsn(Opcodes.NEW, "javax/swing/JTable"); + call(mv, Opcodes.INVOKESPECIAL, "javax/swing/JTable", "", "()V"); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTable", "setRowHeight", "(I)V"); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTable", "setRowHeight", "(I)V"); + line(mv, 81); + getStatic(mv, "java/awt/Color", "RED", "Ljava/awt/Color;"); + ldc(mv, Type.getObjectType("javax/swing/Missing6")); + typeInsn(mv, Opcodes.CHECKCAST, "[Ljavax/swing/Missing7;"); + multiArray(mv, "[[Ljavax/swing/Missing8;"); + lambda(mv, "actionPerformed", "()Ljava/awt/event/ActionListener;", + Type.getMethodType("(Ljava/awt/event/ActionEvent;)V"), + new Handle(Opcodes.H_INVOKEVIRTUAL, "javax/swing/JTable", "repaint", "()V", false), + Type.getMethodType("(Ljava/awt/event/ActionEvent;)V")); + call(mv, Opcodes.INVOKESTATIC, "javafx/application/Platform", "exit", "()V"); + call(mv, Opcodes.INVOKESTATIC, "com/vendor/Sdk", "start", "()V"); + }); + cw.visitEnd(); + + List violations = scan(cw.toByteArray(), Collections.emptyList(), + Collections.emptyList()); + assertEquals(Arrays.asList( + "javax/swing/JTable (type) @80", + "javax/swing/JTable#()V @80", + "javax/swing/JTable#setRowHeight(I)V @80", + "javax/swing/JTable#setRowHeight(I)V @80", + "java/awt/Color#RED:Ljava/awt/Color; @81", + "javafx/application/Platform#exit()V @81", + "com/vendor/Sdk#start()V @81"), summary(violations)); + for (int i = 0; i < 5; i++) { + assertEquals(CompatLayers.enableHint(CompatLayers.SWING), field(violations.get(i), "suggestion")); + } + assertTrue(CompatLayers.enableHint(CompatLayers.SWING).contains("src/main/desktop")); + assertEquals(CompatLayers.enableHint(CompatLayers.JAVAFX), field(violations.get(5), "suggestion")); + assertNull(field(violations.get(6), "suggestion")); + // A class compiled without debug information says nothing new. + Object bare = newViolation("a/B", "m()V", "c/D#e()V", null, "a/B.class"); + assertEquals("a/B#m()V -> c/D#e()V (a/B.class)", render(bare, "renderInline")); + assertEquals("Source class: a/B\nSource method: m()V\nSource bytecode file: a/B.class\n" + + "Forbidden reference: c/D#e()V", render(bare, "render")); + } + + private static Object newViolation(String sourceClass, String sourceMethod, String referencedMember, + String suggestion, String sourcePath) throws Exception { + java.lang.reflect.Constructor ctor = Class.forName("com.codename1.maven.BytecodeCompliance$Violation") + .getDeclaredConstructor(String.class, String.class, String.class, String.class, String.class); + ctor.setAccessible(true); + return ctor.newInstance(sourceClass, sourceMethod, referencedMember, suggestion, sourcePath); + } + + /// With a layer active, a reference outside its packages is still checked + /// only where it always was, and a raw `java/awt/` name -- a class the + /// relocation never saw -- gets no advice to enable what is enabled. + @Test + public void anActiveLayerWidensTheScanOnlyForItsOwnPackages() throws Exception { + ClassWriter cw = appClass("com/vendor/Base", "com/vendor/Listener"); + cw.visitField(Opcodes.ACC_PRIVATE, "a", "Lcom/vendor/Thing;", null, null).visitEnd(); + method(cw, "run", "(Lcom/vendor/Thing;)V", new String[] {"com/vendor/Failure"}, mv -> { + line(mv, 90); + ldc(mv, Type.getObjectType("com/vendor/Thing")); + typeInsn(mv, Opcodes.CHECKCAST, "[Lcom/vendor/Thing;"); + multiArray(mv, "[[Lcom/vendor/Thing;"); + lambda(mv, "run", "()Lcom/vendor/Listener;", Type.getMethodType("()V"), + new Handle(Opcodes.H_INVOKESTATIC, "com/vendor/Sdk", "start", "()V", false), + Type.getMethodType("()V")); + call(mv, Opcodes.INVOKESTATIC, "com/vendor/Sdk", "start", "()V"); + call(mv, Opcodes.INVOKESTATIC, "com/vendor/Sdk", "start", "()V"); + }); + cw.visitEnd(); + assertEquals(Arrays.asList("com/vendor/Sdk#start()V @90", "com/vendor/Sdk#start()V @90"), + summary(scanWithSwing(cw.toByteArray()))); + + List raw = scan(appWith(mv -> mv.visitTypeInsn(Opcodes.NEW, "java/awt/Color")), + Collections.singletonList(CompatLayers.SWING), Collections.singletonList(swingJar())); + assertEquals(Collections.singletonList("java/awt/Color (type) @0"), summary(raw)); + assertNull(field(raw.get(0), "suggestion")); + } + + /// The Swing jar is authored under the names it ships with, so indexing it + /// "under its relocated names too" must not produce a second entry -- and + /// the one entry has to be the relocated reading, whose members name the + /// JDK classes the device has. + @Test + public void aRuntimeAuthoredUnderItsFinalNamesIsIndexedOnce() throws Exception { + BytecodeCompliance check = new BytecodeCompliance(TestProjectHost.empty()); + Map swing = buildClassIndex(check, Collections.singletonList(swingJar())); + assertEquals(4, swing.size()); + Set methods = (Set) field(swing.get(SWING + "javax/swing/JTable"), "methods"); + assertTrue(methods.toString(), methods.contains("setFile(Lcom/codename1/compat/jdk/File;)V")); + assertFalse(methods.toString(), methods.contains("setFile(Ljava/io/File;)V")); + + // A runtime authored under the API's own names is indexed under both. + File fx = CompatRemapperTest.jar(new File(tmp.getRoot(), "codenameone-javafx-compat-1.jar"), + "javafx/stage/Stage.class", apiClass("javafx/stage/Stage", false, "java/lang/Object", null), + "com/codename1/fxcompat/runtime/FxApp.class", + apiClass("com/codename1/fxcompat/runtime/FxApp", false, "java/lang/Object", null)); + Map javafx = buildClassIndex(check, Collections.singletonList(fx)); + assertEquals(javafx.keySet().toString(), 4, javafx.size()); + assertTrue(javafx.containsKey("javafx/stage/Stage")); + assertTrue(javafx.containsKey("com/codename1/fxcompat/javafx/stage/Stage")); + assertTrue(javafx.containsKey("com/codename1/fxcompat/rt/FxApp")); + } + + /// The whole check, as a build runs it: the active layers come from the + /// project's dependencies, and the failure names the API and the line. + @Test + public void theBuildFailsNamingTheUnsupportedApi() throws Exception { + File classes = tmp.newFolder("classes"); + assertTrue(new File(classes, "com/x").mkdirs()); + byte[] app = appWith(mv -> { + line(mv, 12); + call(mv, Opcodes.INVOKEVIRTUAL, "javax/swing/JTable", "setAutoCreateRowSorter", "(Z)V"); + }); + Files.write(new File(classes, "com/x/Main.class").toPath(), + new ClassRelocator(CompatLayers.SWING).remap(app)); + File runtime = CompatRemapperTest.jar(new File(tmp.getRoot(), "java-runtime-1.jar"), + "java/lang/Object.class", apiClass("java/lang/Object", false, null, null, "", "()V")); + File core = CompatRemapperTest.jar(new File(tmp.getRoot(), "codenameone-core-1.jar"), + "com/codename1/ui/CN.class", apiClass("com/codename1/ui/CN", false, "java/lang/Object", null)); + TestProjectHost host = TestProjectHost.empty(); + host.buildDir = tmp.newFolder("build"); + host.outputDir = classes; + host.artifacts.add(new BuildArtifact("com.codenameone", "java-runtime", "1", null, "jar", "provided", runtime, + null)); + host.artifacts.add(new BuildArtifact("com.codenameone", "codenameone-core", "1", null, "jar", "provided", core, + null)); + host.artifacts.add(new BuildArtifact("com.codenameone", "codenameone-swing-compat", "1", null, "jar", + "compile", swingJar(), null)); + + try { + new BytecodeCompliance(host).execute(); + fail("An unsupported Swing method must fail the build"); + } catch (BuildFailureException expected) { + String message = expected.getMessage(); + assertTrue(message, message.contains("javax.swing.JTable.setAutoCreateRowSorter(boolean)" + NOT_SUPPORTED)); + assertTrue(message, message.contains("at Main.java:12")); + } + } +} diff --git a/maven/build-engine/src/test/java/com/codename1/maven/CompatRemapperTest.java b/maven/build-engine/src/test/java/com/codename1/maven/CompatRemapperTest.java new file mode 100644 index 00000000000..95a0df7ad95 --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/maven/CompatRemapperTest.java @@ -0,0 +1,409 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/// The remap entry point for any set of compatibility layers: which layers a +/// classpath switches on, that they are applied as one pass, and that a +/// second run finds nothing left to do. +public class CompatRemapperTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + private static final com.codename1.build.Log LOG = new com.codename1.build.Log() { + public void debug(CharSequence c) { } + public void debug(CharSequence c, Throwable e) { } + public void debug(Throwable e) { } + public void info(CharSequence c) { } + public void info(CharSequence c, Throwable e) { } + public void info(Throwable e) { } + public void warn(CharSequence c) { } + public void warn(CharSequence c, Throwable e) { } + public void warn(Throwable e) { } + public void error(CharSequence c) { } + public void error(CharSequence c, Throwable e) { } + public void error(Throwable e) { } + public boolean isDebugEnabled() { return false; } + public boolean isInfoEnabled() { return false; } + public boolean isWarnEnabled() { return false; } + public boolean isErrorEnabled() { return false; } + }; + + static final String SWING = "com/codename1/desktopcompat/"; + static final String FX = "com/codename1/fxcompat/"; + + /// A class with no members, as a runtime jar would hold it. + static byte[] emptyClass(String name, String superName) { + ClassWriter cw = new ClassWriter(0); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name, null, superName, null); + cw.visitEnd(); + return cw.toByteArray(); + } + + static File jar(File file, Object... nameThenBytes) throws IOException { + ZipOutputStream z = new ZipOutputStream(new FileOutputStream(file)); + try { + for (int i = 0; i < nameThenBytes.length; i += 2) { + z.putNextEntry(new ZipEntry((String) nameThenBytes[i])); + z.write((byte[]) nameThenBytes[i + 1]); + z.closeEntry(); + } + } finally { + z.close(); + } + return file; + } + + /// An application class that touches every layer: it extends a Swing + /// frame, holds a JavaFX stage, has an `android:onClick` handler and + /// opens a `java.io.File`. + private static byte[] appClass() { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/x/Main", null, "javax/swing/JFrame", null); + FieldVisitor fv = cw.visitField(Opcodes.ACC_PRIVATE, "stage", "Ljavafx/stage/Stage;", null, null); + fv.visitEnd(); + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "tapped", "(Landroid/view/View;)V", null, null); + mv.visitCode(); + mv.visitTypeInsn(Opcodes.NEW, "java/io/File"); + mv.visitInsn(Opcodes.POP); + mv.visitTypeInsn(Opcodes.NEW, "java/awt/Color"); + mv.visitInsn(Opcodes.POP); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + cw.visitEnd(); + return cw.toByteArray(); + } + + /// Every class name, descriptor and type instruction of `cls`. + private static List refs(byte[] cls) { + final List out = new ArrayList(); + new ClassReader(cls).accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public void visit(int v, int a, String name, String sig, String sup, String[] itf) { + out.add(name); + out.add(sup); + } + + @Override + public FieldVisitor visitField(int a, String n, String d, String s, Object value) { + out.add(d); + return null; + } + + @Override + public MethodVisitor visitMethod(int a, String n, String d, String s, String[] e) { + out.add(d); + return new MethodVisitor(Opcodes.ASM9) { + @Override + public void visitTypeInsn(int op, String type) { + out.add(type); + } + }; + } + }, 0); + return out; + } + + private File swingJar() throws IOException { + // Authored under the names it ships with; one class uses a JDK class + // the device lacks, which is redirected as it is extracted. + ClassWriter cw = new ClassWriter(0); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, SWING + "javax/swing/JFileChooser", null, "java/lang/Object", null); + cw.visitField(Opcodes.ACC_PRIVATE, "selected", "Ljava/io/File;", null, null).visitEnd(); + cw.visitEnd(); + return jar(new File(tmp.getRoot(), "codenameone-swing-compat-1.jar"), + SWING + "javax/swing/JFrame.class", emptyClass(SWING + "javax/swing/JFrame", "java/lang/Object"), + SWING + "java/awt/Color.class", emptyClass(SWING + "java/awt/Color", "java/lang/Object"), + SWING + "javax/swing/JFileChooser.class", cw.toByteArray(), + SWING + "rt/SwingApp.class", emptyClass(SWING + "rt/SwingApp", "java/lang/Object"), + "cn1_swing_defaults.bin", new byte[] {4, 5, 6}, + "META-INF/MANIFEST.MF", new byte[] {1}); + } + + private File javafxJar() throws IOException { + return jar(new File(tmp.getRoot(), "codenameone-javafx-compat-1.jar"), + "javafx/stage/Stage.class", emptyClass("javafx/stage/Stage", "java/lang/Object"), + FX + "runtime/FxApp.class", emptyClass(FX + "runtime/FxApp", "javafx/stage/Stage")); + } + + private File androidJar() throws IOException { + return jar(new File(tmp.getRoot(), "codenameone-android-compat-1.jar"), + "android/view/View.class", emptyClass("android/view/View", "java/lang/Object"), + "com/codename1/androidcompat/runtime/AndroidApp.class", + emptyClass("com/codename1/androidcompat/runtime/AndroidApp", "java/lang/Object")); + } + + private File jdkJar() throws IOException { + return jar(new File(tmp.getRoot(), "codenameone-compat-jdk-1.jar"), + "com/codename1/compat/jdk/File.class", emptyClass("com/codename1/compat/jdk/File", "java/lang/Object")); + } + + private File classesWithApp(String folder) throws IOException { + File classes = tmp.newFolder(folder); + assertTrue(new File(classes, "com/x").mkdirs()); + Files.write(new File(classes, "com/x/Main.class").toPath(), appClass()); + return classes; + } + + /// Every file under `dir`, by relative path, with its bytes. + private static Map snapshot(File dir) throws IOException { + Map out = new TreeMap(); + snapshot(dir, dir, out); + return out; + } + + private static void snapshot(File root, File dir, Map out) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + snapshot(root, f, out); + } else { + out.put(root.toURI().relativize(f.toURI()).getPath(), Files.readAllBytes(f.toPath())); + } + } + } + + private static void assertSameTree(Map expected, Map actual) { + assertEquals(expected.keySet(), actual.keySet()); + for (Map.Entry e : expected.entrySet()) { + assertArrayEquals(e.getKey(), e.getValue(), actual.get(e.getKey())); + } + } + + @Test + public void theLayersAreRegisteredWithTheirOwnPackages() { + assertEquals(Arrays.asList(AndroidRemapper.RELOCATION, CompatLayers.SWING, CompatLayers.JAVAFX), + CompatLayers.ALL); + ClassRelocator every = CompatLayers.EVERY; + assertEquals(SWING + "java/awt/Color", every.map("java/awt/Color")); + assertEquals(SWING + "javax/swing/JTable", every.map("javax/swing/JTable")); + assertEquals(SWING + "java/beans/PropertyChangeListener", every.map("java/beans/PropertyChangeListener")); + assertEquals(SWING + "javax/accessibility/Accessible", every.map("javax/accessibility/Accessible")); + assertEquals(SWING + "org/jdesktop/layout/GroupLayout", every.map("org/jdesktop/layout/GroupLayout")); + assertEquals(FX + "javafx/stage/Stage", every.map("javafx/stage/Stage")); + assertEquals(FX + "rt/FxApp", every.map(FX + "runtime/FxApp")); + assertEquals("com/codename1/compat/jdk/File", every.map("java/io/File")); + // The names the Swing runtime is authored under are final already. + assertEquals(SWING + "javax/swing/JTable", every.map(SWING + "javax/swing/JTable")); + assertEquals(SWING + "rt/SwingApp", every.map(SWING + "rt/SwingApp")); + // And back, for build messages. + assertEquals("javax/swing/JTable", every.original(SWING + "javax/swing/JTable")); + assertEquals("javafx/stage/Stage", every.original(FX + "javafx/stage/Stage")); + assertEquals(SWING + "rt/SwingApp", every.original(SWING + "rt/SwingApp")); + assertSame(CompatLayers.SWING, CompatLayers.owning("javax/swing/JTable")); + assertSame(CompatLayers.JAVAFX, CompatLayers.owning("javafx/stage/Stage")); + assertEquals(null, CompatLayers.owning("java/util/List")); + assertEquals(null, CompatLayers.owning(SWING + "javax/swing/JTable")); + } + + @Test + public void relocatesSwingReferencesOntoARuntimeAuthoredUnderItsFinalNames() throws Exception { + File classes = classesWithApp("classes"); + List classpath = Arrays.asList(swingJar(), jdkJar(), new File(tmp.getRoot(), "other-1.jar")); + + CompatRemapper remapper = new CompatRemapper(classes, classpath, null, LOG); + assertEquals(Collections.singletonList(CompatLayers.SWING), remapper.activeLayers()); + assertTrue(remapper.run()); + + List app = refs(Files.readAllBytes(new File(classes, "com/x/Main.class").toPath())); + assertTrue(app.toString(), app.contains(SWING + "javax/swing/JFrame")); + assertTrue(app.toString(), app.contains(SWING + "java/awt/Color")); + assertTrue(app.toString(), app.contains("com/codename1/compat/jdk/File")); + // Layers that are not active leave their names alone. + assertTrue(app.toString(), app.contains("Ljavafx/stage/Stage;")); + assertTrue(app.toString(), app.contains("(Landroid/view/View;)V")); + + assertTrue(new File(classes, SWING + "javax/swing/JFrame.class").isFile()); + assertTrue(new File(classes, SWING + "rt/SwingApp.class").isFile()); + assertTrue(new File(classes, "cn1_swing_defaults.bin").isFile()); + assertTrue(new File(classes, "com/codename1/compat/jdk/File.class").isFile()); + assertFalse(new File(classes, "javax").exists()); + assertFalse(new File(classes, "META-INF").exists()); + // No Android step ran. + assertFalse(new File(classes, AndroidRemapper.ON_CLICK_DISPATCH + ".class").exists()); + // The runtime's own use of a JDK class the device lacks is redirected. + List chooser = refs(Files.readAllBytes( + new File(classes, SWING + "javax/swing/JFileChooser.class").toPath())); + assertTrue(chooser.toString(), chooser.contains("Lcom/codename1/compat/jdk/File;")); + } + + @Test + public void appliesAndroidSwingAndJavaFxAsOnePass() throws Exception { + File classes = classesWithApp("classes"); + File onClick = tmp.newFile("onclick.txt"); + Files.write(onClick.toPath(), "tapped\n".getBytes("UTF-8")); + List classpath = Arrays.asList(androidJar(), swingJar(), javafxJar(), jdkJar()); + + CompatRemapper remapper = new CompatRemapper(classes, classpath, onClick, LOG); + assertEquals(CompatLayers.ALL, remapper.activeLayers()); + assertEquals(3, remapper.relocator().relocations().size()); + assertTrue(remapper.run()); + + List app = refs(Files.readAllBytes(new File(classes, "com/x/Main.class").toPath())); + assertTrue(app.toString(), app.contains(SWING + "javax/swing/JFrame")); + assertTrue(app.toString(), app.contains("L" + FX + "javafx/stage/Stage;")); + assertTrue(app.toString(), app.contains("(Lcom/codename1/androidcompat/android/view/View;)V")); + assertTrue(app.toString(), app.contains("com/codename1/compat/jdk/File")); + + assertTrue(new File(classes, "com/codename1/androidcompat/android/view/View.class").isFile()); + assertTrue(new File(classes, "com/codename1/androidcompat/rt/AndroidApp.class").isFile()); + assertTrue(new File(classes, SWING + "javax/swing/JFrame.class").isFile()); + assertTrue(new File(classes, FX + "javafx/stage/Stage.class").isFile()); + assertTrue(new File(classes, FX + "rt/FxApp.class").isFile()); + assertFalse(new File(classes, FX + "runtime").exists()); + assertFalse(new File(classes, "javafx").exists()); + assertFalse(new File(classes, "android").exists()); + assertTrue(new File(classes, "com/codename1/compat/jdk/File.class").isFile()); + // The JavaFX runtime's reference to its own API moved with it. + assertTrue(refs(Files.readAllBytes(new File(classes, FX + "rt/FxApp.class").toPath())) + .contains(FX + "javafx/stage/Stage")); + // The Android generators ran, over the composed relocation. + byte[] dispatch = Files.readAllBytes(new File(classes, AndroidRemapper.ON_CLICK_DISPATCH + ".class").toPath()); + final boolean[] callsHandler = new boolean[1]; + new ClassReader(dispatch).accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(int a, String n, String d, String s, String[] e) { + return new MethodVisitor(Opcodes.ASM9) { + @Override + public void visitMethodInsn(int op, String owner, String name, String desc, boolean itf) { + callsHandler[0] |= owner.equals("com/x/Main") && name.equals("tapped"); + } + }; + } + }, 0); + assertTrue(callsHandler[0]); + } + + @Test + public void aSecondRunChangesNothing() throws Exception { + File swingOnly = classesWithApp("swing"); + List swingClasspath = Arrays.asList(swingJar(), jdkJar()); + assertTrue(new CompatRemapper(swingOnly, swingClasspath, null, LOG).run()); + Map first = snapshot(swingOnly); + assertTrue(new CompatRemapper(swingOnly, swingClasspath, null, LOG).run()); + assertSameTree(first, snapshot(swingOnly)); + + File all = classesWithApp("all"); + File onClick = tmp.newFile("onclick.txt"); + Files.write(onClick.toPath(), "tapped\n".getBytes("UTF-8")); + List classpath = Arrays.asList(androidJar(), swingJar(), javafxJar(), jdkJar()); + assertTrue(new CompatRemapper(all, classpath, onClick, LOG).run()); + first = snapshot(all); + assertTrue(new CompatRemapper(all, classpath, onClick, LOG).run()); + assertSameTree(first, snapshot(all)); + assertTrue(new CompatRemapper(all, classpath, onClick, LOG).relocateOnly().run()); + assertSameTree(first, snapshot(all)); + } + + @Test + public void withoutALayerNothingIsTouched() throws Exception { + File classes = classesWithApp("classes"); + Map before = snapshot(classes); + + CompatRemapper remapper = new CompatRemapper(classes, + Arrays.asList(jdkJar(), new File(tmp.getRoot(), "codenameone-core-1.jar")), null, LOG); + assertTrue(remapper.activeLayers().isEmpty()); + assertFalse(remapper.run()); + assertSameTree(before, snapshot(classes)); + + assertFalse(new CompatRemapper(classes, null, null, LOG).run()); + assertSameTree(before, snapshot(classes)); + } + + /// An application that has not switched the Swing layer on keeps its + /// `java/awt/` references, whatever other layer it uses: the compliance + /// check is what tells its developer about them. + @Test + public void anInactiveLayersNamesAreLeftForTheComplianceCheck() throws Exception { + File classes = classesWithApp("classes"); + assertTrue(new CompatRemapper(classes, Arrays.asList(androidJar(), jdkJar()), null, LOG).run()); + + List app = refs(Files.readAllBytes(new File(classes, "com/x/Main.class").toPath())); + assertTrue(app.toString(), app.contains("javax/swing/JFrame")); + assertTrue(app.toString(), app.contains("java/awt/Color")); + assertTrue(app.toString(), app.contains("Ljavafx/stage/Stage;")); + assertTrue(app.toString(), app.contains("(Lcom/codename1/androidcompat/android/view/View;)V")); + } + + /// With only Android active the generic entry point is the Android step + /// and nothing else: the same files with the same bytes. + @Test + public void androidAloneIsExactlyTheAndroidStep() throws Exception { + File onClick = tmp.newFile("onclick.txt"); + Files.write(onClick.toPath(), "tapped\nmissing\n".getBytes("UTF-8")); + File android = androidJar(); + File jdk = jdkJar(); + + File direct = classesWithApp("direct"); + new AndroidRemapper(direct, android, onClick, LOG).withSupportJars(Collections.singletonList(jdk)).run(); + File generic = classesWithApp("generic"); + assertTrue(new CompatRemapper(generic, Arrays.asList(android, jdk), onClick, LOG).run()); + + assertSameTree(snapshot(direct), snapshot(generic)); + } + + @Test + public void relocateOnlyShipsNoRuntime() throws Exception { + File classes = classesWithApp("classes"); + assertTrue(new CompatRemapper(classes, Arrays.asList(swingJar(), javafxJar(), jdkJar()), null, LOG) + .relocateOnly().run()); + + List app = refs(Files.readAllBytes(new File(classes, "com/x/Main.class").toPath())); + assertTrue(app.toString(), app.contains(SWING + "javax/swing/JFrame")); + assertTrue(app.toString(), app.contains("L" + FX + "javafx/stage/Stage;")); + assertEquals(Collections.singleton("com/x/Main.class"), snapshot(classes).keySet()); + } +} From 60dcd86e2c95ed421907782610078dde66e568f1 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:39:10 +0300 Subject: [PATCH 004/257] JavaFX layer: beans, bindings, observable collections, events, geometry and util The pure-logic foundation of the javafx.* subset, written under the real names for the remap step to relocate: properties and bindings with lazy invalidation, bidirectional and content bindings, observable lists, maps and sets with the full list change protocol, FilteredList and SortedList, the event types and dispatch chain, the geometry values and Duration, Pair and the string converters. Internal classes live in com.codename1.fxcompat.runtime. Unimplemented API is absent rather than stubbed. Two JDK types the device class library lacks are named on purpose and need shims: java.util.concurrent.Callable and java.util.EventObject. Co-Authored-By: Claude Opus 5.5 --- maven/javafx-compat/spotbugs-exclude.xml | 115 ++ maven/javafx-compat/src/main/java/.gitkeep | 0 .../fxcompat/runtime/ArrayObservableList.java | 294 ++++ .../runtime/BidirectionalBinding.java | 184 ++ .../fxcompat/runtime/BindingObserver.java | 61 + .../codename1/fxcompat/runtime/Changes.java | 220 +++ .../runtime/ConstantObservableList.java | 100 ++ .../fxcompat/runtime/ContentBindings.java | 338 ++++ .../fxcompat/runtime/Diagnostics.java | 36 + .../runtime/EventDispatchChainImpl.java | 79 + .../fxcompat/runtime/ListChangeBuilder.java | 404 +++++ .../fxcompat/runtime/ListenerSet.java | 142 ++ .../runtime/ObservableMapWrapper.java | 213 +++ .../runtime/ObservableSetWrapper.java | 138 ++ .../fxcompat/runtime/PropertyText.java | 59 + .../fxcompat/runtime/UnmodifiableViews.java | 216 +++ .../fxcompat/runtime/ValueListeners.java | 121 ++ .../fxcompat/runtime/package-info.java | 26 + .../java/javafx/beans/DefaultProperty.java | 42 + .../javafx/beans/InvalidationListener.java | 31 + .../src/main/java/javafx/beans/NamedArg.java | 41 + .../main/java/javafx/beans/Observable.java | 38 + .../beans/WeakInvalidationListener.java | 56 + .../main/java/javafx/beans/WeakListener.java | 31 + .../java/javafx/beans/binding/Binding.java | 47 + .../java/javafx/beans/binding/Bindings.java | 1538 +++++++++++++++++ .../javafx/beans/binding/BooleanBinding.java | 137 ++ .../beans/binding/BooleanExpression.java | 101 ++ .../main/java/javafx/beans/binding/Const.java | 184 ++ .../javafx/beans/binding/DoubleBinding.java | 137 ++ .../beans/binding/DoubleExpression.java | 211 +++ .../javafx/beans/binding/FloatBinding.java | 137 ++ .../javafx/beans/binding/FloatExpression.java | 190 ++ .../main/java/javafx/beans/binding/Fn.java | 264 +++ .../javafx/beans/binding/IntegerBinding.java | 137 ++ .../beans/binding/IntegerExpression.java | 190 ++ .../javafx/beans/binding/ListExpression.java | 262 +++ .../javafx/beans/binding/LongBinding.java | 137 ++ .../javafx/beans/binding/LongExpression.java | 190 ++ .../javafx/beans/binding/NumberBinding.java | 27 + .../beans/binding/NumberExpression.java | 210 +++ .../beans/binding/NumberExpressionBase.java | 362 ++++ .../javafx/beans/binding/ObjectBinding.java | 137 ++ .../beans/binding/ObjectExpression.java | 91 + .../javafx/beans/binding/StringBinding.java | 137 ++ .../beans/binding/StringExpression.java | 174 ++ .../main/java/javafx/beans/binding/When.java | 243 +++ .../javafx/beans/binding/package-info.java | 26 + .../main/java/javafx/beans/package-info.java | 25 + .../beans/property/BooleanProperty.java | 113 ++ .../beans/property/BooleanPropertyBase.java | 192 ++ .../javafx/beans/property/DoubleProperty.java | 113 ++ .../beans/property/DoublePropertyBase.java | 192 ++ .../javafx/beans/property/FloatProperty.java | 113 ++ .../beans/property/FloatPropertyBase.java | 192 ++ .../beans/property/IntegerProperty.java | 113 ++ .../beans/property/IntegerPropertyBase.java | 192 ++ .../javafx/beans/property/ListProperty.java | 59 + .../beans/property/ListPropertyBase.java | 357 ++++ .../javafx/beans/property/LongProperty.java | 113 ++ .../beans/property/LongPropertyBase.java | 192 ++ .../javafx/beans/property/ObjectProperty.java | 55 + .../beans/property/ObjectPropertyBase.java | 188 ++ .../java/javafx/beans/property/Property.java | 50 + .../property/ReadOnlyBooleanProperty.java | 113 ++ .../property/ReadOnlyBooleanPropertyBase.java | 64 + .../property/ReadOnlyBooleanWrapper.java | 84 + .../property/ReadOnlyDoubleProperty.java | 113 ++ .../property/ReadOnlyDoublePropertyBase.java | 64 + .../beans/property/ReadOnlyDoubleWrapper.java | 84 + .../beans/property/ReadOnlyFloatProperty.java | 113 ++ .../property/ReadOnlyFloatPropertyBase.java | 64 + .../beans/property/ReadOnlyFloatWrapper.java | 84 + .../property/ReadOnlyIntegerProperty.java | 113 ++ .../property/ReadOnlyIntegerPropertyBase.java | 64 + .../property/ReadOnlyIntegerWrapper.java | 84 + .../beans/property/ReadOnlyListProperty.java | 104 ++ .../beans/property/ReadOnlyLongProperty.java | 113 ++ .../property/ReadOnlyLongPropertyBase.java | 64 + .../beans/property/ReadOnlyLongWrapper.java | 84 + .../property/ReadOnlyObjectProperty.java | 42 + .../property/ReadOnlyObjectPropertyBase.java | 64 + .../beans/property/ReadOnlyObjectWrapper.java | 84 + .../beans/property/ReadOnlyProperty.java | 35 + .../property/ReadOnlyStringProperty.java | 42 + .../property/ReadOnlyStringPropertyBase.java | 64 + .../beans/property/ReadOnlyStringWrapper.java | 84 + .../beans/property/SimpleBooleanProperty.java | 66 + .../beans/property/SimpleDoubleProperty.java | 66 + .../beans/property/SimpleFloatProperty.java | 66 + .../beans/property/SimpleIntegerProperty.java | 66 + .../beans/property/SimpleListProperty.java | 68 + .../beans/property/SimpleLongProperty.java | 66 + .../beans/property/SimpleObjectProperty.java | 66 + .../beans/property/SimpleStringProperty.java | 66 + .../javafx/beans/property/StringProperty.java | 67 + .../beans/property/StringPropertyBase.java | 188 ++ .../javafx/beans/property/package-info.java | 25 + .../javafx/beans/value/ChangeListener.java | 31 + .../beans/value/ObservableBooleanValue.java | 30 + .../beans/value/ObservableDoubleValue.java | 30 + .../beans/value/ObservableFloatValue.java | 30 + .../beans/value/ObservableIntegerValue.java | 30 + .../beans/value/ObservableListValue.java | 30 + .../beans/value/ObservableLongValue.java | 30 + .../beans/value/ObservableNumberValue.java | 39 + .../beans/value/ObservableObjectValue.java | 30 + .../beans/value/ObservableStringValue.java | 27 + .../javafx/beans/value/ObservableValue.java | 42 + .../beans/value/ObservableValueBase.java | 64 + .../beans/value/WeakChangeListener.java | 59 + .../beans/value/WritableBooleanValue.java | 37 + .../beans/value/WritableDoubleValue.java | 37 + .../beans/value/WritableFloatValue.java | 37 + .../beans/value/WritableIntegerValue.java | 37 + .../javafx/beans/value/WritableListValue.java | 30 + .../javafx/beans/value/WritableLongValue.java | 37 + .../beans/value/WritableNumberValue.java | 27 + .../beans/value/WritableObjectValue.java | 33 + .../beans/value/WritableStringValue.java | 27 + .../javafx/beans/value/WritableValue.java | 33 + .../java/javafx/beans/value/package-info.java | 25 + .../javafx/collections/FXCollections.java | 276 +++ .../collections/ListChangeListener.java | 138 ++ .../javafx/collections/MapChangeListener.java | 63 + .../ModifiableObservableListBase.java | 174 ++ .../javafx/collections/ObservableList.java | 95 + .../collections/ObservableListBase.java | 171 ++ .../javafx/collections/ObservableMap.java | 37 + .../javafx/collections/ObservableSet.java | 37 + .../javafx/collections/SetChangeListener.java | 59 + .../collections/WeakListChangeListener.java | 59 + .../collections/WeakMapChangeListener.java | 59 + .../collections/WeakSetChangeListener.java | 59 + .../java/javafx/collections/package-info.java | 26 + .../transformation/FilteredList.java | 277 +++ .../transformation/SortedList.java | 365 ++++ .../transformation/TransformationList.java | 105 ++ .../transformation/package-info.java | 25 + .../main/java/javafx/event/ActionEvent.java | 63 + .../src/main/java/javafx/event/Event.java | 125 ++ .../java/javafx/event/EventDispatchChain.java | 37 + .../java/javafx/event/EventDispatcher.java | 33 + .../main/java/javafx/event/EventHandler.java | 33 + .../main/java/javafx/event/EventTarget.java | 32 + .../src/main/java/javafx/event/EventType.java | 88 + .../java/javafx/event/WeakEventHandler.java | 51 + .../main/java/javafx/event/package-info.java | 26 + .../java/javafx/geometry/BoundingBox.java | 135 ++ .../src/main/java/javafx/geometry/Bounds.java | 141 ++ .../java/javafx/geometry/Dimension2D.java | 70 + .../main/java/javafx/geometry/Geometry.java | 42 + .../src/main/java/javafx/geometry/HPos.java | 33 + .../javafx/geometry/HorizontalDirection.java | 31 + .../src/main/java/javafx/geometry/Insets.java | 94 + .../java/javafx/geometry/NodeOrientation.java | 33 + .../java/javafx/geometry/Orientation.java | 31 + .../main/java/javafx/geometry/Point2D.java | 188 ++ .../src/main/java/javafx/geometry/Pos.java | 69 + .../java/javafx/geometry/Rectangle2D.java | 150 ++ .../src/main/java/javafx/geometry/Side.java | 45 + .../src/main/java/javafx/geometry/VPos.java | 35 + .../javafx/geometry/VerticalDirection.java | 31 + .../java/javafx/geometry/package-info.java | 25 + .../src/main/java/javafx/util/Callback.java | 32 + .../src/main/java/javafx/util/Duration.java | 261 +++ .../src/main/java/javafx/util/Pair.java | 76 + .../java/javafx/util/StringConverter.java | 37 + .../converter/BooleanStringConverter.java | 55 + .../converter/DefaultStringConverter.java | 44 + .../util/converter/DoubleStringConverter.java | 55 + .../converter/IntegerStringConverter.java | 55 + .../util/converter/NumberStringConverter.java | 166 ++ .../javafx/util/converter/package-info.java | 24 + .../main/java/javafx/util/package-info.java | 25 + maven/javafx-compat/src/test/java/.gitkeep | 0 .../com/codename1/fxcompat/BindingsTest.java | 693 ++++++++ .../com/codename1/fxcompat/EventTest.java | 225 +++ .../com/codename1/fxcompat/GeometryTest.java | 227 +++ .../fxcompat/ObservableListTest.java | 480 +++++ .../fxcompat/ObservableMapSetTest.java | 216 +++ .../java/com/codename1/fxcompat/Probe.java | 123 ++ .../com/codename1/fxcompat/PropertyTest.java | 614 +++++++ .../fxcompat/TransformationListTest.java | 309 ++++ .../java/com/codename1/fxcompat/UtilTest.java | 263 +++ 185 files changed, 21625 insertions(+) delete mode 100644 maven/javafx-compat/src/main/java/.gitkeep create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ArrayObservableList.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/BidirectionalBinding.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/BindingObserver.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/Changes.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ConstantObservableList.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ContentBindings.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/Diagnostics.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/EventDispatchChainImpl.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ListChangeBuilder.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ListenerSet.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ObservableMapWrapper.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ObservableSetWrapper.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/PropertyText.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/UnmodifiableViews.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ValueListeners.java create mode 100644 maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/package-info.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/DefaultProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/InvalidationListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/NamedArg.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/Observable.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/WeakInvalidationListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/WeakListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/Binding.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/Bindings.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/BooleanBinding.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/BooleanExpression.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/Const.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/DoubleBinding.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/DoubleExpression.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/FloatBinding.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/FloatExpression.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/Fn.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/IntegerBinding.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/IntegerExpression.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/ListExpression.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/LongBinding.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/LongExpression.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/NumberBinding.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/NumberExpression.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/NumberExpressionBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/ObjectBinding.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/ObjectExpression.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/StringBinding.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/StringExpression.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/When.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/binding/package-info.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/package-info.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/BooleanProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/BooleanPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/DoubleProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/DoublePropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/FloatProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/FloatPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/IntegerProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/IntegerPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ListProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ListPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/LongProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/LongPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ObjectProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ObjectPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/Property.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanWrapper.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoubleProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoublePropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoubleWrapper.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatWrapper.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerWrapper.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyListProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongWrapper.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectWrapper.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringWrapper.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/SimpleBooleanProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/SimpleDoubleProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/SimpleFloatProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/SimpleIntegerProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/SimpleListProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/SimpleLongProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/SimpleObjectProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/SimpleStringProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/StringProperty.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/StringPropertyBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/property/package-info.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ChangeListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableBooleanValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableDoubleValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableFloatValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableIntegerValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableListValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableLongValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableNumberValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableObjectValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableStringValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/ObservableValueBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WeakChangeListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WritableBooleanValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WritableDoubleValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WritableFloatValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WritableIntegerValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WritableListValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WritableLongValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WritableNumberValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WritableObjectValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WritableStringValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/WritableValue.java create mode 100644 maven/javafx-compat/src/main/java/javafx/beans/value/package-info.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/FXCollections.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/ListChangeListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/MapChangeListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/ModifiableObservableListBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/ObservableList.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/ObservableListBase.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/ObservableMap.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/ObservableSet.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/SetChangeListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/WeakListChangeListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/WeakMapChangeListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/WeakSetChangeListener.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/package-info.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/transformation/FilteredList.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/transformation/SortedList.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/transformation/TransformationList.java create mode 100644 maven/javafx-compat/src/main/java/javafx/collections/transformation/package-info.java create mode 100644 maven/javafx-compat/src/main/java/javafx/event/ActionEvent.java create mode 100644 maven/javafx-compat/src/main/java/javafx/event/Event.java create mode 100644 maven/javafx-compat/src/main/java/javafx/event/EventDispatchChain.java create mode 100644 maven/javafx-compat/src/main/java/javafx/event/EventDispatcher.java create mode 100644 maven/javafx-compat/src/main/java/javafx/event/EventHandler.java create mode 100644 maven/javafx-compat/src/main/java/javafx/event/EventTarget.java create mode 100644 maven/javafx-compat/src/main/java/javafx/event/EventType.java create mode 100644 maven/javafx-compat/src/main/java/javafx/event/WeakEventHandler.java create mode 100644 maven/javafx-compat/src/main/java/javafx/event/package-info.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/BoundingBox.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/Bounds.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/Dimension2D.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/Geometry.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/HPos.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/HorizontalDirection.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/Insets.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/NodeOrientation.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/Orientation.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/Point2D.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/Pos.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/Rectangle2D.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/Side.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/VPos.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/VerticalDirection.java create mode 100644 maven/javafx-compat/src/main/java/javafx/geometry/package-info.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/Callback.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/Duration.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/Pair.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/StringConverter.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/converter/BooleanStringConverter.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/converter/DefaultStringConverter.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/converter/DoubleStringConverter.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/converter/IntegerStringConverter.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/converter/NumberStringConverter.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/converter/package-info.java create mode 100644 maven/javafx-compat/src/main/java/javafx/util/package-info.java delete mode 100644 maven/javafx-compat/src/test/java/.gitkeep create mode 100644 maven/javafx-compat/src/test/java/com/codename1/fxcompat/BindingsTest.java create mode 100644 maven/javafx-compat/src/test/java/com/codename1/fxcompat/EventTest.java create mode 100644 maven/javafx-compat/src/test/java/com/codename1/fxcompat/GeometryTest.java create mode 100644 maven/javafx-compat/src/test/java/com/codename1/fxcompat/ObservableListTest.java create mode 100644 maven/javafx-compat/src/test/java/com/codename1/fxcompat/ObservableMapSetTest.java create mode 100644 maven/javafx-compat/src/test/java/com/codename1/fxcompat/Probe.java create mode 100644 maven/javafx-compat/src/test/java/com/codename1/fxcompat/PropertyTest.java create mode 100644 maven/javafx-compat/src/test/java/com/codename1/fxcompat/TransformationListTest.java create mode 100644 maven/javafx-compat/src/test/java/com/codename1/fxcompat/UtilTest.java diff --git a/maven/javafx-compat/spotbugs-exclude.xml b/maven/javafx-compat/spotbugs-exclude.xml index f4c69d2e2a6..a612c8bbbe9 100644 --- a/maven/javafx-compat/spotbugs-exclude.xml +++ b/maven/javafx-compat/spotbugs-exclude.xml @@ -2,4 +2,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maven/javafx-compat/src/main/java/.gitkeep b/maven/javafx-compat/src/main/java/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ArrayObservableList.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ArrayObservableList.java new file mode 100644 index 00000000000..6c75ee559f1 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ArrayObservableList.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.RandomAccess; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.collections.ModifiableObservableListBase; +import javafx.util.Callback; + +/// The observable list `FXCollections` hands out: a `java.util.List` whose +/// modifications are reported, each bulk operation as one change and a sort +/// as a permutation. +/// +/// With an extractor the list also watches the observables the extractor +/// names for each element and reports an update of the element when one of +/// them turns invalid. +public final class ArrayObservableList extends ModifiableObservableListBase implements RandomAccess { + + private final List backing; + private final Callback extractor; + private final IdentityHashMap watched; + private final InvalidationListener elementListener; + + /// Wraps a list, optionally watching its elements. + public ArrayObservableList(List backing, Callback extractor) { + this.backing = backing; + this.extractor = extractor; + if (extractor == null) { + watched = null; + elementListener = null; + } else { + watched = new IdentityHashMap(); + elementListener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + elementInvalidated(observable); + } + }; + for (int i = 0; i < backing.size(); i++) { + attach(backing.get(i)); + } + } + } + + private void elementInvalidated(Observable observable) { + beginChange(); + try { + for (int i = 0; i < backing.size(); i++) { + Watched entry = watched.get(backing.get(i)); + if (entry != null && entry.has(observable)) { + nextUpdate(i); + } + } + } finally { + endChange(); + } + } + + private void attach(E element) { + if (extractor == null || element == null) { + return; + } + Watched entry = watched.get(element); + if (entry == null) { + Observable[] observables = extractor.call(element); + entry = new Watched(observables == null ? new Observable[0] : observables.clone()); + watched.put(element, entry); + for (Observable observable : entry.observables) { + observable.addListener(elementListener); + } + } + entry.count++; + } + + private void detach(E element) { + if (extractor == null || element == null) { + return; + } + Watched entry = watched.get(element); + if (entry != null && --entry.count == 0) { + watched.remove(element); + for (Observable observable : entry.observables) { + observable.removeListener(elementListener); + } + } + } + + @Override + public E get(int index) { + return backing.get(index); + } + + @Override + public int size() { + return backing.size(); + } + + @Override + protected void doAdd(int index, E element) { + backing.add(index, element); + attach(element); + } + + @Override + protected E doSet(int index, E element) { + E old = backing.set(index, element); + detach(old); + attach(element); + return old; + } + + @Override + protected E doRemove(int index) { + E old = backing.remove(index); + detach(old); + return old; + } + + @Override + public int indexOf(Object o) { + return backing.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return backing.lastIndexOf(o); + } + + @Override + public boolean contains(Object o) { + return backing.contains(o); + } + + @Override + public Object[] toArray() { + return backing.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return backing.toArray(a); + } + + @Override + public boolean addAll(Collection c) { + return addAll(backing.size(), c); + } + + @Override + public boolean addAll(int index, Collection c) { + if (index < 0 || index > backing.size()) { + throw new IndexOutOfBoundsException("Index: " + index); + } + List copy = new ArrayList(c); + if (copy.isEmpty()) { + return false; + } + beginChange(); + try { + backing.addAll(index, copy); + for (int i = 0; i < copy.size(); i++) { + attach(copy.get(i)); + } + modCount++; + nextAdd(index, index + copy.size()); + } finally { + endChange(); + } + return true; + } + + @Override + protected void removeRange(int fromIndex, int toIndex) { + if (fromIndex >= toIndex) { + return; + } + beginChange(); + try { + List range = backing.subList(fromIndex, toIndex); + List removed = new ArrayList(range); + range.clear(); + for (int i = 0; i < removed.size(); i++) { + detach(removed.get(i)); + } + modCount++; + nextRemove(fromIndex, removed); + } finally { + endChange(); + } + } + + /// Sorts the content and reports the reordering as a permutation. A + /// `null` comparator sorts into the natural order. + /// + /// #### Throws + /// + /// - `ClassCastException`: when there is no comparator and an element + /// is not `Comparable` + @Override + @SuppressWarnings("unchecked") + public void sort(final Comparator comparator) { + int count = backing.size(); + if (count < 2) { + return; + } + final Object[] elements = backing.toArray(); + Integer[] order = new Integer[count]; + for (int i = 0; i < count; i++) { + order[i] = Integer.valueOf(i); + } + Arrays.sort(order, new Comparator() { + @Override + public int compare(Integer a, Integer b) { + Object first = elements[a.intValue()]; + Object second = elements[b.intValue()]; + if (comparator != null) { + return comparator.compare((E) first, (E) second); + } + if (!(first instanceof Comparable)) { + throw new ClassCastException("Element is not Comparable"); + } + return ((Comparable) first).compareTo(second); + } + }); + int[] perm = new int[count]; + boolean moved = false; + for (int i = 0; i < count; i++) { + int old = order[i].intValue(); + perm[old] = i; + moved |= old != i; + } + if (!moved) { + return; + } + beginChange(); + try { + for (int i = 0; i < count; i++) { + backing.set(i, (E) elements[order[i].intValue()]); + } + modCount++; + nextPermutation(0, count, perm); + } finally { + endChange(); + } + } + + /// The observables watched on behalf of one element, and how often that + /// element is in the list. + private static final class Watched { + private final Observable[] observables; + private int count; + + Watched(Observable[] observables) { + this.observables = observables; + } + + boolean has(Observable observable) { + for (Observable candidate : observables) { + if (candidate == observable) { + return true; + } + } + return false; + } + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/BidirectionalBinding.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/BidirectionalBinding.java new file mode 100644 index 00000000000..ee583a3b4de --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/BidirectionalBinding.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.lang.ref.WeakReference; +import java.util.function.Function; + +import javafx.beans.WeakListener; +import javafx.beans.property.Property; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; + +/// Keeps two properties equal: a change of either is written to the other, +/// through a conversion when their types differ. +/// +/// The binding is the change listener of both properties and holds both +/// weakly, so neither property keeps the other alive; when one is collected +/// the binding removes itself from the survivor. Two bindings of the same +/// pair are equal whatever the order, which is how unbinding finds the +/// listener to remove. +public final class BidirectionalBinding implements ChangeListener, WeakListener { + + private final WeakReference first; + private final WeakReference second; + private final Function toFirst; + private final Function toSecond; + private final int hash; + private boolean updating; + + private BidirectionalBinding(Object first, Object second, Function toFirst, + Function toSecond) { + this.first = new WeakReference(first); + this.second = new WeakReference(second); + this.toFirst = toFirst; + this.toSecond = toSecond; + this.hash = System.identityHashCode(first) * System.identityHashCode(second); + } + + private static void check(Object first, Object second) { + if (first == null || second == null) { + throw new NullPointerException("Both properties must be specified."); + } + if (first == second) { + throw new IllegalArgumentException("Cannot bind property to itself"); + } + } + + /// Binds two properties of the same type; the first takes the value of + /// the second. + public static void bind(Property first, Property second) { + bind(first, second, new Function() { + @Override + public T apply(T value) { + return value; + } + }, new Function() { + @Override + public T apply(T value) { + return value; + } + }); + } + + /// Binds two properties through a pair of conversions; the first takes + /// the converted value of the second. + @SuppressWarnings("unchecked") + public static void bind(Property first, Property second, Function toFirst, + Function toSecond) { + check(first, second); + BidirectionalBinding binding = new BidirectionalBinding(first, second, toFirst, toSecond); + first.setValue(toFirst.apply(second.getValue())); + ((ObservableValue) first).addListener(binding); + ((ObservableValue) second).addListener(binding); + } + + /// Removes the binding of two objects, when both are properties and one + /// exists. + @SuppressWarnings("unchecked") + public static void unbind(Object first, Object second) { + check(first, second); + if (first instanceof ObservableValue && second instanceof ObservableValue) { + BidirectionalBinding probe = new BidirectionalBinding(first, second, null, + null); + ((ObservableValue) first).removeListener(probe); + ((ObservableValue) second).removeListener(probe); + } + } + + @Override + public boolean wasGarbageCollected() { + return first.get() == null || second.get() == null; + } + + @Override + @SuppressWarnings("unchecked") + public void changed(ObservableValue source, Object oldValue, Object newValue) { + if (updating) { + return; + } + Object a = first.get(); + Object b = second.get(); + if (!(a instanceof Property) || !(b instanceof Property)) { + if (a instanceof Property) { + ((Property) a).removeListener(this); + } + if (b instanceof Property) { + ((Property) b).removeListener(this); + } + return; + } + Property one = (Property) a; + Property two = (Property) b; + updating = true; + try { + if (one == source) { + two.setValue(toSecond.apply((A) newValue)); + } else { + one.setValue(toFirst.apply((B) newValue)); + } + } catch (RuntimeException failure) { + restore(one, two, source, oldValue); + throw new RuntimeException("Bidirectional binding failed, setting to the previous value", failure); + } finally { + updating = false; + } + } + + /// Puts the changed property back to its old value and drops the binding, + /// so the two properties are not left disagreeing while bound. + @SuppressWarnings("unchecked") + private void restore(Property one, Property two, ObservableValue source, Object oldValue) { + one.removeListener(this); + two.removeListener(this); + if (one == source) { + one.setValue((A) oldValue); + } else { + two.setValue((B) oldValue); + } + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BidirectionalBinding)) { + return false; + } + Object a = first.get(); + Object b = second.get(); + if (a == null || b == null) { + return false; + } + BidirectionalBinding that = (BidirectionalBinding) other; + Object otherA = that.first.get(); + Object otherB = that.second.get(); + return (a == otherA && b == otherB) || (a == otherB && b == otherA); + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/BindingObserver.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/BindingObserver.java new file mode 100644 index 00000000000..2d33788e127 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/BindingObserver.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.lang.ref.WeakReference; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; +import javafx.beans.binding.Binding; + +/// The listener a binding registers with its dependencies. It holds the +/// binding weakly, so a binding nobody uses any more is collected although +/// its dependencies still list this observer, which then removes itself. +public final class BindingObserver implements InvalidationListener, WeakListener { + + private final WeakReference ref; + + /// Creates the observer of a binding. + public BindingObserver(Binding binding) { + if (binding == null) { + throw new NullPointerException("Binding has to be specified."); + } + this.ref = new WeakReference(binding); + } + + @Override + public void invalidated(Observable observable) { + Object binding = ref.get(); + if (binding instanceof Binding) { + ((Binding) binding).invalidate(); + } else { + observable.removeListener(this); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/Changes.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/Changes.java new file mode 100644 index 00000000000..85cc2f3ddb7 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/Changes.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.util.List; + +import javafx.collections.ListChangeListener; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; + +/// The change reports of maps and sets, the report a view sends on behalf +/// of its source, and the loops that deliver them. +public final class Changes { + + private static final int[] NO_PERMUTATION = new int[0]; + + private Changes() { + } + + /// Tells the listeners of a map about a change. + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void fire(ListenerSet> set, ObservableMap map, + MapChangeListener.Change change) { + ListenerSet.fireInvalidation(set, map); + List> targets = ListenerSet.changeListeners(set); + for (int i = 0; i < targets.size(); i++) { + ((MapChangeListener) targets.get(i)).onChanged(change); + } + } + + /// Tells the listeners of a set about a change. + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void fire(ListenerSet> set, ObservableSet observable, + SetChangeListener.Change change) { + ListenerSet.fireInvalidation(set, observable); + List> targets = ListenerSet.changeListeners(set); + for (int i = 0; i < targets.size(); i++) { + ((SetChangeListener) targets.get(i)).onChanged(change); + } + } + + /// What happened to one key of a map. + public static final class OfMap extends MapChangeListener.Change { + private final K key; + private final V removed; + private final V added; + private final boolean wasAdded; + private final boolean wasRemoved; + + /// Creates the report. + public OfMap(ObservableMap map, K key, V removed, V added, boolean wasAdded, boolean wasRemoved) { + super(map); + this.key = key; + this.removed = removed; + this.added = added; + this.wasAdded = wasAdded; + this.wasRemoved = wasRemoved; + } + + @Override + public boolean wasAdded() { + return wasAdded; + } + + @Override + public boolean wasRemoved() { + return wasRemoved; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValueAdded() { + return added; + } + + @Override + public V getValueRemoved() { + return removed; + } + + @Override + public String toString() { + if (wasAdded && wasRemoved) { + return removed + " replaced by " + added + " at key " + key; + } + return wasAdded ? added + " added at key " + key : removed + " removed at key " + key; + } + } + + /// One element added to or removed from a set. + public static final class OfSet extends SetChangeListener.Change { + private final E element; + private final boolean added; + + /// Creates the report. + public OfSet(ObservableSet set, E element, boolean added) { + super(set); + this.element = element; + this.added = added; + } + + @Override + public boolean wasAdded() { + return added; + } + + @Override + public boolean wasRemoved() { + return !added; + } + + @Override + public E getElementAdded() { + return added ? element : null; + } + + @Override + public E getElementRemoved() { + return added ? null : element; + } + + @Override + public String toString() { + return (added ? "added " : "removed ") + element; + } + } + + /// The change of a source list, told again as a change of a view that + /// shows the same content at the same positions. + public static final class Relayed extends ListChangeListener.Change { + private final ListChangeListener.Change change; + + /// Creates the report of a view from the report of its source. + public Relayed(ObservableList list, ListChangeListener.Change change) { + super(list); + this.change = change; + } + + @Override + public boolean next() { + return change.next(); + } + + @Override + public void reset() { + change.reset(); + } + + @Override + public int getFrom() { + return change.getFrom(); + } + + @Override + public int getTo() { + return change.getTo(); + } + + @Override + @SuppressWarnings("unchecked") + public List getRemoved() { + return (List) change.getRemoved(); + } + + @Override + public boolean wasPermutated() { + return change.wasPermutated(); + } + + @Override + public boolean wasUpdated() { + return change.wasUpdated(); + } + + @Override + protected int[] getPermutation() { + if (!change.wasPermutated()) { + return NO_PERMUTATION; + } + int from = change.getFrom(); + int[] result = new int[change.getTo() - from]; + for (int i = 0; i < result.length; i++) { + result[i] = change.getPermutation(from + i); + } + return result; + } + + @Override + public String toString() { + return change.toString(); + } + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ConstantObservableList.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ConstantObservableList.java new file mode 100644 index 00000000000..db6cd7d3724 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ConstantObservableList.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.util.AbstractList; +import java.util.Collection; +import java.util.List; +import java.util.RandomAccess; + +import javafx.beans.InvalidationListener; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +/// An observable list that cannot change. It refuses every modification +/// and, having nothing to report, forgets its listeners. +public final class ConstantObservableList extends AbstractList implements ObservableList, RandomAccess { + + private final List content; + + /// Creates a constant list over content nobody else will modify. + public ConstantObservableList(List content) { + this.content = content; + } + + @Override + public E get(int index) { + return content.get(index); + } + + @Override + public int size() { + return content.size(); + } + + @Override + public void addListener(InvalidationListener listener) { + } + + @Override + public void removeListener(InvalidationListener listener) { + } + + @Override + public void addListener(ListChangeListener listener) { + } + + @Override + public void removeListener(ListChangeListener listener) { + } + + @Override + public boolean addAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setAll(Collection col) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(E... elements) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(int from, int to) { + throw new UnsupportedOperationException(); + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ContentBindings.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ContentBindings.java new file mode 100644 index 00000000000..3c054d8adf6 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ContentBindings.java @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javafx.beans.WeakListener; +import javafx.collections.ListChangeListener; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; + +/// The bindings that make one collection mirror another. +/// +/// A binding is a change listener of the observed collection that replays +/// each change on the mirror, which it holds weakly: a mirror nobody uses is +/// collected, and its binding then removes itself. Bindings of the same +/// mirror are equal, which is how unbinding finds the listener to remove. +public final class ContentBindings { + + private ContentBindings() { + } + + private static void check(Object first, Object second) { + if (first == null || second == null) { + throw new NullPointerException("Both parameters must be specified."); + } + if (first == second) { + throw new IllegalArgumentException("Cannot bind object to itself"); + } + } + + /// Replays a list change on another list holding the same content. + static void replay(ListChangeListener.Change change, List target) { + while (change.next()) { + int from = change.getFrom(); + int to = change.getTo(); + if (change.wasPermutated()) { + target.subList(from, to).clear(); + target.addAll(from, new ArrayList(change.getList().subList(from, to))); + } else { + if (change.wasRemoved()) { + target.subList(from, from + change.getRemovedSize()).clear(); + } + if (change.wasAdded()) { + target.addAll(from, new ArrayList(change.getAddedSubList())); + } + } + } + } + + /// Makes a list mirror an observable list. + public static void bind(List list1, ObservableList list2) { + check(list1, list2); + ListContent binding = new ListContent(list1); + if (list1 instanceof ObservableList) { + ((ObservableList) list1).setAll(list2); + } else { + list1.clear(); + list1.addAll(list2); + } + list2.removeListener(binding); + list2.addListener(binding); + } + + /// Makes a set mirror an observable set. + public static void bind(Set set1, ObservableSet set2) { + check(set1, set2); + SetContent binding = new SetContent(set1); + set1.clear(); + set1.addAll(set2); + set2.removeListener(binding); + set2.addListener(binding); + } + + /// Makes a map mirror an observable map. + public static void bind(Map map1, ObservableMap map2) { + check(map1, map2); + MapContent binding = new MapContent(map1); + map1.clear(); + map1.putAll(map2); + map2.removeListener(binding); + map2.addListener(binding); + } + + /// Removes the binding that makes the first object mirror the second. + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void unbind(Object obj1, Object obj2) { + check(obj1, obj2); + if (obj1 instanceof List && obj2 instanceof ObservableList) { + ((ObservableList) obj2).removeListener(new ListContent((List) obj1)); + } else if (obj1 instanceof Set && obj2 instanceof ObservableSet) { + ((ObservableSet) obj2).removeListener(new SetContent((Set) obj1)); + } else if (obj1 instanceof Map && obj2 instanceof ObservableMap) { + ((ObservableMap) obj2).removeListener(new MapContent((Map) obj1)); + } + } + + /// Keeps two observable lists equal, starting with the content of the + /// second. + public static void bindBidirectional(ObservableList list1, ObservableList list2) { + check(list1, list2); + BothLists binding = new BothLists(list1, list2); + list1.setAll(list2); + list1.addListener(binding); + list2.addListener(binding); + } + + /// Removes the binding that keeps two objects equal. + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void unbindBidirectional(Object obj1, Object obj2) { + check(obj1, obj2); + if (obj1 instanceof ObservableList && obj2 instanceof ObservableList) { + BothLists probe = new BothLists((ObservableList) obj1, (ObservableList) obj2); + ((ObservableList) obj1).removeListener(probe); + ((ObservableList) obj2).removeListener(probe); + } + } + + /// Mirrors an observable list into a list. + private static final class ListContent implements ListChangeListener, WeakListener { + private final WeakReference ref; + + ListContent(List target) { + this.ref = new WeakReference(target); + } + + @Override + @SuppressWarnings("unchecked") + public void onChanged(Change change) { + Object target = ref.get(); + if (target instanceof List) { + replay(change, (List) target); + } else { + change.getList().removeListener(this); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + + @Override + public int hashCode() { + Object target = ref.get(); + return target == null ? 0 : System.identityHashCode(target); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + Object target = ref.get(); + return target != null && other instanceof ListContent && ((ListContent) other).ref.get() == target; + } + } + + /// Mirrors an observable set into a set. + private static final class SetContent implements SetChangeListener, WeakListener { + private final WeakReference ref; + + SetContent(Set target) { + this.ref = new WeakReference(target); + } + + @Override + @SuppressWarnings("unchecked") + public void onChanged(Change change) { + Object target = ref.get(); + if (!(target instanceof Set)) { + change.getSet().removeListener(this); + } else if (change.wasRemoved()) { + ((Set) target).remove(change.getElementRemoved()); + } else { + ((Set) target).add(change.getElementAdded()); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + + @Override + public int hashCode() { + Object target = ref.get(); + return target == null ? 0 : System.identityHashCode(target); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + Object target = ref.get(); + return target != null && other instanceof SetContent && ((SetContent) other).ref.get() == target; + } + } + + /// Mirrors an observable map into a map. + private static final class MapContent implements MapChangeListener, WeakListener { + private final WeakReference ref; + + MapContent(Map target) { + this.ref = new WeakReference(target); + } + + @Override + @SuppressWarnings("unchecked") + public void onChanged(Change change) { + Object target = ref.get(); + if (!(target instanceof Map)) { + change.getMap().removeListener(this); + } else if (change.wasAdded()) { + ((Map) target).put(change.getKey(), change.getValueAdded()); + } else { + ((Map) target).remove(change.getKey()); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + + @Override + public int hashCode() { + Object target = ref.get(); + return target == null ? 0 : System.identityHashCode(target); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + Object target = ref.get(); + return target != null && other instanceof MapContent && ((MapContent) other).ref.get() == target; + } + } + + /// Keeps two observable lists equal, replaying the changes of each on + /// the other and ignoring the echo. + private static final class BothLists implements ListChangeListener, WeakListener { + private final WeakReference first; + private final WeakReference second; + private final int hash; + private boolean updating; + + BothLists(ObservableList first, ObservableList second) { + this.first = new WeakReference(first); + this.second = new WeakReference(second); + this.hash = System.identityHashCode(first) * System.identityHashCode(second); + } + + @Override + @SuppressWarnings("unchecked") + public void onChanged(Change change) { + if (updating) { + return; + } + Object a = first.get(); + Object b = second.get(); + if (!(a instanceof ObservableList) || !(b instanceof ObservableList)) { + if (a instanceof ObservableList) { + ((ObservableList) a).removeListener(this); + } + if (b instanceof ObservableList) { + ((ObservableList) b).removeListener(this); + } + return; + } + updating = true; + try { + replay(change, a == change.getList() ? (ObservableList) b : (ObservableList) a); + } finally { + updating = false; + } + } + + @Override + public boolean wasGarbageCollected() { + return first.get() == null || second.get() == null; + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BothLists)) { + return false; + } + Object a = first.get(); + Object b = second.get(); + if (a == null || b == null) { + return false; + } + BothLists that = (BothLists) other; + Object otherA = that.first.get(); + Object otherB = that.second.get(); + return (a == otherA && b == otherB) || (a == otherB && b == otherA); + } + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/Diagnostics.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/Diagnostics.java new file mode 100644 index 00000000000..7b0fbf0a7cf --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/Diagnostics.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +/// Where the layer reports a failure the JavaFX contract tells it to survive, +/// such as a binding function that throws. +public final class Diagnostics { + + private Diagnostics() { + } + + /// Reports a failure that was recovered from. + public static void report(Throwable failure) { + failure.printStackTrace(); + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/EventDispatchChainImpl.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/EventDispatchChainImpl.java new file mode 100644 index 00000000000..f9096bd297f --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/EventDispatchChainImpl.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.util.ArrayList; + +import javafx.event.Event; +import javafx.event.EventDispatchChain; +import javafx.event.EventDispatcher; + +/// The dispatch chain events travel along: an ordered list of dispatchers +/// and the position of the next one to call. +/// +/// Dispatching hands the event to the dispatcher at the current position +/// together with the chain itself as the tail, moved one position on; a +/// dispatcher that passes the event to the tail thereby reaches the next +/// dispatcher. The position is restored when the dispatcher returns, so the +/// chain can be dispatched through again. +public final class EventDispatchChainImpl implements EventDispatchChain { + + private final ArrayList dispatchers = new ArrayList(); + private int next; + + /// Creates an empty chain. + public EventDispatchChainImpl() { + } + + @Override + public EventDispatchChain append(EventDispatcher eventDispatcher) { + if (eventDispatcher == null) { + throw new NullPointerException(); + } + dispatchers.add(eventDispatcher); + return this; + } + + @Override + public EventDispatchChain prepend(EventDispatcher eventDispatcher) { + if (eventDispatcher == null) { + throw new NullPointerException(); + } + dispatchers.add(next, eventDispatcher); + return this; + } + + @Override + public Event dispatchEvent(Event event) { + if (next >= dispatchers.size()) { + return event; + } + int position = next; + next = position + 1; + try { + return dispatchers.get(position).dispatchEvent(event, this); + } finally { + next = position; + } + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ListChangeBuilder.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ListChangeBuilder.java new file mode 100644 index 00000000000..bc9b64f3823 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ListChangeBuilder.java @@ -0,0 +1,404 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +/// Assembles the change report of an observable list from the single steps +/// its implementation records. +/// +/// The steps arrive in the order the list was modified, each in the +/// coordinates the list had at that moment. The builder keeps them ordered +/// by position, moves the earlier ones as later ones shift the content, and +/// folds neighbours together, so that a removal followed by an insertion at +/// the same place is reported as one replacement and a run of single +/// insertions as one range. An element added and removed again inside one +/// change leaves no trace. +/// +/// A permutation is kept as one only while it is the whole change; combined +/// with any other step it is reported as a replacement of its range, which +/// says the same less precisely. +public final class ListChangeBuilder { + + private static final int ADD_REMOVE = 0; + private static final int UPDATE = 1; + private static final int PERMUTATION = 2; + private static final int[] NO_PERMUTATION = new int[0]; + + private final ObservableList list; + private int depth; + private ArrayList> steps = new ArrayList>(); + + /// Creates the builder of a list. + public ListChangeBuilder(ObservableList list) { + this.list = list; + } + + /// Opens a change; calls nest. + public void beginChange() { + depth++; + } + + /// Closes a change. Returns the report when this closed the outermost + /// one and something was recorded, otherwise `null`. + public ListChangeListener.Change endChange() { + if (depth <= 0) { + throw new IllegalStateException("Called endChange before beginChange"); + } + depth--; + if (depth == 0 && !steps.isEmpty()) { + ArrayList> done = steps; + steps = new ArrayList>(); + return new Built(list, done); + } + return null; + } + + private void check() { + if (depth == 0) { + throw new IllegalStateException("beginChange was not called on this builder"); + } + } + + /// Records that the range from `from` to `to` was added. + public void nextAdd(int from, int to) { + check(); + if (from >= to) { + return; + } + convertPermutation(); + splitUpdate(from); + int count = to - from; + Step target = null; + int insertAt = steps.size(); + for (int i = 0; i < steps.size(); i++) { + Step step = steps.get(i); + if (step.kind == ADD_REMOVE && step.from <= from && from <= step.to) { + target = step; + break; + } + if (step.from >= from) { + insertAt = i; + break; + } + } + if (target == null) { + target = new Step(ADD_REMOVE, from, from); + steps.add(insertAt, target); + } + for (int i = 0; i < steps.size(); i++) { + Step step = steps.get(i); + if (step != target && step.from >= from) { + step.from += count; + step.to += count; + } + } + target.to += count; + tidy(); + } + + /// Records that an element was removed from a position. + public void nextRemove(int index, E removed) { + check(); + convertPermutation(); + splitUpdate(index); + Step target = null; + int insertAt = steps.size(); + for (int i = 0; i < steps.size(); i++) { + Step step = steps.get(i); + if (step.kind == ADD_REMOVE) { + if (step.from <= index && index < step.to) { + // The element was added by this very change. + step.to--; + target = step; + break; + } + if (step.to == index) { + step.removed.add(removed); + target = step; + break; + } + if (step.from == index + 1) { + step.from--; + step.to--; + step.removed.add(0, removed); + target = step; + break; + } + } + if (step.from >= index) { + insertAt = i; + break; + } + } + if (target == null) { + target = new Step(ADD_REMOVE, index, index); + target.removed.add(removed); + steps.add(insertAt, target); + } + for (int i = 0; i < steps.size(); i++) { + Step step = steps.get(i); + if (step == target) { + continue; + } + if (step.from > index) { + step.from--; + step.to--; + } else if (step.kind == UPDATE && step.from <= index && index < step.to) { + step.to--; + } + } + tidy(); + } + + /// Records that the given elements were removed from a position. + public void nextRemove(int index, List removed) { + for (int i = 0; i < removed.size(); i++) { + nextRemove(index, removed.get(i)); + } + } + + /// Records that the element at a position was replaced. + public void nextSet(int index, E old) { + nextRemove(index, old); + nextAdd(index, index + 1); + } + + /// Records that the range now at `from` to `to` replaced the given + /// elements. + public void nextReplace(int from, int to, List removed) { + nextRemove(from, removed); + nextAdd(from, to); + } + + /// Records that the element at a position changed its own state. + public void nextUpdate(int index) { + check(); + convertPermutation(); + int insertAt = steps.size(); + for (int i = 0; i < steps.size(); i++) { + Step step = steps.get(i); + if (step.from <= index && index < step.to) { + // Already reported as added or updated. + return; + } + } + for (int i = 0; i < steps.size(); i++) { + Step step = steps.get(i); + if (step.kind == UPDATE && step.to == index) { + step.to++; + tidy(); + return; + } + if (step.kind == UPDATE && step.from == index + 1) { + step.from--; + tidy(); + return; + } + if (step.from > index) { + insertAt = i; + break; + } + } + steps.add(insertAt, new Step(UPDATE, index, index + 1)); + } + + /// Records that a range was reordered; entry `i` of the permutation is + /// the new position of the element that was at `from + i`. + public void nextPermutation(int from, int to, int[] perm) { + check(); + if (from >= to) { + return; + } + // The order before the permutation, should it have to be reported + // as a replacement. + List before = new ArrayList(to - from); + for (int i = 0; i < to - from; i++) { + before.add(list.get(perm[i])); + } + if (steps.isEmpty()) { + Step step = new Step(PERMUTATION, from, to); + step.removed = before; + step.perm = perm.clone(); + steps.add(step); + return; + } + Step first = steps.get(0); + if (steps.size() == 1 && first.kind == PERMUTATION && first.from == from && first.to == to) { + for (int i = 0; i < first.perm.length; i++) { + first.perm[i] = perm[first.perm[i] - from]; + } + return; + } + convertPermutation(); + nextReplace(from, to, before); + } + + private void convertPermutation() { + if (steps.size() == 1) { + Step step = steps.get(0); + if (step.kind == PERMUTATION) { + step.kind = ADD_REMOVE; + step.perm = null; + } + } + } + + /// Cuts an update range in two at a position strictly inside it, so a + /// step recorded at that position can sit between the halves. + private void splitUpdate(int index) { + for (int i = 0; i < steps.size(); i++) { + Step step = steps.get(i); + if (step.kind == UPDATE && step.from < index && index < step.to) { + Step tail = new Step(UPDATE, index, step.to); + step.to = index; + steps.add(i + 1, tail); + return; + } + } + } + + /// Drops the steps that ended up describing nothing and joins the + /// neighbours of the same kind that touch. + private void tidy() { + for (int i = steps.size() - 1; i >= 0; i--) { + Step step = steps.get(i); + if (step.from == step.to && (step.kind == UPDATE || step.removed.isEmpty())) { + steps.remove(i); + } + } + for (int i = 0; i + 1 < steps.size(); i++) { + Step first = steps.get(i); + Step second = steps.get(i + 1); + if (first.kind == second.kind && first.kind != PERMUTATION && first.to == second.from) { + first.to = second.to; + first.removed.addAll(second.removed); + steps.remove(i + 1); + i--; + } + } + } + + /// One step of a change. + private static final class Step { + private int kind; + private int from; + private int to; + private List removed = new ArrayList(); + private int[] perm; + + Step(int kind, int from, int to) { + this.kind = kind; + this.from = from; + this.to = to; + } + } + + /// The finished report. + private static final class Built extends ListChangeListener.Change { + private final List> steps; + private int cursor = -1; + + Built(ObservableList list, List> steps) { + super(list); + this.steps = steps; + } + + private Step current() { + if (cursor < 0) { + throw new IllegalStateException( + "Invalid Change state: next() must be called before inspecting the Change."); + } + return steps.get(cursor); + } + + @Override + public boolean next() { + if (cursor + 1 < steps.size()) { + cursor++; + return true; + } + return false; + } + + @Override + public void reset() { + cursor = -1; + } + + @Override + public int getFrom() { + return current().from; + } + + @Override + public int getTo() { + return current().to; + } + + @Override + public List getRemoved() { + Step step = current(); + if (step.kind == ADD_REMOVE) { + return Collections.unmodifiableList(step.removed); + } + return Collections.emptyList(); + } + + @Override + protected int[] getPermutation() { + Step step = current(); + return step.kind == PERMUTATION ? step.perm : NO_PERMUTATION; + } + + @Override + public boolean wasUpdated() { + return current().kind == UPDATE; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder("{ "); + for (int i = 0; i < steps.size(); i++) { + Step step = steps.get(i); + if (i > 0) { + result.append(", "); + } + if (step.kind == PERMUTATION) { + result.append("permutated [").append(step.from).append(", ").append(step.to).append(')'); + } else if (step.kind == UPDATE) { + result.append("updated [").append(step.from).append(", ").append(step.to).append(')'); + } else { + result.append(step.removed).append(" replaced by [").append(step.from).append(", ") + .append(step.to).append(')'); + } + } + return result.append(" }").toString(); + } + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ListenerSet.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ListenerSet.java new file mode 100644 index 00000000000..3d3c73e71f0 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ListenerSet.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; + +/// The listeners of one observable: its invalidation listeners and its +/// change listeners of type `L`. +/// +/// Both lists are replaced, never edited, when a listener is added or +/// removed, so a notification in progress keeps walking the list it started +/// with and a listener may add or remove listeners freely. An owner with no +/// listeners holds `null` instead of an empty set, which is why every +/// operation is static and returns the set to keep. +public final class ListenerSet { + + private List invalidation = Collections.emptyList(); + private List change = Collections.emptyList(); + + private ListenerSet() { + } + + /// Returns a copy with a listener appended and without the weak + /// listeners whose target is gone. + static List plus(List source, X listener) { + if (listener == null) { + throw new NullPointerException(); + } + List result = new ArrayList(source.size() + 1); + for (int i = 0; i < source.size(); i++) { + X existing = source.get(i); + if (!(existing instanceof WeakListener) || !((WeakListener) existing).wasGarbageCollected()) { + result.add(existing); + } + } + result.add(listener); + return result; + } + + /// Returns a copy without the first listener equal to the given one, or + /// the list itself when there is none. + static List minus(List source, Object listener) { + if (listener == null) { + throw new NullPointerException(); + } + int index = source.indexOf(listener); + if (index < 0) { + return source; + } + List result = new ArrayList(source); + result.remove(index); + return result; + } + + /// Adds an invalidation listener. + public static ListenerSet addInvalidation(ListenerSet set, InvalidationListener listener) { + ListenerSet result = set == null ? new ListenerSet() : set; + result.invalidation = plus(result.invalidation, listener); + return result; + } + + /// Removes an invalidation listener; returns `null` once nothing is left. + public static ListenerSet removeInvalidation(ListenerSet set, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + if (set == null) { + return null; + } + set.invalidation = minus(set.invalidation, listener); + return set.invalidation.isEmpty() && set.change.isEmpty() ? null : set; + } + + /// Adds a change listener. + public static ListenerSet addChange(ListenerSet set, L listener) { + ListenerSet result = set == null ? new ListenerSet() : set; + result.change = plus(result.change, listener); + return result; + } + + /// Removes a change listener; returns `null` once nothing is left. + public static ListenerSet removeChange(ListenerSet set, Object listener) { + if (listener == null) { + throw new NullPointerException(); + } + if (set == null) { + return null; + } + set.change = minus(set.change, listener); + return set.invalidation.isEmpty() && set.change.isEmpty() ? null : set; + } + + /// Tells every invalidation listener that the observable is invalid. + public static void fireInvalidation(ListenerSet set, Observable observable) { + if (set != null) { + List listeners = set.invalidation; + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).invalidated(observable); + } + } + } + + /// Returns the change listeners as they are now; the result is never + /// modified afterwards and must not be modified by the caller. + public static List changeListeners(ListenerSet set) { + if (set == null) { + return Collections.emptyList(); + } + return set.change; + } + + /// Returns whether the set holds any listener at all. + public static boolean hasListeners(ListenerSet set) { + return set != null; + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ObservableMapWrapper.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ObservableMapWrapper.java new file mode 100644 index 00000000000..c25a2fe5a32 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ObservableMapWrapper.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javafx.beans.InvalidationListener; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableMap; + +/// The observable map `FXCollections` hands out: a `java.util.Map` whose +/// modifications are reported one key at a time, also when they are made +/// through its key, value or entry views. +public final class ObservableMapWrapper extends AbstractMap implements ObservableMap { + + private final Map backing; + private ListenerSet> listeners; + private Set> entries; + + /// Wraps a map. + public ObservableMapWrapper(Map backing) { + this.backing = backing; + } + + @Override + public void addListener(InvalidationListener listener) { + listeners = ListenerSet.addInvalidation(listeners, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + listeners = ListenerSet.removeInvalidation(listeners, listener); + } + + @Override + public void addListener(MapChangeListener listener) { + listeners = ListenerSet.addChange(listeners, listener); + } + + @Override + public void removeListener(MapChangeListener listener) { + listeners = ListenerSet.removeChange(listeners, listener); + } + + private void fire(K key, V removed, V added, boolean wasAdded, boolean wasRemoved) { + if (ListenerSet.hasListeners(listeners)) { + Changes.fire(listeners, this, new Changes.OfMap(this, key, removed, added, wasAdded, wasRemoved)); + } + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public boolean isEmpty() { + return backing.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return backing.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return backing.containsValue(value); + } + + @Override + public V get(Object key) { + return backing.get(key); + } + + @Override + public V put(K key, V value) { + boolean present = backing.containsKey(key); + V old = backing.put(key, value); + if (!present) { + fire(key, null, value, true, false); + } else if (old == null ? value != null : !old.equals(value)) { + fire(key, old, value, true, true); + } + return old; + } + + @Override + @SuppressWarnings("unchecked") + public V remove(Object key) { + if (!backing.containsKey(key)) { + return null; + } + V old = backing.remove(key); + fire((K) key, old, null, false, true); + return old; + } + + @Override + public void clear() { + for (Iterator> it = backing.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + K key = entry.getKey(); + V value = entry.getValue(); + it.remove(); + fire(key, value, null, false, true); + } + } + + @Override + public Set> entrySet() { + if (entries == null) { + entries = new AbstractSet>() { + @Override + public int size() { + return backing.size(); + } + + @Override + public Iterator> iterator() { + return new EntryIterator(); + } + }; + } + return entries; + } + + /// Walks the entries, reporting a removal made through the iterator and + /// a value set through an entry. + private final class EntryIterator implements Iterator> { + private final Iterator> source = backing.entrySet().iterator(); + private K lastKey; + private V lastValue; + + @Override + public boolean hasNext() { + return source.hasNext(); + } + + @Override + public Map.Entry next() { + final Map.Entry entry = source.next(); + lastKey = entry.getKey(); + lastValue = entry.getValue(); + return new Map.Entry() { + @Override + public K getKey() { + return entry.getKey(); + } + + @Override + public V getValue() { + return entry.getValue(); + } + + @Override + public V setValue(V value) { + V old = entry.setValue(value); + lastValue = value; + if (old == null ? value != null : !old.equals(value)) { + fire(entry.getKey(), old, value, true, true); + } + return old; + } + + @Override + public boolean equals(Object other) { + return entry.equals(other); + } + + @Override + public int hashCode() { + return entry.hashCode(); + } + + @Override + public String toString() { + return entry.toString(); + } + }; + } + + @Override + public void remove() { + source.remove(); + fire(lastKey, lastValue, null, false, true); + } + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ObservableSetWrapper.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ObservableSetWrapper.java new file mode 100644 index 00000000000..54166fac286 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ObservableSetWrapper.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.util.AbstractSet; +import java.util.Iterator; +import java.util.Set; + +import javafx.beans.InvalidationListener; +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; + +/// The observable set `FXCollections` hands out: a `java.util.Set` whose +/// modifications are reported one element at a time. +public final class ObservableSetWrapper extends AbstractSet implements ObservableSet { + + private final Set backing; + private ListenerSet> listeners; + + /// Wraps a set. + public ObservableSetWrapper(Set backing) { + this.backing = backing; + } + + @Override + public void addListener(InvalidationListener listener) { + listeners = ListenerSet.addInvalidation(listeners, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + listeners = ListenerSet.removeInvalidation(listeners, listener); + } + + @Override + public void addListener(SetChangeListener listener) { + listeners = ListenerSet.addChange(listeners, listener); + } + + @Override + public void removeListener(SetChangeListener listener) { + listeners = ListenerSet.removeChange(listeners, listener); + } + + private void fire(E element, boolean added) { + if (ListenerSet.hasListeners(listeners)) { + Changes.fire(listeners, this, new Changes.OfSet(this, element, added)); + } + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public boolean isEmpty() { + return backing.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return backing.contains(o); + } + + @Override + public boolean add(E element) { + if (backing.add(element)) { + fire(element, true); + return true; + } + return false; + } + + @Override + @SuppressWarnings("unchecked") + public boolean remove(Object o) { + if (backing.remove(o)) { + fire((E) o, false); + return true; + } + return false; + } + + @Override + public void clear() { + for (Iterator it = backing.iterator(); it.hasNext();) { + E element = it.next(); + it.remove(); + fire(element, false); + } + } + + @Override + public Iterator iterator() { + final Iterator source = backing.iterator(); + return new Iterator() { + private E last; + + @Override + public boolean hasNext() { + return source.hasNext(); + } + + @Override + public E next() { + last = source.next(); + return last; + } + + @Override + public void remove() { + source.remove(); + fire(last, false); + } + }; + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/PropertyText.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/PropertyText.java new file mode 100644 index 00000000000..ba8fdd1e224 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/PropertyText.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import javafx.beans.property.ReadOnlyProperty; + +/// The text properties describe themselves with. +public final class PropertyText { + + private PropertyText() { + } + + /// Builds `Type [bean: b, name: n, tail]`, leaving out a missing bean + /// and a missing or empty name. + public static String describe(String type, ReadOnlyProperty property, String tail) { + StringBuilder result = new StringBuilder(type); + result.append(" ["); + Object bean = property.getBean(); + String name = property.getName(); + if (bean != null) { + result.append("bean: ").append(bean).append(", "); + } + if (name != null && name.length() > 0) { + result.append("name: ").append(name).append(", "); + } + return result.append(tail).append(']').toString(); + } + + /// Returns `Bean.name : ` for a property that has both, otherwise an + /// empty string; the prefix of the error a bound property raises. + public static String owner(ReadOnlyProperty property) { + Object bean = property.getBean(); + String name = property.getName(); + if (bean != null && name != null) { + return bean.getClass().getSimpleName() + "." + name + " : "; + } + return ""; + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/UnmodifiableViews.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/UnmodifiableViews.java new file mode 100644 index 00000000000..43b5429cb09 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/UnmodifiableViews.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javafx.beans.InvalidationListener; +import javafx.collections.ListChangeListener; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableListBase; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; +import javafx.collections.WeakListChangeListener; +import javafx.collections.WeakMapChangeListener; +import javafx.collections.WeakSetChangeListener; + +/// Read-only views of observable collections. Each refuses modification, +/// shows the content of its source and tells its own listeners about every +/// change of the source, which it observes weakly. +public final class UnmodifiableViews { + + private UnmodifiableViews() { + } + + /// A read-only view of a list. + public static final class OfList extends ObservableListBase { + private final ObservableList source; + private final ListChangeListener listener; + + /// Creates the view. + public OfList(ObservableList source) { + this.source = source; + this.listener = new ListChangeListener() { + @Override + public void onChanged(Change change) { + relay(change); + } + }; + source.addListener(new WeakListChangeListener(listener)); + } + + private void relay(ListChangeListener.Change change) { + modCount++; + fireChange(new Changes.Relayed(this, change)); + } + + @Override + public E get(int index) { + return source.get(index); + } + + @Override + public int size() { + return source.size(); + } + } + + /// A read-only view of a map. + public static final class OfMap extends AbstractMap implements ObservableMap { + private final ObservableMap source; + private final Map readOnly; + private final MapChangeListener listener; + private ListenerSet> listeners; + + /// Creates the view. + public OfMap(ObservableMap source) { + this.source = source; + this.readOnly = Collections.unmodifiableMap(source); + this.listener = new MapChangeListener() { + @Override + public void onChanged(Change change) { + relay(change); + } + }; + source.addListener(new WeakMapChangeListener(listener)); + } + + private void relay(MapChangeListener.Change change) { + if (ListenerSet.hasListeners(listeners)) { + Changes.fire(listeners, this, new Changes.OfMap(this, change.getKey(), + change.getValueRemoved(), change.getValueAdded(), change.wasAdded(), change.wasRemoved())); + } + } + + @Override + public void addListener(InvalidationListener invalidationListener) { + listeners = ListenerSet.addInvalidation(listeners, invalidationListener); + } + + @Override + public void removeListener(InvalidationListener invalidationListener) { + listeners = ListenerSet.removeInvalidation(listeners, invalidationListener); + } + + @Override + public void addListener(MapChangeListener changeListener) { + listeners = ListenerSet.addChange(listeners, changeListener); + } + + @Override + public void removeListener(MapChangeListener changeListener) { + listeners = ListenerSet.removeChange(listeners, changeListener); + } + + @Override + public int size() { + return source.size(); + } + + @Override + public boolean containsKey(Object key) { + return source.containsKey(key); + } + + @Override + public V get(Object key) { + return source.get(key); + } + + @Override + public Set> entrySet() { + return readOnly.entrySet(); + } + } + + /// A read-only view of a set. + public static final class OfSet extends AbstractSet implements ObservableSet { + private final ObservableSet source; + private final Set readOnly; + private final SetChangeListener listener; + private ListenerSet> listeners; + + /// Creates the view. + public OfSet(ObservableSet source) { + this.source = source; + this.readOnly = Collections.unmodifiableSet(source); + this.listener = new SetChangeListener() { + @Override + public void onChanged(Change change) { + relay(change); + } + }; + source.addListener(new WeakSetChangeListener(listener)); + } + + private void relay(SetChangeListener.Change change) { + if (ListenerSet.hasListeners(listeners)) { + boolean added = change.wasAdded(); + E element = added ? change.getElementAdded() : change.getElementRemoved(); + Changes.fire(listeners, this, new Changes.OfSet(this, element, added)); + } + } + + @Override + public void addListener(InvalidationListener invalidationListener) { + listeners = ListenerSet.addInvalidation(listeners, invalidationListener); + } + + @Override + public void removeListener(InvalidationListener invalidationListener) { + listeners = ListenerSet.removeInvalidation(listeners, invalidationListener); + } + + @Override + public void addListener(SetChangeListener changeListener) { + listeners = ListenerSet.addChange(listeners, changeListener); + } + + @Override + public void removeListener(SetChangeListener changeListener) { + listeners = ListenerSet.removeChange(listeners, changeListener); + } + + @Override + public int size() { + return source.size(); + } + + @Override + public boolean contains(Object o) { + return source.contains(o); + } + + @Override + public Iterator iterator() { + return readOnly.iterator(); + } + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ValueListeners.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ValueListeners.java new file mode 100644 index 00000000000..892c1ac16a3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/ValueListeners.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat.runtime; + +import java.util.List; + +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; + +/// The listeners of an observable value, and the last value its change +/// listeners were told about. +/// +/// The first change listener makes the owner eager: its value is read when +/// the listener is added and again on every invalidation, because that is +/// the only way to know the old and the new value. +public final class ValueListeners { + + private final ObservableValue observable; + private ListenerSet> set; + private T currentValue; + + private ValueListeners(ObservableValue observable) { + this.observable = observable; + } + + /// Adds an invalidation listener. + public static ValueListeners add(ValueListeners helper, ObservableValue observable, + InvalidationListener listener) { + if (observable == null || listener == null) { + throw new NullPointerException(); + } + ValueListeners result = helper == null ? new ValueListeners(observable) : helper; + result.set = ListenerSet.addInvalidation(result.set, listener); + return result; + } + + /// Removes an invalidation listener; returns `null` once nothing is left. + public static ValueListeners remove(ValueListeners helper, InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + if (helper == null) { + return null; + } + helper.set = ListenerSet.removeInvalidation(helper.set, listener); + return helper.set == null ? null : helper; + } + + /// Adds a change listener, reading the value if it is the first one. + public static ValueListeners add(ValueListeners helper, ObservableValue observable, + ChangeListener listener) { + if (observable == null || listener == null) { + throw new NullPointerException(); + } + ValueListeners result = helper == null ? new ValueListeners(observable) : helper; + boolean first = ListenerSet.changeListeners(result.set).isEmpty(); + result.set = ListenerSet.addChange(result.set, listener); + if (first) { + result.currentValue = observable.getValue(); + } + return result; + } + + /// Removes a change listener; returns `null` once nothing is left. + public static ValueListeners remove(ValueListeners helper, ChangeListener listener) { + if (listener == null) { + throw new NullPointerException(); + } + if (helper == null) { + return null; + } + helper.set = ListenerSet.removeChange(helper.set, listener); + if (ListenerSet.changeListeners(helper.set).isEmpty()) { + helper.currentValue = null; + } + return helper.set == null ? null : helper; + } + + /// Tells the invalidation listeners the value is stale and, when the + /// value really differs from the last one reported, the change listeners. + public static void fire(ValueListeners helper) { + if (helper == null) { + return; + } + ListenerSet> set = helper.set; + ListenerSet.fireInvalidation(set, helper.observable); + List> listeners = ListenerSet.changeListeners(set); + if (!listeners.isEmpty()) { + T oldValue = helper.currentValue; + T newValue = helper.observable.getValue(); + helper.currentValue = newValue; + boolean changed = newValue == null ? oldValue != null : !newValue.equals(oldValue); + if (changed) { + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).changed(helper.observable, oldValue, newValue); + } + } + } + } +} diff --git a/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/package-info.java b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/package-info.java new file mode 100644 index 00000000000..56184a93459 --- /dev/null +++ b/maven/javafx-compat/src/main/java/com/codename1/fxcompat/runtime/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The machinery behind the `javafx` packages of this module: listener +/// bookkeeping, change assembly and the bindings that keep two objects in +/// sync. Nothing here is JavaFX API; applications must not use it. +package com.codename1.fxcompat.runtime; diff --git a/maven/javafx-compat/src/main/java/javafx/beans/DefaultProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/DefaultProperty.java new file mode 100644 index 00000000000..f876628abe2 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/DefaultProperty.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// Names the property of a class that child content is added to when no +/// property is named. +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DefaultProperty { + + /// The name of the default property. + String value(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/InvalidationListener.java b/maven/javafx-compat/src/main/java/javafx/beans/InvalidationListener.java new file mode 100644 index 00000000000..ddb362c6dd3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/InvalidationListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans; + +/// Told when an [Observable] turns invalid. +@FunctionalInterface +public interface InvalidationListener { + + /// Called when the observable turned invalid. + void invalidated(Observable observable); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/NamedArg.java b/maven/javafx-compat/src/main/java/javafx/beans/NamedArg.java new file mode 100644 index 00000000000..841f2543dcb --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/NamedArg.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/// Names a constructor parameter, so a declarative loader can match an +/// attribute to it. +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface NamedArg { + + /// The name of the parameter. + String value(); + + /// The value used when the attribute is absent. + String defaultValue() default ""; +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/Observable.java b/maven/javafx-compat/src/main/java/javafx/beans/Observable.java new file mode 100644 index 00000000000..aa2e22d9bb0 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/Observable.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans; + +/// Something whose content can become stale and that says so. +/// +/// An invalidation tells a listener only that the content may have changed. +/// Implementations that evaluate lazily send it once and stay silent until +/// the content has been read again. +public interface Observable { + + /// Adds a listener told whenever this object turns invalid. Adding the + /// same listener twice makes it hear every invalidation twice. + void addListener(InvalidationListener listener); + + /// Removes one registration of the listener; unknown listeners are ignored. + void removeListener(InvalidationListener listener); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/WeakInvalidationListener.java b/maven/javafx-compat/src/main/java/javafx/beans/WeakInvalidationListener.java new file mode 100644 index 00000000000..8a2aade86a4 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/WeakInvalidationListener.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans; + +import java.lang.ref.WeakReference; + +/// An [InvalidationListener] that forwards to another one without keeping it +/// alive. Whoever creates it must hold the wrapped listener; once that is +/// collected, this listener removes itself on the next invalidation. +public final class WeakInvalidationListener implements InvalidationListener, WeakListener { + + private final WeakReference ref; + + /// Wraps a listener. + public WeakInvalidationListener(InvalidationListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + this.ref = new WeakReference(listener); + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + + @Override + public void invalidated(Observable observable) { + Object listener = ref.get(); + if (listener instanceof InvalidationListener) { + ((InvalidationListener) listener).invalidated(observable); + } else { + observable.removeListener(this); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/WeakListener.java b/maven/javafx-compat/src/main/java/javafx/beans/WeakListener.java new file mode 100644 index 00000000000..f30c3ef8022 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/WeakListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans; + +/// Implemented by listeners that hold their target weakly, so the observable +/// they are registered with can drop them once the target is gone. +public interface WeakListener { + + /// Returns whether the wrapped listener has been garbage collected. + boolean wasGarbageCollected(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/Binding.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/Binding.java new file mode 100644 index 00000000000..bb4b9f0d2f3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/Binding.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; + +/// A value computed from other observable values. +/// +/// A binding is valid once its value has been computed and turns invalid +/// when a dependency changes; it tells its listeners so once and recomputes +/// only when the value is next read. +public interface Binding extends ObservableValue { + + /// Returns whether the cached value is current. + boolean isValid(); + + /// Marks the value stale, so the next read computes it again. + void invalidate(); + + /// Returns what this binding depends on, as an unmodifiable list. + ObservableList getDependencies(); + + /// Tells the binding it is no longer used, so it can stop observing its + /// dependencies. Bindings observe weakly, so calling this is optional. + void dispose(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/Bindings.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/Bindings.java new file mode 100644 index 00000000000..f99a6c872f3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/Bindings.java @@ -0,0 +1,1538 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; + +import com.codename1.fxcompat.runtime.BidirectionalBinding; +import com.codename1.fxcompat.runtime.ContentBindings; +import com.codename1.fxcompat.runtime.Diagnostics; + +import javafx.beans.Observable; +import javafx.beans.property.Property; +import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableDoubleValue; +import javafx.beans.value.ObservableFloatValue; +import javafx.beans.value.ObservableIntegerValue; +import javafx.beans.value.ObservableLongValue; +import javafx.beans.value.ObservableNumberValue; +import javafx.beans.value.ObservableObjectValue; +import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; +import javafx.util.StringConverter; + +/// The operators of the binding API: every method returns a binding that +/// follows its operands and recomputes lazily. +/// +/// An arithmetic result has the wider of its two operand types -- `double` +/// over `float` over `long` over `int` -- as the same expression would in +/// Java. An integer division by zero throws `ArithmeticException` when the +/// binding is read. +public final class Bindings { + + static final int ADD = 0; + static final int SUB = 1; + static final int MUL = 2; + static final int DIV = 3; + static final int MIN = 4; + static final int MAX = 5; + static final int NEG = 6; + + static final int EQ = 0; + static final int NE = 1; + static final int GT = 2; + static final int LT = 3; + static final int GE = 4; + static final int LE = 5; + static final int EQ_IC = 6; + static final int NE_IC = 7; + + private Bindings() { + } + + // ---- shared machinery + + /// Returns the candidates a binding has to observe: the ones that are + /// there and can change. + static Observable[] deps(Observable... candidates) { + int count = 0; + for (Observable candidate : candidates) { + if (candidate != null && !Const.isConstant(candidate)) { + count++; + } + } + Observable[] result = new Observable[count]; + int next = 0; + for (Observable candidate : candidates) { + if (candidate != null && !Const.isConstant(candidate)) { + result[next++] = candidate; + } + } + return result; + } + + private static int rank(ObservableNumberValue value) { + if (value instanceof ObservableDoubleValue) { + return 3; + } + if (value instanceof ObservableFloatValue) { + return 2; + } + if (value instanceof ObservableLongValue) { + return 1; + } + return 0; + } + + private static void operands(Object first, Object second) { + if (first == null || second == null) { + throw new NullPointerException("Operands cannot be null."); + } + } + + static NumberBinding arith(final int op, final ObservableNumberValue a, final ObservableNumberValue b) { + operands(a, b); + Observable[] deps = deps(a, b); + switch (Math.max(rank(a), rank(b))) { + case 3: + return new Fn.DoubleFn(() -> apply(op, a.doubleValue(), b.doubleValue()), deps); + case 2: + return new Fn.FloatFn(() -> apply(op, a.floatValue(), b.floatValue()), deps); + case 1: + return new Fn.LongFn(() -> apply(op, a.longValue(), b.longValue()), deps); + default: + return new Fn.IntegerFn(() -> apply(op, a.intValue(), b.intValue()), deps); + } + } + + static NumberBinding choose(final ObservableBooleanValue condition, final ObservableNumberValue a, + final ObservableNumberValue b) { + Observable[] deps = deps(condition, a, b); + switch (Math.max(rank(a), rank(b))) { + case 3: + return new Fn.DoubleFn(() -> condition.get() ? a.doubleValue() : b.doubleValue(), deps); + case 2: + return new Fn.FloatFn(() -> condition.get() ? a.floatValue() : b.floatValue(), deps); + case 1: + return new Fn.LongFn(() -> condition.get() ? a.longValue() : b.longValue(), deps); + default: + return new Fn.IntegerFn(() -> condition.get() ? a.intValue() : b.intValue(), deps); + } + } + + private static double apply(int op, double x, double y) { + switch (op) { + case ADD: + return x + y; + case SUB: + return x - y; + case MUL: + return x * y; + case DIV: + return x / y; + case MIN: + return Math.min(x, y); + case MAX: + return Math.max(x, y); + default: + return -x; + } + } + + private static float apply(int op, float x, float y) { + switch (op) { + case ADD: + return x + y; + case SUB: + return x - y; + case MUL: + return x * y; + case DIV: + return x / y; + case MIN: + return Math.min(x, y); + case MAX: + return Math.max(x, y); + default: + return -x; + } + } + + private static long apply(int op, long x, long y) { + switch (op) { + case ADD: + return x + y; + case SUB: + return x - y; + case MUL: + return x * y; + case DIV: + // The device VM answers 0 for an integer division by zero, + // so the exception Java promises is raised by hand. + if (y == 0) { + throw new ArithmeticException("/ by zero"); + } + return x / y; + case MIN: + return Math.min(x, y); + case MAX: + return Math.max(x, y); + default: + return -x; + } + } + + private static int apply(int op, int x, int y) { + switch (op) { + case ADD: + return x + y; + case SUB: + return x - y; + case MUL: + return x * y; + case DIV: + if (y == 0) { + throw new ArithmeticException("/ by zero"); + } + return x / y; + case MIN: + return Math.min(x, y); + case MAX: + return Math.max(x, y); + default: + return -x; + } + } + + static BooleanBinding compare(final int op, final ObservableNumberValue a, final ObservableNumberValue b, + final double epsilon) { + operands(a, b); + Observable[] deps = deps(a, b); + switch (Math.max(rank(a), rank(b))) { + case 3: + return new Fn.BooleanFn(() -> { + double x = a.doubleValue(); + double y = b.doubleValue(); + return pick(op, Math.abs(x - y) <= epsilon, x > y, x >= y, x < y, x <= y); + }, deps); + case 2: + return new Fn.BooleanFn(() -> { + float x = a.floatValue(); + float y = b.floatValue(); + return pick(op, Math.abs(x - y) <= epsilon, x > y, x >= y, x < y, x <= y); + }, deps); + case 1: + return new Fn.BooleanFn(() -> { + long x = a.longValue(); + long y = b.longValue(); + return pick(op, Math.abs(x - y) <= epsilon, x > y, x >= y, x < y, x <= y); + }, deps); + default: + return new Fn.BooleanFn(() -> { + int x = a.intValue(); + int y = b.intValue(); + return pick(op, Math.abs(x - y) <= epsilon, x > y, x >= y, x < y, x <= y); + }, deps); + } + } + + private static boolean pick(int op, boolean eq, boolean gt, boolean ge, boolean lt, boolean le) { + switch (op) { + case EQ: + return eq; + case NE: + return !eq; + case GT: + return gt; + case GE: + return ge; + case LT: + return lt; + default: + return le; + } + } + + static BooleanBinding strings(final int op, final ObservableStringValue a, final ObservableStringValue b) { + operands(a, b); + return new Fn.BooleanFn(() -> { + String x = a.get(); + String y = b.get(); + if (x == null) { + x = ""; + } + if (y == null) { + y = ""; + } + switch (op) { + case EQ: + return x.equals(y); + case NE: + return !x.equals(y); + case EQ_IC: + return x.equalsIgnoreCase(y); + case NE_IC: + return !x.equalsIgnoreCase(y); + case GT: + return x.compareTo(y) > 0; + case GE: + return x.compareTo(y) >= 0; + case LT: + return x.compareTo(y) < 0; + default: + return x.compareTo(y) <= 0; + } + }, deps(a, b)); + } + + /// The text of any observable value; `"null"` for none. + static StringBinding text(final ObservableValue value) { + if (value == null) { + throw new NullPointerException("ObservableValue must be specified"); + } + return new Fn.StringFn(() -> String.valueOf(value.getValue()), value); + } + + private static Observable[] observables(Object[] args) { + int count = 0; + for (Object arg : args) { + if (arg instanceof ObservableValue) { + count++; + } + } + Observable[] result = new Observable[count]; + int next = 0; + for (Object arg : args) { + if (arg instanceof ObservableValue) { + result[next++] = (ObservableValue) arg; + } + } + return result; + } + + static StringBinding formatted(final String format, final Object... args) { + if (format == null) { + throw new NullPointerException("Format cannot be null."); + } + final Object[] source = args == null ? new Object[0] : args.clone(); + return new Fn.StringFn(() -> { + Object[] values = new Object[source.length]; + for (int i = 0; i < values.length; i++) { + Object arg = source[i]; + values[i] = arg instanceof ObservableValue ? ((ObservableValue) arg).getValue() : arg; + } + return String.format(format, values); + }, observables(source)); + } + + // ---- custom bindings + + /// Returns a boolean binding computed by a function; a function that + /// throws or returns `null` yields `false`. + public static BooleanBinding createBooleanBinding(final Callable func, final Observable... dependencies) { + return new Fn.BooleanFn(() -> { + try { + Boolean value = func.call(); + if (value != null) { + return value.booleanValue(); + } + } catch (Exception failure) { + Diagnostics.report(failure); + } + return false; + }, dependencies); + } + + /// Returns a `double` binding computed by a function; a function that + /// throws or returns `null` yields zero. + public static DoubleBinding createDoubleBinding(final Callable func, final Observable... dependencies) { + return new Fn.DoubleFn(() -> { + try { + Double value = func.call(); + if (value != null) { + return value.doubleValue(); + } + } catch (Exception failure) { + Diagnostics.report(failure); + } + return 0.0; + }, dependencies); + } + + /// Returns a `float` binding computed by a function; a function that + /// throws or returns `null` yields zero. + public static FloatBinding createFloatBinding(final Callable func, final Observable... dependencies) { + return new Fn.FloatFn(() -> { + try { + Float value = func.call(); + if (value != null) { + return value.floatValue(); + } + } catch (Exception failure) { + Diagnostics.report(failure); + } + return 0.0f; + }, dependencies); + } + + /// Returns an `int` binding computed by a function; a function that + /// throws or returns `null` yields zero. + public static IntegerBinding createIntegerBinding(final Callable func, final Observable... dependencies) { + return new Fn.IntegerFn(() -> { + try { + Integer value = func.call(); + if (value != null) { + return value.intValue(); + } + } catch (Exception failure) { + Diagnostics.report(failure); + } + return 0; + }, dependencies); + } + + /// Returns a `long` binding computed by a function; a function that + /// throws or returns `null` yields zero. + public static LongBinding createLongBinding(final Callable func, final Observable... dependencies) { + return new Fn.LongFn(() -> { + try { + Long value = func.call(); + if (value != null) { + return value.longValue(); + } + } catch (Exception failure) { + Diagnostics.report(failure); + } + return 0L; + }, dependencies); + } + + /// Returns an object binding computed by a function; a function that + /// throws yields `null`. + public static ObjectBinding createObjectBinding(final Callable func, final Observable... dependencies) { + return new Fn.ObjectFn(() -> { + try { + return func.call(); + } catch (Exception failure) { + Diagnostics.report(failure); + return null; + } + }, dependencies); + } + + /// Returns a string binding computed by a function; a function that + /// throws yields the empty string. + public static StringBinding createStringBinding(final Callable func, final Observable... dependencies) { + return new Fn.StringFn(() -> { + try { + return func.call(); + } catch (Exception failure) { + Diagnostics.report(failure); + return ""; + } + }, dependencies); + } + + // ---- numbers + + /// Returns a binding computing the negation of a number. + public static NumberBinding negate(final ObservableNumberValue value) { + if (value == null) { + throw new NullPointerException("Operand cannot be null."); + } + return arith(NEG, value, Const.of(0)); + } + + /// Returns a binding computing the sum of two numbers, typed by the wider operand. + public static NumberBinding add(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return arith(ADD, op1, op2); + } + + /// Returns a binding computing the sum of a number and a constant. + public static DoubleBinding add(final ObservableNumberValue op1, final double op2) { + return (DoubleBinding) arith(ADD, op1, Const.of(op2)); + } + + /// Returns a binding computing the sum of a number and a constant. + public static DoubleBinding add(final double op1, final ObservableNumberValue op2) { + return (DoubleBinding) arith(ADD, Const.of(op1), op2); + } + + /// Returns a binding computing the sum of a number and a constant. + public static NumberBinding add(final ObservableNumberValue op1, final float op2) { + return arith(ADD, op1, Const.of(op2)); + } + + /// Returns a binding computing the sum of a number and a constant. + public static NumberBinding add(final float op1, final ObservableNumberValue op2) { + return arith(ADD, Const.of(op1), op2); + } + + /// Returns a binding computing the sum of a number and a constant. + public static NumberBinding add(final ObservableNumberValue op1, final long op2) { + return arith(ADD, op1, Const.of(op2)); + } + + /// Returns a binding computing the sum of a number and a constant. + public static NumberBinding add(final long op1, final ObservableNumberValue op2) { + return arith(ADD, Const.of(op1), op2); + } + + /// Returns a binding computing the sum of a number and a constant. + public static NumberBinding add(final ObservableNumberValue op1, final int op2) { + return arith(ADD, op1, Const.of(op2)); + } + + /// Returns a binding computing the sum of a number and a constant. + public static NumberBinding add(final int op1, final ObservableNumberValue op2) { + return arith(ADD, Const.of(op1), op2); + } + + /// Returns a binding computing the difference of two numbers, typed by the wider operand. + public static NumberBinding subtract(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return arith(SUB, op1, op2); + } + + /// Returns a binding computing the difference of a number and a constant. + public static DoubleBinding subtract(final ObservableNumberValue op1, final double op2) { + return (DoubleBinding) arith(SUB, op1, Const.of(op2)); + } + + /// Returns a binding computing the difference of a number and a constant. + public static DoubleBinding subtract(final double op1, final ObservableNumberValue op2) { + return (DoubleBinding) arith(SUB, Const.of(op1), op2); + } + + /// Returns a binding computing the difference of a number and a constant. + public static NumberBinding subtract(final ObservableNumberValue op1, final float op2) { + return arith(SUB, op1, Const.of(op2)); + } + + /// Returns a binding computing the difference of a number and a constant. + public static NumberBinding subtract(final float op1, final ObservableNumberValue op2) { + return arith(SUB, Const.of(op1), op2); + } + + /// Returns a binding computing the difference of a number and a constant. + public static NumberBinding subtract(final ObservableNumberValue op1, final long op2) { + return arith(SUB, op1, Const.of(op2)); + } + + /// Returns a binding computing the difference of a number and a constant. + public static NumberBinding subtract(final long op1, final ObservableNumberValue op2) { + return arith(SUB, Const.of(op1), op2); + } + + /// Returns a binding computing the difference of a number and a constant. + public static NumberBinding subtract(final ObservableNumberValue op1, final int op2) { + return arith(SUB, op1, Const.of(op2)); + } + + /// Returns a binding computing the difference of a number and a constant. + public static NumberBinding subtract(final int op1, final ObservableNumberValue op2) { + return arith(SUB, Const.of(op1), op2); + } + + /// Returns a binding computing the product of two numbers, typed by the wider operand. + public static NumberBinding multiply(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return arith(MUL, op1, op2); + } + + /// Returns a binding computing the product of a number and a constant. + public static DoubleBinding multiply(final ObservableNumberValue op1, final double op2) { + return (DoubleBinding) arith(MUL, op1, Const.of(op2)); + } + + /// Returns a binding computing the product of a number and a constant. + public static DoubleBinding multiply(final double op1, final ObservableNumberValue op2) { + return (DoubleBinding) arith(MUL, Const.of(op1), op2); + } + + /// Returns a binding computing the product of a number and a constant. + public static NumberBinding multiply(final ObservableNumberValue op1, final float op2) { + return arith(MUL, op1, Const.of(op2)); + } + + /// Returns a binding computing the product of a number and a constant. + public static NumberBinding multiply(final float op1, final ObservableNumberValue op2) { + return arith(MUL, Const.of(op1), op2); + } + + /// Returns a binding computing the product of a number and a constant. + public static NumberBinding multiply(final ObservableNumberValue op1, final long op2) { + return arith(MUL, op1, Const.of(op2)); + } + + /// Returns a binding computing the product of a number and a constant. + public static NumberBinding multiply(final long op1, final ObservableNumberValue op2) { + return arith(MUL, Const.of(op1), op2); + } + + /// Returns a binding computing the product of a number and a constant. + public static NumberBinding multiply(final ObservableNumberValue op1, final int op2) { + return arith(MUL, op1, Const.of(op2)); + } + + /// Returns a binding computing the product of a number and a constant. + public static NumberBinding multiply(final int op1, final ObservableNumberValue op2) { + return arith(MUL, Const.of(op1), op2); + } + + /// Returns a binding computing the quotient of two numbers, typed by the wider operand. + public static NumberBinding divide(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return arith(DIV, op1, op2); + } + + /// Returns a binding computing the quotient of a number and a constant. + public static DoubleBinding divide(final ObservableNumberValue op1, final double op2) { + return (DoubleBinding) arith(DIV, op1, Const.of(op2)); + } + + /// Returns a binding computing the quotient of a number and a constant. + public static DoubleBinding divide(final double op1, final ObservableNumberValue op2) { + return (DoubleBinding) arith(DIV, Const.of(op1), op2); + } + + /// Returns a binding computing the quotient of a number and a constant. + public static NumberBinding divide(final ObservableNumberValue op1, final float op2) { + return arith(DIV, op1, Const.of(op2)); + } + + /// Returns a binding computing the quotient of a number and a constant. + public static NumberBinding divide(final float op1, final ObservableNumberValue op2) { + return arith(DIV, Const.of(op1), op2); + } + + /// Returns a binding computing the quotient of a number and a constant. + public static NumberBinding divide(final ObservableNumberValue op1, final long op2) { + return arith(DIV, op1, Const.of(op2)); + } + + /// Returns a binding computing the quotient of a number and a constant. + public static NumberBinding divide(final long op1, final ObservableNumberValue op2) { + return arith(DIV, Const.of(op1), op2); + } + + /// Returns a binding computing the quotient of a number and a constant. + public static NumberBinding divide(final ObservableNumberValue op1, final int op2) { + return arith(DIV, op1, Const.of(op2)); + } + + /// Returns a binding computing the quotient of a number and a constant. + public static NumberBinding divide(final int op1, final ObservableNumberValue op2) { + return arith(DIV, Const.of(op1), op2); + } + + /// Returns a binding computing the smaller of two numbers, typed by the wider operand. + public static NumberBinding min(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return arith(MIN, op1, op2); + } + + /// Returns a binding computing the smaller of a number and a constant. + public static DoubleBinding min(final ObservableNumberValue op1, final double op2) { + return (DoubleBinding) arith(MIN, op1, Const.of(op2)); + } + + /// Returns a binding computing the smaller of a number and a constant. + public static DoubleBinding min(final double op1, final ObservableNumberValue op2) { + return (DoubleBinding) arith(MIN, Const.of(op1), op2); + } + + /// Returns a binding computing the smaller of a number and a constant. + public static NumberBinding min(final ObservableNumberValue op1, final float op2) { + return arith(MIN, op1, Const.of(op2)); + } + + /// Returns a binding computing the smaller of a number and a constant. + public static NumberBinding min(final float op1, final ObservableNumberValue op2) { + return arith(MIN, Const.of(op1), op2); + } + + /// Returns a binding computing the smaller of a number and a constant. + public static NumberBinding min(final ObservableNumberValue op1, final long op2) { + return arith(MIN, op1, Const.of(op2)); + } + + /// Returns a binding computing the smaller of a number and a constant. + public static NumberBinding min(final long op1, final ObservableNumberValue op2) { + return arith(MIN, Const.of(op1), op2); + } + + /// Returns a binding computing the smaller of a number and a constant. + public static NumberBinding min(final ObservableNumberValue op1, final int op2) { + return arith(MIN, op1, Const.of(op2)); + } + + /// Returns a binding computing the smaller of a number and a constant. + public static NumberBinding min(final int op1, final ObservableNumberValue op2) { + return arith(MIN, Const.of(op1), op2); + } + + /// Returns a binding computing the larger of two numbers, typed by the wider operand. + public static NumberBinding max(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return arith(MAX, op1, op2); + } + + /// Returns a binding computing the larger of a number and a constant. + public static DoubleBinding max(final ObservableNumberValue op1, final double op2) { + return (DoubleBinding) arith(MAX, op1, Const.of(op2)); + } + + /// Returns a binding computing the larger of a number and a constant. + public static DoubleBinding max(final double op1, final ObservableNumberValue op2) { + return (DoubleBinding) arith(MAX, Const.of(op1), op2); + } + + /// Returns a binding computing the larger of a number and a constant. + public static NumberBinding max(final ObservableNumberValue op1, final float op2) { + return arith(MAX, op1, Const.of(op2)); + } + + /// Returns a binding computing the larger of a number and a constant. + public static NumberBinding max(final float op1, final ObservableNumberValue op2) { + return arith(MAX, Const.of(op1), op2); + } + + /// Returns a binding computing the larger of a number and a constant. + public static NumberBinding max(final ObservableNumberValue op1, final long op2) { + return arith(MAX, op1, Const.of(op2)); + } + + /// Returns a binding computing the larger of a number and a constant. + public static NumberBinding max(final long op1, final ObservableNumberValue op2) { + return arith(MAX, Const.of(op1), op2); + } + + /// Returns a binding computing the larger of a number and a constant. + public static NumberBinding max(final ObservableNumberValue op1, final int op2) { + return arith(MAX, op1, Const.of(op2)); + } + + /// Returns a binding computing the larger of a number and a constant. + public static NumberBinding max(final int op1, final ObservableNumberValue op2) { + return arith(MAX, Const.of(op1), op2); + } + + /// Returns a binding telling whether two numbers are equal, within a tolerance. + public static BooleanBinding equal(final ObservableNumberValue op1, final ObservableNumberValue op2, final double epsilon) { + return compare(EQ, op1, op2, epsilon); + } + + /// Returns a binding telling whether two numbers are equal. + public static BooleanBinding equal(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return compare(EQ, op1, op2, 0.0); + } + + /// Returns a binding telling whether a number and a constant are equal, within a tolerance. + public static BooleanBinding equal(final ObservableNumberValue op1, final double op2, final double epsilon) { + return compare(EQ, op1, Const.of(op2), epsilon); + } + + /// Returns a binding telling whether a number and a constant are equal, within a tolerance. + public static BooleanBinding equal(final double op1, final ObservableNumberValue op2, final double epsilon) { + return compare(EQ, Const.of(op1), op2, epsilon); + } + + /// Returns a binding telling whether a number and a constant are equal, within a tolerance. + public static BooleanBinding equal(final ObservableNumberValue op1, final float op2, final double epsilon) { + return compare(EQ, op1, Const.of(op2), epsilon); + } + + /// Returns a binding telling whether a number and a constant are equal, within a tolerance. + public static BooleanBinding equal(final float op1, final ObservableNumberValue op2, final double epsilon) { + return compare(EQ, Const.of(op1), op2, epsilon); + } + + /// Returns a binding telling whether a number and a constant are equal, within a tolerance. + public static BooleanBinding equal(final ObservableNumberValue op1, final long op2, final double epsilon) { + return compare(EQ, op1, Const.of(op2), epsilon); + } + + /// Returns a binding telling whether a number and a constant are equal, within a tolerance. + public static BooleanBinding equal(final long op1, final ObservableNumberValue op2, final double epsilon) { + return compare(EQ, Const.of(op1), op2, epsilon); + } + + /// Returns a binding telling whether a number and a constant are equal. + public static BooleanBinding equal(final ObservableNumberValue op1, final long op2) { + return compare(EQ, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether a number and a constant are equal. + public static BooleanBinding equal(final long op1, final ObservableNumberValue op2) { + return compare(EQ, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether a number and a constant are equal, within a tolerance. + public static BooleanBinding equal(final ObservableNumberValue op1, final int op2, final double epsilon) { + return compare(EQ, op1, Const.of(op2), epsilon); + } + + /// Returns a binding telling whether a number and a constant are equal, within a tolerance. + public static BooleanBinding equal(final int op1, final ObservableNumberValue op2, final double epsilon) { + return compare(EQ, Const.of(op1), op2, epsilon); + } + + /// Returns a binding telling whether a number and a constant are equal. + public static BooleanBinding equal(final ObservableNumberValue op1, final int op2) { + return compare(EQ, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether a number and a constant are equal. + public static BooleanBinding equal(final int op1, final ObservableNumberValue op2) { + return compare(EQ, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether two numbers are not equal, within a tolerance. + public static BooleanBinding notEqual(final ObservableNumberValue op1, final ObservableNumberValue op2, final double epsilon) { + return compare(NE, op1, op2, epsilon); + } + + /// Returns a binding telling whether two numbers are not equal. + public static BooleanBinding notEqual(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return compare(NE, op1, op2, 0.0); + } + + /// Returns a binding telling whether a number and a constant are not equal, within a tolerance. + public static BooleanBinding notEqual(final ObservableNumberValue op1, final double op2, final double epsilon) { + return compare(NE, op1, Const.of(op2), epsilon); + } + + /// Returns a binding telling whether a number and a constant are not equal, within a tolerance. + public static BooleanBinding notEqual(final double op1, final ObservableNumberValue op2, final double epsilon) { + return compare(NE, Const.of(op1), op2, epsilon); + } + + /// Returns a binding telling whether a number and a constant are not equal, within a tolerance. + public static BooleanBinding notEqual(final ObservableNumberValue op1, final float op2, final double epsilon) { + return compare(NE, op1, Const.of(op2), epsilon); + } + + /// Returns a binding telling whether a number and a constant are not equal, within a tolerance. + public static BooleanBinding notEqual(final float op1, final ObservableNumberValue op2, final double epsilon) { + return compare(NE, Const.of(op1), op2, epsilon); + } + + /// Returns a binding telling whether a number and a constant are not equal, within a tolerance. + public static BooleanBinding notEqual(final ObservableNumberValue op1, final long op2, final double epsilon) { + return compare(NE, op1, Const.of(op2), epsilon); + } + + /// Returns a binding telling whether a number and a constant are not equal, within a tolerance. + public static BooleanBinding notEqual(final long op1, final ObservableNumberValue op2, final double epsilon) { + return compare(NE, Const.of(op1), op2, epsilon); + } + + /// Returns a binding telling whether a number and a constant are not equal. + public static BooleanBinding notEqual(final ObservableNumberValue op1, final long op2) { + return compare(NE, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether a number and a constant are not equal. + public static BooleanBinding notEqual(final long op1, final ObservableNumberValue op2) { + return compare(NE, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether a number and a constant are not equal, within a tolerance. + public static BooleanBinding notEqual(final ObservableNumberValue op1, final int op2, final double epsilon) { + return compare(NE, op1, Const.of(op2), epsilon); + } + + /// Returns a binding telling whether a number and a constant are not equal, within a tolerance. + public static BooleanBinding notEqual(final int op1, final ObservableNumberValue op2, final double epsilon) { + return compare(NE, Const.of(op1), op2, epsilon); + } + + /// Returns a binding telling whether a number and a constant are not equal. + public static BooleanBinding notEqual(final ObservableNumberValue op1, final int op2) { + return compare(NE, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether a number and a constant are not equal. + public static BooleanBinding notEqual(final int op1, final ObservableNumberValue op2) { + return compare(NE, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first number is greater than the second. + public static BooleanBinding greaterThan(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return compare(GT, op1, op2, 0.0); + } + + /// Returns a binding telling whether the first operand is greater than the second. + public static BooleanBinding greaterThan(final ObservableNumberValue op1, final double op2) { + return compare(GT, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is greater than the second. + public static BooleanBinding greaterThan(final double op1, final ObservableNumberValue op2) { + return compare(GT, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is greater than the second. + public static BooleanBinding greaterThan(final ObservableNumberValue op1, final float op2) { + return compare(GT, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is greater than the second. + public static BooleanBinding greaterThan(final float op1, final ObservableNumberValue op2) { + return compare(GT, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is greater than the second. + public static BooleanBinding greaterThan(final ObservableNumberValue op1, final long op2) { + return compare(GT, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is greater than the second. + public static BooleanBinding greaterThan(final long op1, final ObservableNumberValue op2) { + return compare(GT, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is greater than the second. + public static BooleanBinding greaterThan(final ObservableNumberValue op1, final int op2) { + return compare(GT, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is greater than the second. + public static BooleanBinding greaterThan(final int op1, final ObservableNumberValue op2) { + return compare(GT, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first number is less than the second. + public static BooleanBinding lessThan(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return compare(LT, op1, op2, 0.0); + } + + /// Returns a binding telling whether the first operand is less than the second. + public static BooleanBinding lessThan(final ObservableNumberValue op1, final double op2) { + return compare(LT, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is less than the second. + public static BooleanBinding lessThan(final double op1, final ObservableNumberValue op2) { + return compare(LT, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is less than the second. + public static BooleanBinding lessThan(final ObservableNumberValue op1, final float op2) { + return compare(LT, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is less than the second. + public static BooleanBinding lessThan(final float op1, final ObservableNumberValue op2) { + return compare(LT, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is less than the second. + public static BooleanBinding lessThan(final ObservableNumberValue op1, final long op2) { + return compare(LT, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is less than the second. + public static BooleanBinding lessThan(final long op1, final ObservableNumberValue op2) { + return compare(LT, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is less than the second. + public static BooleanBinding lessThan(final ObservableNumberValue op1, final int op2) { + return compare(LT, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is less than the second. + public static BooleanBinding lessThan(final int op1, final ObservableNumberValue op2) { + return compare(LT, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first number is greater than or equal to the second. + public static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return compare(GE, op1, op2, 0.0); + } + + /// Returns a binding telling whether the first operand is greater than or equal to the second. + public static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final double op2) { + return compare(GE, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is greater than or equal to the second. + public static BooleanBinding greaterThanOrEqual(final double op1, final ObservableNumberValue op2) { + return compare(GE, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is greater than or equal to the second. + public static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final float op2) { + return compare(GE, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is greater than or equal to the second. + public static BooleanBinding greaterThanOrEqual(final float op1, final ObservableNumberValue op2) { + return compare(GE, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is greater than or equal to the second. + public static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final long op2) { + return compare(GE, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is greater than or equal to the second. + public static BooleanBinding greaterThanOrEqual(final long op1, final ObservableNumberValue op2) { + return compare(GE, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is greater than or equal to the second. + public static BooleanBinding greaterThanOrEqual(final ObservableNumberValue op1, final int op2) { + return compare(GE, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is greater than or equal to the second. + public static BooleanBinding greaterThanOrEqual(final int op1, final ObservableNumberValue op2) { + return compare(GE, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first number is less than or equal to the second. + public static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final ObservableNumberValue op2) { + return compare(LE, op1, op2, 0.0); + } + + /// Returns a binding telling whether the first operand is less than or equal to the second. + public static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final double op2) { + return compare(LE, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is less than or equal to the second. + public static BooleanBinding lessThanOrEqual(final double op1, final ObservableNumberValue op2) { + return compare(LE, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is less than or equal to the second. + public static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final float op2) { + return compare(LE, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is less than or equal to the second. + public static BooleanBinding lessThanOrEqual(final float op1, final ObservableNumberValue op2) { + return compare(LE, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is less than or equal to the second. + public static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final long op2) { + return compare(LE, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is less than or equal to the second. + public static BooleanBinding lessThanOrEqual(final long op1, final ObservableNumberValue op2) { + return compare(LE, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether the first operand is less than or equal to the second. + public static BooleanBinding lessThanOrEqual(final ObservableNumberValue op1, final int op2) { + return compare(LE, op1, Const.of(op2), 0.0); + } + + /// Returns a binding telling whether the first operand is less than or equal to the second. + public static BooleanBinding lessThanOrEqual(final int op1, final ObservableNumberValue op2) { + return compare(LE, Const.of(op1), op2, 0.0); + } + + /// Returns a binding telling whether two strings are equal; `null` counts as empty. + public static BooleanBinding equal(final ObservableStringValue op1, final ObservableStringValue op2) { + return strings(EQ, op1, op2); + } + + /// Returns a binding telling whether two strings are equal; `null` counts as empty. + public static BooleanBinding equal(final ObservableStringValue op1, final String op2) { + return strings(EQ, op1, new Const.Text(op2)); + } + + /// Returns a binding telling whether two strings are equal; `null` counts as empty. + public static BooleanBinding equal(final String op1, final ObservableStringValue op2) { + return strings(EQ, new Const.Text(op1), op2); + } + + /// Returns a binding telling whether two strings differ; `null` counts as empty. + public static BooleanBinding notEqual(final ObservableStringValue op1, final ObservableStringValue op2) { + return strings(NE, op1, op2); + } + + /// Returns a binding telling whether two strings differ; `null` counts as empty. + public static BooleanBinding notEqual(final ObservableStringValue op1, final String op2) { + return strings(NE, op1, new Const.Text(op2)); + } + + /// Returns a binding telling whether two strings differ; `null` counts as empty. + public static BooleanBinding notEqual(final String op1, final ObservableStringValue op2) { + return strings(NE, new Const.Text(op1), op2); + } + + /// Returns a binding telling whether two strings are equal ignoring case; `null` counts as empty. + public static BooleanBinding equalIgnoreCase(final ObservableStringValue op1, final ObservableStringValue op2) { + return strings(EQ_IC, op1, op2); + } + + /// Returns a binding telling whether two strings are equal ignoring case; `null` counts as empty. + public static BooleanBinding equalIgnoreCase(final ObservableStringValue op1, final String op2) { + return strings(EQ_IC, op1, new Const.Text(op2)); + } + + /// Returns a binding telling whether two strings are equal ignoring case; `null` counts as empty. + public static BooleanBinding equalIgnoreCase(final String op1, final ObservableStringValue op2) { + return strings(EQ_IC, new Const.Text(op1), op2); + } + + /// Returns a binding telling whether two strings differ even ignoring case; `null` counts as empty. + public static BooleanBinding notEqualIgnoreCase(final ObservableStringValue op1, final ObservableStringValue op2) { + return strings(NE_IC, op1, op2); + } + + /// Returns a binding telling whether two strings differ even ignoring case; `null` counts as empty. + public static BooleanBinding notEqualIgnoreCase(final ObservableStringValue op1, final String op2) { + return strings(NE_IC, op1, new Const.Text(op2)); + } + + /// Returns a binding telling whether two strings differ even ignoring case; `null` counts as empty. + public static BooleanBinding notEqualIgnoreCase(final String op1, final ObservableStringValue op2) { + return strings(NE_IC, new Const.Text(op1), op2); + } + + /// Returns a binding telling whether two strings are in descending order; `null` counts as empty. + public static BooleanBinding greaterThan(final ObservableStringValue op1, final ObservableStringValue op2) { + return strings(GT, op1, op2); + } + + /// Returns a binding telling whether two strings are in descending order; `null` counts as empty. + public static BooleanBinding greaterThan(final ObservableStringValue op1, final String op2) { + return strings(GT, op1, new Const.Text(op2)); + } + + /// Returns a binding telling whether two strings are in descending order; `null` counts as empty. + public static BooleanBinding greaterThan(final String op1, final ObservableStringValue op2) { + return strings(GT, new Const.Text(op1), op2); + } + + /// Returns a binding telling whether two strings are in ascending order; `null` counts as empty. + public static BooleanBinding lessThan(final ObservableStringValue op1, final ObservableStringValue op2) { + return strings(LT, op1, op2); + } + + /// Returns a binding telling whether two strings are in ascending order; `null` counts as empty. + public static BooleanBinding lessThan(final ObservableStringValue op1, final String op2) { + return strings(LT, op1, new Const.Text(op2)); + } + + /// Returns a binding telling whether two strings are in ascending order; `null` counts as empty. + public static BooleanBinding lessThan(final String op1, final ObservableStringValue op2) { + return strings(LT, new Const.Text(op1), op2); + } + + /// Returns a binding telling whether two strings are equal or in descending order; `null` counts as empty. + public static BooleanBinding greaterThanOrEqual(final ObservableStringValue op1, final ObservableStringValue op2) { + return strings(GE, op1, op2); + } + + /// Returns a binding telling whether two strings are equal or in descending order; `null` counts as empty. + public static BooleanBinding greaterThanOrEqual(final ObservableStringValue op1, final String op2) { + return strings(GE, op1, new Const.Text(op2)); + } + + /// Returns a binding telling whether two strings are equal or in descending order; `null` counts as empty. + public static BooleanBinding greaterThanOrEqual(final String op1, final ObservableStringValue op2) { + return strings(GE, new Const.Text(op1), op2); + } + + /// Returns a binding telling whether two strings are equal or in ascending order; `null` counts as empty. + public static BooleanBinding lessThanOrEqual(final ObservableStringValue op1, final ObservableStringValue op2) { + return strings(LE, op1, op2); + } + + /// Returns a binding telling whether two strings are equal or in ascending order; `null` counts as empty. + public static BooleanBinding lessThanOrEqual(final ObservableStringValue op1, final String op2) { + return strings(LE, op1, new Const.Text(op2)); + } + + /// Returns a binding telling whether two strings are equal or in ascending order; `null` counts as empty. + public static BooleanBinding lessThanOrEqual(final String op1, final ObservableStringValue op2) { + return strings(LE, new Const.Text(op1), op2); + } + + // ---- booleans + + /// Returns a binding that is true while both operands are. + public static BooleanBinding and(final ObservableBooleanValue op1, final ObservableBooleanValue op2) { + operands(op1, op2); + return new Fn.BooleanFn(() -> op1.get() && op2.get(), op1, op2); + } + + /// Returns a binding that is true while either operand is. + public static BooleanBinding or(final ObservableBooleanValue op1, final ObservableBooleanValue op2) { + operands(op1, op2); + return new Fn.BooleanFn(() -> op1.get() || op2.get(), op1, op2); + } + + /// Returns a binding holding the opposite of a boolean. + public static BooleanBinding not(final ObservableBooleanValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null."); + } + return new Fn.BooleanFn(() -> !op.get(), op); + } + + /// Returns a binding telling whether two booleans are equal. + public static BooleanBinding equal(final ObservableBooleanValue op1, final ObservableBooleanValue op2) { + operands(op1, op2); + return new Fn.BooleanFn(() -> op1.get() == op2.get(), op1, op2); + } + + /// Returns a binding telling whether two booleans differ. + public static BooleanBinding notEqual(final ObservableBooleanValue op1, final ObservableBooleanValue op2) { + operands(op1, op2); + return new Fn.BooleanFn(() -> op1.get() != op2.get(), op1, op2); + } + + // ---- objects + + private static boolean same(Object x, Object y) { + return x == null ? y == null : x.equals(y); + } + + /// Returns a binding telling whether two objects are equal. + public static BooleanBinding equal(final ObservableObjectValue op1, final ObservableObjectValue op2) { + operands(op1, op2); + return new Fn.BooleanFn(() -> same(op1.get(), op2.get()), op1, op2); + } + + /// Returns a binding telling whether an object equals a constant. + public static BooleanBinding equal(final ObservableObjectValue op1, Object op2) { + if (op1 == null) { + throw new NullPointerException("Operands cannot be null."); + } + return new Fn.BooleanFn(() -> same(op1.get(), op2), op1); + } + + /// Returns a binding telling whether a constant equals an object. + public static BooleanBinding equal(Object op1, final ObservableObjectValue op2) { + if (op2 == null) { + throw new NullPointerException("Operands cannot be null."); + } + return new Fn.BooleanFn(() -> same(op1, op2.get()), op2); + } + + /// Returns a binding telling whether two objects differ. + public static BooleanBinding notEqual(final ObservableObjectValue op1, final ObservableObjectValue op2) { + operands(op1, op2); + return new Fn.BooleanFn(() -> !same(op1.get(), op2.get()), op1, op2); + } + + /// Returns a binding telling whether an object differs from a constant. + public static BooleanBinding notEqual(final ObservableObjectValue op1, Object op2) { + if (op1 == null) { + throw new NullPointerException("Operands cannot be null."); + } + return new Fn.BooleanFn(() -> !same(op1.get(), op2), op1); + } + + /// Returns a binding telling whether a constant differs from an object. + public static BooleanBinding notEqual(Object op1, final ObservableObjectValue op2) { + if (op2 == null) { + throw new NullPointerException("Operands cannot be null."); + } + return new Fn.BooleanFn(() -> !same(op1, op2.get()), op2); + } + + /// Returns a binding telling whether a value is `null`. + public static BooleanBinding isNull(final ObservableObjectValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null."); + } + return new Fn.BooleanFn(() -> op.get() == null, op); + } + + /// Returns a binding telling whether a value is not `null`. + public static BooleanBinding isNotNull(final ObservableObjectValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null."); + } + return new Fn.BooleanFn(() -> op.get() != null, op); + } + + // ---- strings + + /// Returns a binding holding the length of a string; zero for `null`. + public static IntegerBinding length(final ObservableStringValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null"); + } + return new Fn.IntegerFn(() -> { + String value = op.get(); + return value == null ? 0 : value.length(); + }, op); + } + + /// Returns a binding telling whether a string is `null` or empty. + public static BooleanBinding isEmpty(final ObservableStringValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null"); + } + return new Fn.BooleanFn(() -> { + String value = op.get(); + return value == null || value.length() == 0; + }, op); + } + + /// Returns a binding telling whether a string has any character. + public static BooleanBinding isNotEmpty(final ObservableStringValue op) { + if (op == null) { + throw new NullPointerException("Operand cannot be null"); + } + return new Fn.BooleanFn(() -> { + String value = op.get(); + return value != null && value.length() > 0; + }, op); + } + + /// Returns an expression holding the text of every argument in a row. + /// An argument that is an observable value contributes its current + /// value and is observed. + public static StringExpression concat(Object... args) { + final Object[] source = args == null ? new Object[0] : args.clone(); + return new Fn.StringFn(() -> { + StringBuilder result = new StringBuilder(); + for (Object arg : source) { + result.append(arg instanceof ObservableValue ? ((ObservableValue) arg).getValue() : arg); + } + return result.toString(); + }, observables(source)); + } + + /// Returns any observable value as a string expression: the value itself + /// when it already is one, otherwise a binding holding its text. + public static StringExpression convert(ObservableValue observableValue) { + if (observableValue instanceof StringExpression) { + return (StringExpression) observableValue; + } + return text(observableValue); + } + + /// Returns an expression holding the arguments formatted with a + /// `String.format` pattern. An argument that is an observable value + /// contributes its current value and is observed. + public static StringExpression format(String format, Object... args) { + return formatted(format, args); + } + + // ---- collections + + /// Returns a binding holding the size of a list. + public static IntegerBinding size(final ObservableList op) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + return new Fn.IntegerFn(() -> op.size(), op); + } + + /// Returns a binding telling whether a list is empty. + public static BooleanBinding isEmpty(final ObservableList op) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + return new Fn.BooleanFn(() -> op.isEmpty(), op); + } + + /// Returns a binding telling whether a list has any element. + public static BooleanBinding isNotEmpty(final ObservableList op) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + return new Fn.BooleanFn(() -> !op.isEmpty(), op); + } + + /// Returns a binding holding the element at a position of a list, or + /// `null` while the list is shorter than that. + public static ObjectBinding valueAt(final ObservableList op, final int index) { + if (op == null) { + throw new NullPointerException("List cannot be null."); + } + if (index < 0) { + throw new IllegalArgumentException("Index cannot be negative"); + } + return new Fn.ObjectFn(() -> index < op.size() ? op.get(index) : null, op); + } + + /// Returns a binding holding the element at an observed position of a + /// list, or `null` while the position is outside the list. + public static ObjectBinding valueAt(final ObservableList op, final ObservableIntegerValue index) { + return valueAt(op, (ObservableNumberValue) index); + } + + /// Returns a binding holding the element at an observed position of a + /// list, or `null` while the position is outside the list. + public static ObjectBinding valueAt(final ObservableList op, final ObservableNumberValue index) { + if (op == null || index == null) { + throw new NullPointerException("Operands cannot be null."); + } + return new Fn.ObjectFn(() -> { + int at = index.intValue(); + return at >= 0 && at < op.size() ? op.get(at) : null; + }, op, index); + } + + /// Returns a binding holding the size of a set. + public static IntegerBinding size(final ObservableSet op) { + if (op == null) { + throw new NullPointerException("Set cannot be null."); + } + return new Fn.IntegerFn(() -> op.size(), op); + } + + /// Returns a binding telling whether a set is empty. + public static BooleanBinding isEmpty(final ObservableSet op) { + if (op == null) { + throw new NullPointerException("Set cannot be null."); + } + return new Fn.BooleanFn(() -> op.isEmpty(), op); + } + + /// Returns a binding telling whether a set has any element. + public static BooleanBinding isNotEmpty(final ObservableSet op) { + if (op == null) { + throw new NullPointerException("Set cannot be null."); + } + return new Fn.BooleanFn(() -> !op.isEmpty(), op); + } + + /// Returns a binding holding the size of a map. + public static IntegerBinding size(final ObservableMap op) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + return new Fn.IntegerFn(() -> op.size(), op); + } + + /// Returns a binding telling whether a map is empty. + public static BooleanBinding isEmpty(final ObservableMap op) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + return new Fn.BooleanFn(() -> op.isEmpty(), op); + } + + /// Returns a binding telling whether a map has any entry. + public static BooleanBinding isNotEmpty(final ObservableMap op) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + return new Fn.BooleanFn(() -> !op.isEmpty(), op); + } + + /// Returns a binding holding the value a map has for a key. + public static ObjectBinding valueAt(final ObservableMap op, final K key) { + if (op == null) { + throw new NullPointerException("Map cannot be null."); + } + return new Fn.ObjectFn(() -> op.get(key), op); + } + + /// Returns a binding holding the value a map has for an observed key. + public static ObjectBinding valueAt(final ObservableMap op, + final ObservableValue key) { + if (op == null || key == null) { + throw new NullPointerException("Operands cannot be null."); + } + return new Fn.ObjectFn(() -> op.get(key.getValue()), op, key); + } + + // ---- ternary + + /// Starts a ternary binding: `when(condition).then(a).otherwise(b)`. + public static When when(final ObservableBooleanValue condition) { + return new When(condition); + } + + // ---- bidirectional bindings + + /// Keeps two properties equal, starting with the value of the second. + /// Neither property keeps the other alive. + public static void bindBidirectional(Property property1, Property property2) { + BidirectionalBinding.bind(property1, property2); + } + + /// Removes a bidirectional binding between two properties. + public static void unbindBidirectional(Property property1, Property property2) { + BidirectionalBinding.unbind(property1, property2); + } + + /// Removes a bidirectional binding between two objects, whatever kind + /// it was. + public static void unbindBidirectional(Object property1, Object property2) { + BidirectionalBinding.unbind(property1, property2); + } + + /// Keeps a string property and a property of another type in sync + /// through a converter, starting with the text of the second. + public static void bindBidirectional(Property stringProperty, Property otherProperty, + final StringConverter converter) { + if (converter == null) { + throw new NullPointerException("Converter cannot be null"); + } + BidirectionalBinding.bind(stringProperty, otherProperty, value -> converter.toString(value), + text -> converter.fromString(text)); + } + + // ---- content bindings + + /// Makes a list mirror an observable list: it takes the content now and + /// follows every later change. The list is held weakly. + public static void bindContent(List list1, ObservableList list2) { + ContentBindings.bind(list1, list2); + } + + /// Makes a set mirror an observable set, held weakly. + public static void bindContent(Set set1, ObservableSet set2) { + ContentBindings.bind(set1, set2); + } + + /// Makes a map mirror an observable map, held weakly. + public static void bindContent(Map map1, ObservableMap map2) { + ContentBindings.bind(map1, map2); + } + + /// Removes a content binding of the first object to the second. + public static void unbindContent(Object obj1, Object obj2) { + ContentBindings.unbind(obj1, obj2); + } + + /// Keeps two observable lists equal, starting with the content of the + /// second. Neither list keeps the other alive. + public static void bindContentBidirectional(ObservableList list1, ObservableList list2) { + ContentBindings.bindBidirectional(list1, list2); + } + + /// Removes a bidirectional content binding between two objects. + public static void unbindContentBidirectional(Object obj1, Object obj2) { + ContentBindings.unbindBidirectional(obj1, obj2); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/BooleanBinding.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/BooleanBinding.java new file mode 100644 index 00000000000..2332cb3ed71 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/BooleanBinding.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import com.codename1.fxcompat.runtime.BindingObserver; +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/// Base class for a lazily evaluated binding that computes a boolean. +/// +/// A subclass registers what it depends on with [#bind(Observable...)] and +/// implements [#computeValue()]. The value is computed on the first read after +/// an invalidation and cached until a dependency changes again; listeners are +/// told about an invalidation once, until the value is read. +public abstract class BooleanBinding extends BooleanExpression implements Binding { + + private boolean value; + private boolean valid; + private BindingObserver observer; + private ValueListeners helper; + + /// Creates an invalid binding with no dependencies. + public BooleanBinding() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Starts observing the dependencies; each one invalidates this binding. + protected final void bind(Observable... dependencies) { + if (dependencies != null && dependencies.length > 0) { + if (observer == null) { + observer = new BindingObserver(this); + } + for (Observable dependency : dependencies) { + dependency.addListener(observer); + } + } + } + + /// Stops observing the dependencies. + protected final void unbind(Observable... dependencies) { + if (observer != null && dependencies != null) { + for (Observable dependency : dependencies) { + dependency.removeListener(observer); + } + } + } + + /// Releases the dependencies. The default implementation does nothing. + @Override + public void dispose() { + } + + /// Returns the dependencies; the default implementation reports none. + @Override + public ObservableList getDependencies() { + return FXCollections.emptyObservableList(); + } + + /// Returns the value, computing it when the binding is invalid. + @Override + public final boolean get() { + if (!valid) { + value = computeValue(); + valid = true; + } + return value; + } + + /// Called when the binding turns invalid, before listeners are told. + protected void onInvalidating() { + } + + @Override + public final void invalidate() { + if (valid) { + valid = false; + onInvalidating(); + ValueListeners.fire(helper); + } + } + + @Override + public final boolean isValid() { + return valid; + } + + /// Computes the current value from the dependencies. + protected abstract boolean computeValue(); + + @Override + public String toString() { + return valid ? "BooleanBinding [value: " + get() + "]" : "BooleanBinding [invalid]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/BooleanExpression.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/BooleanExpression.java new file mode 100644 index 00000000000..0db52da9975 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/BooleanExpression.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableValue; + +/// The fluent operations of an observable boolean. +public abstract class BooleanExpression implements ObservableBooleanValue { + + /// Creates the expression. + public BooleanExpression() { + } + + @Override + public Boolean getValue() { + return Boolean.valueOf(get()); + } + + /// Returns the value as an expression: the value itself when it already + /// is one, otherwise a binding that follows it. + public static BooleanExpression booleanExpression(final ObservableBooleanValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof BooleanExpression) { + return (BooleanExpression) value; + } + return new Fn.BooleanFn(() -> value.get(), value); + } + + /// Returns an expression following a boxed boolean; `null` reads as + /// `false`. + public static BooleanExpression booleanExpression(final ObservableValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof BooleanExpression) { + return (BooleanExpression) value; + } + return new Fn.BooleanFn(() -> { + Boolean current = value.getValue(); + return current != null && current.booleanValue(); + }, value); + } + + /// Returns a binding that is true while this value and the other are. + public BooleanBinding and(final ObservableBooleanValue other) { + return Bindings.and(this, other); + } + + /// Returns a binding that is true while this value or the other is. + public BooleanBinding or(final ObservableBooleanValue other) { + return Bindings.or(this, other); + } + + /// Returns a binding holding the opposite of this value. + public BooleanBinding not() { + return Bindings.not(this); + } + + /// Returns a binding telling whether this value equals another. + public BooleanBinding isEqualTo(final ObservableBooleanValue other) { + return Bindings.equal(this, other); + } + + /// Returns a binding telling whether this value differs from another. + public BooleanBinding isNotEqualTo(final ObservableBooleanValue other) { + return Bindings.notEqual(this, other); + } + + /// Returns a binding holding this value as `"true"` or `"false"`. + public StringBinding asString() { + return Bindings.text(this); + } + + /// Returns an object expression holding the boxed value. + public ObjectExpression asObject() { + return new Fn.ObjectFn(() -> getValue(), this); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/Const.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/Const.java new file mode 100644 index 00000000000..d5591e23cbc --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/Const.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableDoubleValue; +import javafx.beans.value.ObservableFloatValue; +import javafx.beans.value.ObservableIntegerValue; +import javafx.beans.value.ObservableLongValue; +import javafx.beans.value.ObservableNumberValue; +import javafx.beans.value.ObservableStringValue; + +/// A constant dressed as an observable number, so an operator taking a +/// constant is the same code as the one taking two observables. It never +/// changes and therefore keeps no listeners. +abstract class Const implements ObservableNumberValue { + + private final Number value; + + Const(Number value) { + this.value = value; + } + + static Const of(double value) { + return new OfDouble(value); + } + + static Const of(float value) { + return new OfFloat(value); + } + + static Const of(long value) { + return new OfLong(value); + } + + static Const of(int value) { + return new OfInt(value); + } + + static boolean isConstant(Object candidate) { + return candidate instanceof Const || candidate instanceof Text; + } + + @Override + public void addListener(InvalidationListener listener) { + } + + @Override + public void removeListener(InvalidationListener listener) { + } + + @Override + public void addListener(ChangeListener listener) { + } + + @Override + public void removeListener(ChangeListener listener) { + } + + @Override + public Number getValue() { + return value; + } + + @Override + public int intValue() { + return value.intValue(); + } + + @Override + public long longValue() { + return value.longValue(); + } + + @Override + public float floatValue() { + return value.floatValue(); + } + + @Override + public double doubleValue() { + return value.doubleValue(); + } + + static final class OfDouble extends Const implements ObservableDoubleValue { + OfDouble(double value) { + super(Double.valueOf(value)); + } + + @Override + public double get() { + return doubleValue(); + } + } + + static final class OfFloat extends Const implements ObservableFloatValue { + OfFloat(float value) { + super(Float.valueOf(value)); + } + + @Override + public float get() { + return floatValue(); + } + } + + static final class OfLong extends Const implements ObservableLongValue { + OfLong(long value) { + super(Long.valueOf(value)); + } + + @Override + public long get() { + return longValue(); + } + } + + static final class OfInt extends Const implements ObservableIntegerValue { + OfInt(int value) { + super(Integer.valueOf(value)); + } + + @Override + public int get() { + return intValue(); + } + } + + /// A constant string. + static final class Text implements ObservableStringValue { + private final String value; + + Text(String value) { + this.value = value; + } + + @Override + public void addListener(InvalidationListener listener) { + } + + @Override + public void removeListener(InvalidationListener listener) { + } + + @Override + public void addListener(ChangeListener listener) { + } + + @Override + public void removeListener(ChangeListener listener) { + } + + @Override + public String get() { + return value; + } + + @Override + public String getValue() { + return value; + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/DoubleBinding.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/DoubleBinding.java new file mode 100644 index 00000000000..757d1a351d6 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/DoubleBinding.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import com.codename1.fxcompat.runtime.BindingObserver; +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/// Base class for a lazily evaluated binding that computes a `double`. +/// +/// A subclass registers what it depends on with [#bind(Observable...)] and +/// implements [#computeValue()]. The value is computed on the first read after +/// an invalidation and cached until a dependency changes again; listeners are +/// told about an invalidation once, until the value is read. +public abstract class DoubleBinding extends DoubleExpression implements NumberBinding { + + private double value; + private boolean valid; + private BindingObserver observer; + private ValueListeners helper; + + /// Creates an invalid binding with no dependencies. + public DoubleBinding() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Starts observing the dependencies; each one invalidates this binding. + protected final void bind(Observable... dependencies) { + if (dependencies != null && dependencies.length > 0) { + if (observer == null) { + observer = new BindingObserver(this); + } + for (Observable dependency : dependencies) { + dependency.addListener(observer); + } + } + } + + /// Stops observing the dependencies. + protected final void unbind(Observable... dependencies) { + if (observer != null && dependencies != null) { + for (Observable dependency : dependencies) { + dependency.removeListener(observer); + } + } + } + + /// Releases the dependencies. The default implementation does nothing. + @Override + public void dispose() { + } + + /// Returns the dependencies; the default implementation reports none. + @Override + public ObservableList getDependencies() { + return FXCollections.emptyObservableList(); + } + + /// Returns the value, computing it when the binding is invalid. + @Override + public final double get() { + if (!valid) { + value = computeValue(); + valid = true; + } + return value; + } + + /// Called when the binding turns invalid, before listeners are told. + protected void onInvalidating() { + } + + @Override + public final void invalidate() { + if (valid) { + valid = false; + onInvalidating(); + ValueListeners.fire(helper); + } + } + + @Override + public final boolean isValid() { + return valid; + } + + /// Computes the current value from the dependencies. + protected abstract double computeValue(); + + @Override + public String toString() { + return valid ? "DoubleBinding [value: " + get() + "]" : "DoubleBinding [invalid]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/DoubleExpression.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/DoubleExpression.java new file mode 100644 index 00000000000..e9e57320526 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/DoubleExpression.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableDoubleValue; +import javafx.beans.value.ObservableNumberValue; +import javafx.beans.value.ObservableValue; + +/// A [NumberExpression] whose value is a `double`, with the arithmetic results typed +/// the way Java would type them. +public abstract class DoubleExpression extends NumberExpressionBase implements ObservableDoubleValue { + + /// Creates the expression. + public DoubleExpression() { + } + + @Override + public int intValue() { + return (int) get(); + } + + @Override + public long longValue() { + return (long) get(); + } + + @Override + public float floatValue() { + return (float) get(); + } + + @Override + public double doubleValue() { + return (double) get(); + } + + @Override + public Double getValue() { + return Double.valueOf(get()); + } + + /// Returns the value as an expression: the value itself when it already + /// is one, otherwise a binding that follows it. + public static DoubleExpression doubleExpression(final ObservableDoubleValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof DoubleExpression) { + return (DoubleExpression) value; + } + return new Fn.DoubleFn(new Fn.ToDouble() { + @Override + public double get() { + return value.get(); + } + }, value); + } + + /// Returns an expression following a boxed numeric value; a `null` value + /// reads as zero. + public static DoubleExpression doubleExpression(final ObservableValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof DoubleExpression) { + return (DoubleExpression) value; + } + return new Fn.DoubleFn(new Fn.ToDouble() { + @Override + public double get() { + T current = value.getValue(); + return current == null ? 0 : current.doubleValue(); + } + }, value); + } + + /// Returns an object expression holding the boxed value. + public ObjectExpression asObject() { + return new Fn.ObjectFn(new java.util.function.Supplier() { + @Override + public Double get() { + return getValue(); + } + }, this); + } + + @Override + public DoubleBinding negate() { + return (DoubleBinding) Bindings.negate(this); + } + + @Override + public DoubleBinding add(final ObservableNumberValue other) { + return (DoubleBinding) Bindings.add(this, other); + } + + @Override + public DoubleBinding add(final double other) { + return (DoubleBinding) Bindings.add(this, other); + } + + @Override + public DoubleBinding add(final float other) { + return (DoubleBinding) Bindings.add(this, other); + } + + @Override + public DoubleBinding add(final long other) { + return (DoubleBinding) Bindings.add(this, other); + } + + @Override + public DoubleBinding add(final int other) { + return (DoubleBinding) Bindings.add(this, other); + } + + @Override + public DoubleBinding subtract(final ObservableNumberValue other) { + return (DoubleBinding) Bindings.subtract(this, other); + } + + @Override + public DoubleBinding subtract(final double other) { + return (DoubleBinding) Bindings.subtract(this, other); + } + + @Override + public DoubleBinding subtract(final float other) { + return (DoubleBinding) Bindings.subtract(this, other); + } + + @Override + public DoubleBinding subtract(final long other) { + return (DoubleBinding) Bindings.subtract(this, other); + } + + @Override + public DoubleBinding subtract(final int other) { + return (DoubleBinding) Bindings.subtract(this, other); + } + + @Override + public DoubleBinding multiply(final ObservableNumberValue other) { + return (DoubleBinding) Bindings.multiply(this, other); + } + + @Override + public DoubleBinding multiply(final double other) { + return (DoubleBinding) Bindings.multiply(this, other); + } + + @Override + public DoubleBinding multiply(final float other) { + return (DoubleBinding) Bindings.multiply(this, other); + } + + @Override + public DoubleBinding multiply(final long other) { + return (DoubleBinding) Bindings.multiply(this, other); + } + + @Override + public DoubleBinding multiply(final int other) { + return (DoubleBinding) Bindings.multiply(this, other); + } + + @Override + public DoubleBinding divide(final ObservableNumberValue other) { + return (DoubleBinding) Bindings.divide(this, other); + } + + @Override + public DoubleBinding divide(final double other) { + return (DoubleBinding) Bindings.divide(this, other); + } + + @Override + public DoubleBinding divide(final float other) { + return (DoubleBinding) Bindings.divide(this, other); + } + + @Override + public DoubleBinding divide(final long other) { + return (DoubleBinding) Bindings.divide(this, other); + } + + @Override + public DoubleBinding divide(final int other) { + return (DoubleBinding) Bindings.divide(this, other); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/FloatBinding.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/FloatBinding.java new file mode 100644 index 00000000000..9efeb60903f --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/FloatBinding.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import com.codename1.fxcompat.runtime.BindingObserver; +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/// Base class for a lazily evaluated binding that computes a `float`. +/// +/// A subclass registers what it depends on with [#bind(Observable...)] and +/// implements [#computeValue()]. The value is computed on the first read after +/// an invalidation and cached until a dependency changes again; listeners are +/// told about an invalidation once, until the value is read. +public abstract class FloatBinding extends FloatExpression implements NumberBinding { + + private float value; + private boolean valid; + private BindingObserver observer; + private ValueListeners helper; + + /// Creates an invalid binding with no dependencies. + public FloatBinding() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Starts observing the dependencies; each one invalidates this binding. + protected final void bind(Observable... dependencies) { + if (dependencies != null && dependencies.length > 0) { + if (observer == null) { + observer = new BindingObserver(this); + } + for (Observable dependency : dependencies) { + dependency.addListener(observer); + } + } + } + + /// Stops observing the dependencies. + protected final void unbind(Observable... dependencies) { + if (observer != null && dependencies != null) { + for (Observable dependency : dependencies) { + dependency.removeListener(observer); + } + } + } + + /// Releases the dependencies. The default implementation does nothing. + @Override + public void dispose() { + } + + /// Returns the dependencies; the default implementation reports none. + @Override + public ObservableList getDependencies() { + return FXCollections.emptyObservableList(); + } + + /// Returns the value, computing it when the binding is invalid. + @Override + public final float get() { + if (!valid) { + value = computeValue(); + valid = true; + } + return value; + } + + /// Called when the binding turns invalid, before listeners are told. + protected void onInvalidating() { + } + + @Override + public final void invalidate() { + if (valid) { + valid = false; + onInvalidating(); + ValueListeners.fire(helper); + } + } + + @Override + public final boolean isValid() { + return valid; + } + + /// Computes the current value from the dependencies. + protected abstract float computeValue(); + + @Override + public String toString() { + return valid ? "FloatBinding [value: " + get() + "]" : "FloatBinding [invalid]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/FloatExpression.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/FloatExpression.java new file mode 100644 index 00000000000..fb940b995b6 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/FloatExpression.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableFloatValue; +import javafx.beans.value.ObservableValue; + +/// A [NumberExpression] whose value is a `float`, with the arithmetic results typed +/// the way Java would type them. +public abstract class FloatExpression extends NumberExpressionBase implements ObservableFloatValue { + + /// Creates the expression. + public FloatExpression() { + } + + @Override + public int intValue() { + return (int) get(); + } + + @Override + public long longValue() { + return (long) get(); + } + + @Override + public float floatValue() { + return (float) get(); + } + + @Override + public double doubleValue() { + return (double) get(); + } + + @Override + public Float getValue() { + return Float.valueOf(get()); + } + + /// Returns the value as an expression: the value itself when it already + /// is one, otherwise a binding that follows it. + public static FloatExpression floatExpression(final ObservableFloatValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof FloatExpression) { + return (FloatExpression) value; + } + return new Fn.FloatFn(new Fn.ToFloat() { + @Override + public float get() { + return value.get(); + } + }, value); + } + + /// Returns an expression following a boxed numeric value; a `null` value + /// reads as zero. + public static FloatExpression floatExpression(final ObservableValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof FloatExpression) { + return (FloatExpression) value; + } + return new Fn.FloatFn(new Fn.ToFloat() { + @Override + public float get() { + T current = value.getValue(); + return current == null ? 0 : current.floatValue(); + } + }, value); + } + + /// Returns an object expression holding the boxed value. + public ObjectExpression asObject() { + return new Fn.ObjectFn(new java.util.function.Supplier() { + @Override + public Float get() { + return getValue(); + } + }, this); + } + + @Override + public FloatBinding negate() { + return (FloatBinding) Bindings.negate(this); + } + + @Override + public DoubleBinding add(final double other) { + return (DoubleBinding) Bindings.add(this, other); + } + + @Override + public FloatBinding add(final float other) { + return (FloatBinding) Bindings.add(this, other); + } + + @Override + public FloatBinding add(final long other) { + return (FloatBinding) Bindings.add(this, other); + } + + @Override + public FloatBinding add(final int other) { + return (FloatBinding) Bindings.add(this, other); + } + + @Override + public DoubleBinding subtract(final double other) { + return (DoubleBinding) Bindings.subtract(this, other); + } + + @Override + public FloatBinding subtract(final float other) { + return (FloatBinding) Bindings.subtract(this, other); + } + + @Override + public FloatBinding subtract(final long other) { + return (FloatBinding) Bindings.subtract(this, other); + } + + @Override + public FloatBinding subtract(final int other) { + return (FloatBinding) Bindings.subtract(this, other); + } + + @Override + public DoubleBinding multiply(final double other) { + return (DoubleBinding) Bindings.multiply(this, other); + } + + @Override + public FloatBinding multiply(final float other) { + return (FloatBinding) Bindings.multiply(this, other); + } + + @Override + public FloatBinding multiply(final long other) { + return (FloatBinding) Bindings.multiply(this, other); + } + + @Override + public FloatBinding multiply(final int other) { + return (FloatBinding) Bindings.multiply(this, other); + } + + @Override + public DoubleBinding divide(final double other) { + return (DoubleBinding) Bindings.divide(this, other); + } + + @Override + public FloatBinding divide(final float other) { + return (FloatBinding) Bindings.divide(this, other); + } + + @Override + public FloatBinding divide(final long other) { + return (FloatBinding) Bindings.divide(this, other); + } + + @Override + public FloatBinding divide(final int other) { + return (FloatBinding) Bindings.divide(this, other); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/Fn.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/Fn.java new file mode 100644 index 00000000000..551c251a501 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/Fn.java @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.Observable; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/// Bindings whose computation is a function object. Every operator of +/// [Bindings] and of the fluent expressions is one of these. +final class Fn { + + private Fn() { + } + + static ObservableList dependencies(Observable[] deps) { + if (deps == null || deps.length == 0) { + return FXCollections.emptyObservableList(); + } + if (deps.length == 1) { + return FXCollections.singletonObservableList(deps[0]); + } + return FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(deps)); + } + + /// Computes a boolean. + interface ToBoolean { + boolean get(); + } + + /// A binding computing a boolean from a function. + static final class BooleanFn extends BooleanBinding { + private final ToBoolean fn; + private final Observable[] deps; + + BooleanFn(ToBoolean fn, Observable... deps) { + this.fn = fn; + this.deps = deps; + bind(deps); + } + + @Override + protected boolean computeValue() { + return fn.get(); + } + + @Override + public void dispose() { + unbind(deps); + } + + @Override + public ObservableList getDependencies() { + return dependencies(deps); + } + } + + /// Computes a `int`. + interface ToInteger { + int get(); + } + + /// A binding computing a `int` from a function. + static final class IntegerFn extends IntegerBinding { + private final ToInteger fn; + private final Observable[] deps; + + IntegerFn(ToInteger fn, Observable... deps) { + this.fn = fn; + this.deps = deps; + bind(deps); + } + + @Override + protected int computeValue() { + return fn.get(); + } + + @Override + public void dispose() { + unbind(deps); + } + + @Override + public ObservableList getDependencies() { + return dependencies(deps); + } + } + + /// Computes a `long`. + interface ToLong { + long get(); + } + + /// A binding computing a `long` from a function. + static final class LongFn extends LongBinding { + private final ToLong fn; + private final Observable[] deps; + + LongFn(ToLong fn, Observable... deps) { + this.fn = fn; + this.deps = deps; + bind(deps); + } + + @Override + protected long computeValue() { + return fn.get(); + } + + @Override + public void dispose() { + unbind(deps); + } + + @Override + public ObservableList getDependencies() { + return dependencies(deps); + } + } + + /// Computes a `float`. + interface ToFloat { + float get(); + } + + /// A binding computing a `float` from a function. + static final class FloatFn extends FloatBinding { + private final ToFloat fn; + private final Observable[] deps; + + FloatFn(ToFloat fn, Observable... deps) { + this.fn = fn; + this.deps = deps; + bind(deps); + } + + @Override + protected float computeValue() { + return fn.get(); + } + + @Override + public void dispose() { + unbind(deps); + } + + @Override + public ObservableList getDependencies() { + return dependencies(deps); + } + } + + /// Computes a `double`. + interface ToDouble { + double get(); + } + + /// A binding computing a `double` from a function. + static final class DoubleFn extends DoubleBinding { + private final ToDouble fn; + private final Observable[] deps; + + DoubleFn(ToDouble fn, Observable... deps) { + this.fn = fn; + this.deps = deps; + bind(deps); + } + + @Override + protected double computeValue() { + return fn.get(); + } + + @Override + public void dispose() { + unbind(deps); + } + + @Override + public ObservableList getDependencies() { + return dependencies(deps); + } + } + + /// Computes a string. + interface ToString { + String get(); + } + + /// A binding computing a string from a function. + static final class StringFn extends StringBinding { + private final ToString fn; + private final Observable[] deps; + + StringFn(ToString fn, Observable... deps) { + this.fn = fn; + this.deps = deps; + bind(deps); + } + + @Override + protected String computeValue() { + return fn.get(); + } + + @Override + public void dispose() { + unbind(deps); + } + + @Override + public ObservableList getDependencies() { + return dependencies(deps); + } + } + + /// A binding computing an object from a function. + static final class ObjectFn extends ObjectBinding { + private final java.util.function.Supplier fn; + private final Observable[] deps; + + ObjectFn(java.util.function.Supplier fn, Observable... deps) { + this.fn = fn; + this.deps = deps; + bind(deps); + } + + @Override + protected T computeValue() { + return fn.get(); + } + + @Override + public void dispose() { + unbind(deps); + } + + @Override + public ObservableList getDependencies() { + return dependencies(deps); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/IntegerBinding.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/IntegerBinding.java new file mode 100644 index 00000000000..be41eeee1f3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/IntegerBinding.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import com.codename1.fxcompat.runtime.BindingObserver; +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/// Base class for a lazily evaluated binding that computes a `int`. +/// +/// A subclass registers what it depends on with [#bind(Observable...)] and +/// implements [#computeValue()]. The value is computed on the first read after +/// an invalidation and cached until a dependency changes again; listeners are +/// told about an invalidation once, until the value is read. +public abstract class IntegerBinding extends IntegerExpression implements NumberBinding { + + private int value; + private boolean valid; + private BindingObserver observer; + private ValueListeners helper; + + /// Creates an invalid binding with no dependencies. + public IntegerBinding() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Starts observing the dependencies; each one invalidates this binding. + protected final void bind(Observable... dependencies) { + if (dependencies != null && dependencies.length > 0) { + if (observer == null) { + observer = new BindingObserver(this); + } + for (Observable dependency : dependencies) { + dependency.addListener(observer); + } + } + } + + /// Stops observing the dependencies. + protected final void unbind(Observable... dependencies) { + if (observer != null && dependencies != null) { + for (Observable dependency : dependencies) { + dependency.removeListener(observer); + } + } + } + + /// Releases the dependencies. The default implementation does nothing. + @Override + public void dispose() { + } + + /// Returns the dependencies; the default implementation reports none. + @Override + public ObservableList getDependencies() { + return FXCollections.emptyObservableList(); + } + + /// Returns the value, computing it when the binding is invalid. + @Override + public final int get() { + if (!valid) { + value = computeValue(); + valid = true; + } + return value; + } + + /// Called when the binding turns invalid, before listeners are told. + protected void onInvalidating() { + } + + @Override + public final void invalidate() { + if (valid) { + valid = false; + onInvalidating(); + ValueListeners.fire(helper); + } + } + + @Override + public final boolean isValid() { + return valid; + } + + /// Computes the current value from the dependencies. + protected abstract int computeValue(); + + @Override + public String toString() { + return valid ? "IntegerBinding [value: " + get() + "]" : "IntegerBinding [invalid]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/IntegerExpression.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/IntegerExpression.java new file mode 100644 index 00000000000..c1e6c3f5b16 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/IntegerExpression.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableIntegerValue; +import javafx.beans.value.ObservableValue; + +/// A [NumberExpression] whose value is a `int`, with the arithmetic results typed +/// the way Java would type them. +public abstract class IntegerExpression extends NumberExpressionBase implements ObservableIntegerValue { + + /// Creates the expression. + public IntegerExpression() { + } + + @Override + public int intValue() { + return (int) get(); + } + + @Override + public long longValue() { + return (long) get(); + } + + @Override + public float floatValue() { + return (float) get(); + } + + @Override + public double doubleValue() { + return (double) get(); + } + + @Override + public Integer getValue() { + return Integer.valueOf(get()); + } + + /// Returns the value as an expression: the value itself when it already + /// is one, otherwise a binding that follows it. + public static IntegerExpression integerExpression(final ObservableIntegerValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof IntegerExpression) { + return (IntegerExpression) value; + } + return new Fn.IntegerFn(new Fn.ToInteger() { + @Override + public int get() { + return value.get(); + } + }, value); + } + + /// Returns an expression following a boxed numeric value; a `null` value + /// reads as zero. + public static IntegerExpression integerExpression(final ObservableValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof IntegerExpression) { + return (IntegerExpression) value; + } + return new Fn.IntegerFn(new Fn.ToInteger() { + @Override + public int get() { + T current = value.getValue(); + return current == null ? 0 : current.intValue(); + } + }, value); + } + + /// Returns an object expression holding the boxed value. + public ObjectExpression asObject() { + return new Fn.ObjectFn(new java.util.function.Supplier() { + @Override + public Integer get() { + return getValue(); + } + }, this); + } + + @Override + public IntegerBinding negate() { + return (IntegerBinding) Bindings.negate(this); + } + + @Override + public DoubleBinding add(final double other) { + return (DoubleBinding) Bindings.add(this, other); + } + + @Override + public FloatBinding add(final float other) { + return (FloatBinding) Bindings.add(this, other); + } + + @Override + public LongBinding add(final long other) { + return (LongBinding) Bindings.add(this, other); + } + + @Override + public IntegerBinding add(final int other) { + return (IntegerBinding) Bindings.add(this, other); + } + + @Override + public DoubleBinding subtract(final double other) { + return (DoubleBinding) Bindings.subtract(this, other); + } + + @Override + public FloatBinding subtract(final float other) { + return (FloatBinding) Bindings.subtract(this, other); + } + + @Override + public LongBinding subtract(final long other) { + return (LongBinding) Bindings.subtract(this, other); + } + + @Override + public IntegerBinding subtract(final int other) { + return (IntegerBinding) Bindings.subtract(this, other); + } + + @Override + public DoubleBinding multiply(final double other) { + return (DoubleBinding) Bindings.multiply(this, other); + } + + @Override + public FloatBinding multiply(final float other) { + return (FloatBinding) Bindings.multiply(this, other); + } + + @Override + public LongBinding multiply(final long other) { + return (LongBinding) Bindings.multiply(this, other); + } + + @Override + public IntegerBinding multiply(final int other) { + return (IntegerBinding) Bindings.multiply(this, other); + } + + @Override + public DoubleBinding divide(final double other) { + return (DoubleBinding) Bindings.divide(this, other); + } + + @Override + public FloatBinding divide(final float other) { + return (FloatBinding) Bindings.divide(this, other); + } + + @Override + public LongBinding divide(final long other) { + return (LongBinding) Bindings.divide(this, other); + } + + @Override + public IntegerBinding divide(final int other) { + return (IntegerBinding) Bindings.divide(this, other); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/ListExpression.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/ListExpression.java new file mode 100644 index 00000000000..63bff20d14d --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/ListExpression.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.value.ObservableIntegerValue; +import javafx.beans.value.ObservableListValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/// The fluent operations of an observable reference to an observable list. +/// +/// The expression is itself a list: every list operation goes to the list +/// it currently holds. While it holds `null` it reads as an empty list and +/// cannot be modified. +public abstract class ListExpression implements ObservableListValue { + + /// Creates the expression. + public ListExpression() { + } + + private ObservableList content() { + ObservableList list = get(); + return list == null ? FXCollections.emptyObservableList() : list; + } + + @Override + public ObservableList getValue() { + return get(); + } + + /// Returns the number of elements. + public int getSize() { + return size(); + } + + /// Returns a property holding the number of elements. + public abstract ReadOnlyIntegerProperty sizeProperty(); + + /// Returns a property telling whether the list is empty. + public abstract ReadOnlyBooleanProperty emptyProperty(); + + /// Returns a binding holding the element at a position; `null` when the + /// position is outside the list. + public ObjectBinding valueAt(int index) { + return Bindings.valueAt(this, index); + } + + /// Returns a binding holding the element at an observable position. + public ObjectBinding valueAt(ObservableIntegerValue index) { + return Bindings.valueAt(this, index); + } + + /// Returns a binding telling whether this list and another hold equal + /// elements in the same order. + public BooleanBinding isEqualTo(final ObservableList other) { + return new Fn.BooleanFn(() -> sameContent(other), this, other); + } + + /// Returns a binding telling whether this list and another differ. + public BooleanBinding isNotEqualTo(final ObservableList other) { + return new Fn.BooleanFn(() -> !sameContent(other), this, other); + } + + private boolean sameContent(ObservableList other) { + ObservableList list = get(); + if (list == null) { + return other == null; + } + return other != null && list.equals(other); + } + + /// Returns a binding telling whether no list is held. + public BooleanBinding isNull() { + return new Fn.BooleanFn(() -> get() == null, this); + } + + /// Returns a binding telling whether a list is held. + public BooleanBinding isNotNull() { + return new Fn.BooleanFn(() -> get() != null, this); + } + + /// Returns a binding holding the text of the list. + public StringBinding asString() { + return Bindings.text(this); + } + + @Override + public int size() { + return content().size(); + } + + @Override + public boolean isEmpty() { + return content().isEmpty(); + } + + @Override + public boolean contains(Object obj) { + return content().contains(obj); + } + + @Override + public Iterator iterator() { + return content().iterator(); + } + + @Override + public Object[] toArray() { + return content().toArray(); + } + + @Override + public T[] toArray(T[] array) { + return content().toArray(array); + } + + @Override + public boolean add(E element) { + return content().add(element); + } + + @Override + public boolean remove(Object obj) { + return content().remove(obj); + } + + @Override + public boolean containsAll(Collection objects) { + return content().containsAll(objects); + } + + @Override + public boolean addAll(Collection elements) { + return content().addAll(elements); + } + + @Override + public boolean addAll(int i, Collection elements) { + return content().addAll(i, elements); + } + + @Override + public boolean removeAll(Collection objects) { + return content().removeAll(objects); + } + + @Override + public boolean retainAll(Collection objects) { + return content().retainAll(objects); + } + + @Override + public void clear() { + content().clear(); + } + + @Override + public E get(int i) { + return content().get(i); + } + + @Override + public E set(int i, E element) { + return content().set(i, element); + } + + @Override + public void add(int i, E element) { + content().add(i, element); + } + + @Override + public E remove(int i) { + return content().remove(i); + } + + @Override + public int indexOf(Object obj) { + return content().indexOf(obj); + } + + @Override + public int lastIndexOf(Object obj) { + return content().lastIndexOf(obj); + } + + @Override + public ListIterator listIterator() { + return content().listIterator(); + } + + @Override + public ListIterator listIterator(int i) { + return content().listIterator(i); + } + + @Override + public List subList(int from, int to) { + return content().subList(from, to); + } + + @Override + @SuppressWarnings("unchecked") + public boolean addAll(E... elements) { + return content().addAll(elements); + } + + @Override + @SuppressWarnings("unchecked") + public boolean setAll(E... elements) { + return content().setAll(elements); + } + + @Override + public boolean setAll(Collection elements) { + return content().setAll(elements); + } + + @Override + @SuppressWarnings("unchecked") + public boolean removeAll(E... elements) { + return content().removeAll(elements); + } + + @Override + @SuppressWarnings("unchecked") + public boolean retainAll(E... elements) { + return content().retainAll(elements); + } + + @Override + public void remove(int from, int to) { + content().remove(from, to); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/LongBinding.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/LongBinding.java new file mode 100644 index 00000000000..cdfad6b8045 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/LongBinding.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import com.codename1.fxcompat.runtime.BindingObserver; +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/// Base class for a lazily evaluated binding that computes a `long`. +/// +/// A subclass registers what it depends on with [#bind(Observable...)] and +/// implements [#computeValue()]. The value is computed on the first read after +/// an invalidation and cached until a dependency changes again; listeners are +/// told about an invalidation once, until the value is read. +public abstract class LongBinding extends LongExpression implements NumberBinding { + + private long value; + private boolean valid; + private BindingObserver observer; + private ValueListeners helper; + + /// Creates an invalid binding with no dependencies. + public LongBinding() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Starts observing the dependencies; each one invalidates this binding. + protected final void bind(Observable... dependencies) { + if (dependencies != null && dependencies.length > 0) { + if (observer == null) { + observer = new BindingObserver(this); + } + for (Observable dependency : dependencies) { + dependency.addListener(observer); + } + } + } + + /// Stops observing the dependencies. + protected final void unbind(Observable... dependencies) { + if (observer != null && dependencies != null) { + for (Observable dependency : dependencies) { + dependency.removeListener(observer); + } + } + } + + /// Releases the dependencies. The default implementation does nothing. + @Override + public void dispose() { + } + + /// Returns the dependencies; the default implementation reports none. + @Override + public ObservableList getDependencies() { + return FXCollections.emptyObservableList(); + } + + /// Returns the value, computing it when the binding is invalid. + @Override + public final long get() { + if (!valid) { + value = computeValue(); + valid = true; + } + return value; + } + + /// Called when the binding turns invalid, before listeners are told. + protected void onInvalidating() { + } + + @Override + public final void invalidate() { + if (valid) { + valid = false; + onInvalidating(); + ValueListeners.fire(helper); + } + } + + @Override + public final boolean isValid() { + return valid; + } + + /// Computes the current value from the dependencies. + protected abstract long computeValue(); + + @Override + public String toString() { + return valid ? "LongBinding [value: " + get() + "]" : "LongBinding [invalid]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/LongExpression.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/LongExpression.java new file mode 100644 index 00000000000..879dd746a80 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/LongExpression.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableLongValue; +import javafx.beans.value.ObservableValue; + +/// A [NumberExpression] whose value is a `long`, with the arithmetic results typed +/// the way Java would type them. +public abstract class LongExpression extends NumberExpressionBase implements ObservableLongValue { + + /// Creates the expression. + public LongExpression() { + } + + @Override + public int intValue() { + return (int) get(); + } + + @Override + public long longValue() { + return (long) get(); + } + + @Override + public float floatValue() { + return (float) get(); + } + + @Override + public double doubleValue() { + return (double) get(); + } + + @Override + public Long getValue() { + return Long.valueOf(get()); + } + + /// Returns the value as an expression: the value itself when it already + /// is one, otherwise a binding that follows it. + public static LongExpression longExpression(final ObservableLongValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof LongExpression) { + return (LongExpression) value; + } + return new Fn.LongFn(new Fn.ToLong() { + @Override + public long get() { + return value.get(); + } + }, value); + } + + /// Returns an expression following a boxed numeric value; a `null` value + /// reads as zero. + public static LongExpression longExpression(final ObservableValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof LongExpression) { + return (LongExpression) value; + } + return new Fn.LongFn(new Fn.ToLong() { + @Override + public long get() { + T current = value.getValue(); + return current == null ? 0 : current.longValue(); + } + }, value); + } + + /// Returns an object expression holding the boxed value. + public ObjectExpression asObject() { + return new Fn.ObjectFn(new java.util.function.Supplier() { + @Override + public Long get() { + return getValue(); + } + }, this); + } + + @Override + public LongBinding negate() { + return (LongBinding) Bindings.negate(this); + } + + @Override + public DoubleBinding add(final double other) { + return (DoubleBinding) Bindings.add(this, other); + } + + @Override + public FloatBinding add(final float other) { + return (FloatBinding) Bindings.add(this, other); + } + + @Override + public LongBinding add(final long other) { + return (LongBinding) Bindings.add(this, other); + } + + @Override + public LongBinding add(final int other) { + return (LongBinding) Bindings.add(this, other); + } + + @Override + public DoubleBinding subtract(final double other) { + return (DoubleBinding) Bindings.subtract(this, other); + } + + @Override + public FloatBinding subtract(final float other) { + return (FloatBinding) Bindings.subtract(this, other); + } + + @Override + public LongBinding subtract(final long other) { + return (LongBinding) Bindings.subtract(this, other); + } + + @Override + public LongBinding subtract(final int other) { + return (LongBinding) Bindings.subtract(this, other); + } + + @Override + public DoubleBinding multiply(final double other) { + return (DoubleBinding) Bindings.multiply(this, other); + } + + @Override + public FloatBinding multiply(final float other) { + return (FloatBinding) Bindings.multiply(this, other); + } + + @Override + public LongBinding multiply(final long other) { + return (LongBinding) Bindings.multiply(this, other); + } + + @Override + public LongBinding multiply(final int other) { + return (LongBinding) Bindings.multiply(this, other); + } + + @Override + public DoubleBinding divide(final double other) { + return (DoubleBinding) Bindings.divide(this, other); + } + + @Override + public FloatBinding divide(final float other) { + return (FloatBinding) Bindings.divide(this, other); + } + + @Override + public LongBinding divide(final long other) { + return (LongBinding) Bindings.divide(this, other); + } + + @Override + public LongBinding divide(final int other) { + return (LongBinding) Bindings.divide(this, other); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/NumberBinding.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/NumberBinding.java new file mode 100644 index 00000000000..a914b59b316 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/NumberBinding.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +/// A binding whose value is a number. +public interface NumberBinding extends Binding, NumberExpression { +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/NumberExpression.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/NumberExpression.java new file mode 100644 index 00000000000..3c264645ee3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/NumberExpression.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableNumberValue; + +/// The fluent operations of an observable number: arithmetic, comparison and +/// conversion to text, each returning a binding that follows this number. +/// +/// The result of an arithmetic operation has the wider of the two operand +/// types, as the same expression would in Java. +public interface NumberExpression extends ObservableNumberValue { + + /// Returns a binding computing the negation of this number. + NumberBinding negate(); + + /// Returns a binding computing the sum of this number and another. + NumberBinding add(final ObservableNumberValue other); + + /// Returns a binding computing the sum of this number and a constant. + NumberBinding add(final double other); + + /// Returns a binding computing the sum of this number and a constant. + NumberBinding add(final float other); + + /// Returns a binding computing the sum of this number and a constant. + NumberBinding add(final long other); + + /// Returns a binding computing the sum of this number and a constant. + NumberBinding add(final int other); + + /// Returns a binding computing the difference of this number and another. + NumberBinding subtract(final ObservableNumberValue other); + + /// Returns a binding computing the difference of this number and a constant. + NumberBinding subtract(final double other); + + /// Returns a binding computing the difference of this number and a constant. + NumberBinding subtract(final float other); + + /// Returns a binding computing the difference of this number and a constant. + NumberBinding subtract(final long other); + + /// Returns a binding computing the difference of this number and a constant. + NumberBinding subtract(final int other); + + /// Returns a binding computing the product of this number and another. + NumberBinding multiply(final ObservableNumberValue other); + + /// Returns a binding computing the product of this number and a constant. + NumberBinding multiply(final double other); + + /// Returns a binding computing the product of this number and a constant. + NumberBinding multiply(final float other); + + /// Returns a binding computing the product of this number and a constant. + NumberBinding multiply(final long other); + + /// Returns a binding computing the product of this number and a constant. + NumberBinding multiply(final int other); + + /// Returns a binding computing the quotient of this number and another. + NumberBinding divide(final ObservableNumberValue other); + + /// Returns a binding computing the quotient of this number and a constant. + NumberBinding divide(final double other); + + /// Returns a binding computing the quotient of this number and a constant. + NumberBinding divide(final float other); + + /// Returns a binding computing the quotient of this number and a constant. + NumberBinding divide(final long other); + + /// Returns a binding computing the quotient of this number and a constant. + NumberBinding divide(final int other); + + /// Returns a binding telling whether this number and another are equal. + BooleanBinding isEqualTo(final ObservableNumberValue other); + + /// Returns a binding telling whether this number and another are equal, within a tolerance. + BooleanBinding isEqualTo(final ObservableNumberValue other, double epsilon); + + /// Returns a binding telling whether this number and a constant are equal, within a tolerance. + BooleanBinding isEqualTo(final double other, double epsilon); + + /// Returns a binding telling whether this number and a constant are equal, within a tolerance. + BooleanBinding isEqualTo(final float other, double epsilon); + + /// Returns a binding telling whether this number and a constant are equal. + BooleanBinding isEqualTo(final long other); + + /// Returns a binding telling whether this number and a constant are equal, within a tolerance. + BooleanBinding isEqualTo(final long other, double epsilon); + + /// Returns a binding telling whether this number and a constant are equal. + BooleanBinding isEqualTo(final int other); + + /// Returns a binding telling whether this number and a constant are equal, within a tolerance. + BooleanBinding isEqualTo(final int other, double epsilon); + + /// Returns a binding telling whether this number and another are not equal. + BooleanBinding isNotEqualTo(final ObservableNumberValue other); + + /// Returns a binding telling whether this number and another are not equal, within a tolerance. + BooleanBinding isNotEqualTo(final ObservableNumberValue other, double epsilon); + + /// Returns a binding telling whether this number and a constant are not equal, within a tolerance. + BooleanBinding isNotEqualTo(final double other, double epsilon); + + /// Returns a binding telling whether this number and a constant are not equal, within a tolerance. + BooleanBinding isNotEqualTo(final float other, double epsilon); + + /// Returns a binding telling whether this number and a constant are not equal. + BooleanBinding isNotEqualTo(final long other); + + /// Returns a binding telling whether this number and a constant are not equal, within a tolerance. + BooleanBinding isNotEqualTo(final long other, double epsilon); + + /// Returns a binding telling whether this number and a constant are not equal. + BooleanBinding isNotEqualTo(final int other); + + /// Returns a binding telling whether this number and a constant are not equal, within a tolerance. + BooleanBinding isNotEqualTo(final int other, double epsilon); + + /// Returns a binding telling whether this number is greater than another. + BooleanBinding greaterThan(final ObservableNumberValue other); + + /// Returns a binding telling whether this number is greater than a constant. + BooleanBinding greaterThan(final double other); + + /// Returns a binding telling whether this number is greater than a constant. + BooleanBinding greaterThan(final float other); + + /// Returns a binding telling whether this number is greater than a constant. + BooleanBinding greaterThan(final long other); + + /// Returns a binding telling whether this number is greater than a constant. + BooleanBinding greaterThan(final int other); + + /// Returns a binding telling whether this number is less than another. + BooleanBinding lessThan(final ObservableNumberValue other); + + /// Returns a binding telling whether this number is less than a constant. + BooleanBinding lessThan(final double other); + + /// Returns a binding telling whether this number is less than a constant. + BooleanBinding lessThan(final float other); + + /// Returns a binding telling whether this number is less than a constant. + BooleanBinding lessThan(final long other); + + /// Returns a binding telling whether this number is less than a constant. + BooleanBinding lessThan(final int other); + + /// Returns a binding telling whether this number is greater than or equal to another. + BooleanBinding greaterThanOrEqualTo(final ObservableNumberValue other); + + /// Returns a binding telling whether this number is greater than or equal to a constant. + BooleanBinding greaterThanOrEqualTo(final double other); + + /// Returns a binding telling whether this number is greater than or equal to a constant. + BooleanBinding greaterThanOrEqualTo(final float other); + + /// Returns a binding telling whether this number is greater than or equal to a constant. + BooleanBinding greaterThanOrEqualTo(final long other); + + /// Returns a binding telling whether this number is greater than or equal to a constant. + BooleanBinding greaterThanOrEqualTo(final int other); + + /// Returns a binding telling whether this number is less than or equal to another. + BooleanBinding lessThanOrEqualTo(final ObservableNumberValue other); + + /// Returns a binding telling whether this number is less than or equal to a constant. + BooleanBinding lessThanOrEqualTo(final double other); + + /// Returns a binding telling whether this number is less than or equal to a constant. + BooleanBinding lessThanOrEqualTo(final float other); + + /// Returns a binding telling whether this number is less than or equal to a constant. + BooleanBinding lessThanOrEqualTo(final long other); + + /// Returns a binding telling whether this number is less than or equal to a constant. + BooleanBinding lessThanOrEqualTo(final int other); + + /// Returns a binding holding this number as text. + StringBinding asString(); + + /// Returns a binding holding this number formatted with a `String.format` pattern. + StringBinding asString(String format); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/NumberExpressionBase.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/NumberExpressionBase.java new file mode 100644 index 00000000000..06384d8cb29 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/NumberExpressionBase.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableDoubleValue; +import javafx.beans.value.ObservableFloatValue; +import javafx.beans.value.ObservableIntegerValue; +import javafx.beans.value.ObservableLongValue; +import javafx.beans.value.ObservableNumberValue; + +/// Implements every operation of [NumberExpression] on top of [Bindings], +/// leaving only the value itself to the typed subclasses. +public abstract class NumberExpressionBase implements NumberExpression { + + /// Creates the expression. + public NumberExpressionBase() { + } + + /// Returns a number as an expression: the value itself when it already + /// is one, otherwise a binding of the matching type that follows it. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when the value is of none of the four + /// typed kinds + public static NumberExpressionBase numberExpression(final ObservableNumberValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof NumberExpressionBase) { + return (NumberExpressionBase) value; + } + if (value instanceof ObservableIntegerValue) { + return IntegerExpression.integerExpression((ObservableIntegerValue) value); + } + if (value instanceof ObservableDoubleValue) { + return DoubleExpression.doubleExpression((ObservableDoubleValue) value); + } + if (value instanceof ObservableFloatValue) { + return FloatExpression.floatExpression((ObservableFloatValue) value); + } + if (value instanceof ObservableLongValue) { + return LongExpression.longExpression((ObservableLongValue) value); + } + throw new IllegalArgumentException("Unsupported Type"); + } + + @Override + public NumberBinding negate() { + return Bindings.negate(this); + } + + @Override + public NumberBinding add(final ObservableNumberValue other) { + return Bindings.add(this, other); + } + + @Override + public NumberBinding add(final double other) { + return Bindings.add(this, other); + } + + @Override + public NumberBinding add(final float other) { + return Bindings.add(this, other); + } + + @Override + public NumberBinding add(final long other) { + return Bindings.add(this, other); + } + + @Override + public NumberBinding add(final int other) { + return Bindings.add(this, other); + } + + @Override + public NumberBinding subtract(final ObservableNumberValue other) { + return Bindings.subtract(this, other); + } + + @Override + public NumberBinding subtract(final double other) { + return Bindings.subtract(this, other); + } + + @Override + public NumberBinding subtract(final float other) { + return Bindings.subtract(this, other); + } + + @Override + public NumberBinding subtract(final long other) { + return Bindings.subtract(this, other); + } + + @Override + public NumberBinding subtract(final int other) { + return Bindings.subtract(this, other); + } + + @Override + public NumberBinding multiply(final ObservableNumberValue other) { + return Bindings.multiply(this, other); + } + + @Override + public NumberBinding multiply(final double other) { + return Bindings.multiply(this, other); + } + + @Override + public NumberBinding multiply(final float other) { + return Bindings.multiply(this, other); + } + + @Override + public NumberBinding multiply(final long other) { + return Bindings.multiply(this, other); + } + + @Override + public NumberBinding multiply(final int other) { + return Bindings.multiply(this, other); + } + + @Override + public NumberBinding divide(final ObservableNumberValue other) { + return Bindings.divide(this, other); + } + + @Override + public NumberBinding divide(final double other) { + return Bindings.divide(this, other); + } + + @Override + public NumberBinding divide(final float other) { + return Bindings.divide(this, other); + } + + @Override + public NumberBinding divide(final long other) { + return Bindings.divide(this, other); + } + + @Override + public NumberBinding divide(final int other) { + return Bindings.divide(this, other); + } + + @Override + public BooleanBinding isEqualTo(final ObservableNumberValue other) { + return Bindings.equal(this, other); + } + + @Override + public BooleanBinding isEqualTo(final ObservableNumberValue other, double epsilon) { + return Bindings.equal(this, other, epsilon); + } + + @Override + public BooleanBinding isEqualTo(final double other, double epsilon) { + return Bindings.equal(this, other, epsilon); + } + + @Override + public BooleanBinding isEqualTo(final float other, double epsilon) { + return Bindings.equal(this, other, epsilon); + } + + @Override + public BooleanBinding isEqualTo(final long other) { + return Bindings.equal(this, other); + } + + @Override + public BooleanBinding isEqualTo(final long other, double epsilon) { + return Bindings.equal(this, other, epsilon); + } + + @Override + public BooleanBinding isEqualTo(final int other) { + return Bindings.equal(this, other); + } + + @Override + public BooleanBinding isEqualTo(final int other, double epsilon) { + return Bindings.equal(this, other, epsilon); + } + + @Override + public BooleanBinding isNotEqualTo(final ObservableNumberValue other) { + return Bindings.notEqual(this, other); + } + + @Override + public BooleanBinding isNotEqualTo(final ObservableNumberValue other, double epsilon) { + return Bindings.notEqual(this, other, epsilon); + } + + @Override + public BooleanBinding isNotEqualTo(final double other, double epsilon) { + return Bindings.notEqual(this, other, epsilon); + } + + @Override + public BooleanBinding isNotEqualTo(final float other, double epsilon) { + return Bindings.notEqual(this, other, epsilon); + } + + @Override + public BooleanBinding isNotEqualTo(final long other) { + return Bindings.notEqual(this, other); + } + + @Override + public BooleanBinding isNotEqualTo(final long other, double epsilon) { + return Bindings.notEqual(this, other, epsilon); + } + + @Override + public BooleanBinding isNotEqualTo(final int other) { + return Bindings.notEqual(this, other); + } + + @Override + public BooleanBinding isNotEqualTo(final int other, double epsilon) { + return Bindings.notEqual(this, other, epsilon); + } + + @Override + public BooleanBinding greaterThan(final ObservableNumberValue other) { + return Bindings.greaterThan(this, other); + } + + @Override + public BooleanBinding greaterThan(final double other) { + return Bindings.greaterThan(this, other); + } + + @Override + public BooleanBinding greaterThan(final float other) { + return Bindings.greaterThan(this, other); + } + + @Override + public BooleanBinding greaterThan(final long other) { + return Bindings.greaterThan(this, other); + } + + @Override + public BooleanBinding greaterThan(final int other) { + return Bindings.greaterThan(this, other); + } + + @Override + public BooleanBinding lessThan(final ObservableNumberValue other) { + return Bindings.lessThan(this, other); + } + + @Override + public BooleanBinding lessThan(final double other) { + return Bindings.lessThan(this, other); + } + + @Override + public BooleanBinding lessThan(final float other) { + return Bindings.lessThan(this, other); + } + + @Override + public BooleanBinding lessThan(final long other) { + return Bindings.lessThan(this, other); + } + + @Override + public BooleanBinding lessThan(final int other) { + return Bindings.lessThan(this, other); + } + + @Override + public BooleanBinding greaterThanOrEqualTo(final ObservableNumberValue other) { + return Bindings.greaterThanOrEqual(this, other); + } + + @Override + public BooleanBinding greaterThanOrEqualTo(final double other) { + return Bindings.greaterThanOrEqual(this, other); + } + + @Override + public BooleanBinding greaterThanOrEqualTo(final float other) { + return Bindings.greaterThanOrEqual(this, other); + } + + @Override + public BooleanBinding greaterThanOrEqualTo(final long other) { + return Bindings.greaterThanOrEqual(this, other); + } + + @Override + public BooleanBinding greaterThanOrEqualTo(final int other) { + return Bindings.greaterThanOrEqual(this, other); + } + + @Override + public BooleanBinding lessThanOrEqualTo(final ObservableNumberValue other) { + return Bindings.lessThanOrEqual(this, other); + } + + @Override + public BooleanBinding lessThanOrEqualTo(final double other) { + return Bindings.lessThanOrEqual(this, other); + } + + @Override + public BooleanBinding lessThanOrEqualTo(final float other) { + return Bindings.lessThanOrEqual(this, other); + } + + @Override + public BooleanBinding lessThanOrEqualTo(final long other) { + return Bindings.lessThanOrEqual(this, other); + } + + @Override + public BooleanBinding lessThanOrEqualTo(final int other) { + return Bindings.lessThanOrEqual(this, other); + } + + @Override + public StringBinding asString() { + return Bindings.text(this); + } + + @Override + public StringBinding asString(String format) { + return Bindings.formatted(format, this); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/ObjectBinding.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/ObjectBinding.java new file mode 100644 index 00000000000..26b6863d166 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/ObjectBinding.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import com.codename1.fxcompat.runtime.BindingObserver; +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/// Base class for a lazily evaluated binding that computes an object. +/// +/// A subclass registers what it depends on with [#bind(Observable...)] and +/// implements [#computeValue()]. The value is computed on the first read after +/// an invalidation and cached until a dependency changes again; listeners are +/// told about an invalidation once, until the value is read. +public abstract class ObjectBinding extends ObjectExpression implements Binding { + + private T value; + private boolean valid; + private BindingObserver observer; + private ValueListeners helper; + + /// Creates an invalid binding with no dependencies. + public ObjectBinding() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Starts observing the dependencies; each one invalidates this binding. + protected final void bind(Observable... dependencies) { + if (dependencies != null && dependencies.length > 0) { + if (observer == null) { + observer = new BindingObserver(this); + } + for (Observable dependency : dependencies) { + dependency.addListener(observer); + } + } + } + + /// Stops observing the dependencies. + protected final void unbind(Observable... dependencies) { + if (observer != null && dependencies != null) { + for (Observable dependency : dependencies) { + dependency.removeListener(observer); + } + } + } + + /// Releases the dependencies. The default implementation does nothing. + @Override + public void dispose() { + } + + /// Returns the dependencies; the default implementation reports none. + @Override + public ObservableList getDependencies() { + return FXCollections.emptyObservableList(); + } + + /// Returns the value, computing it when the binding is invalid. + @Override + public final T get() { + if (!valid) { + value = computeValue(); + valid = true; + } + return value; + } + + /// Called when the binding turns invalid, before listeners are told. + protected void onInvalidating() { + } + + @Override + public final void invalidate() { + if (valid) { + valid = false; + onInvalidating(); + ValueListeners.fire(helper); + } + } + + @Override + public final boolean isValid() { + return valid; + } + + /// Computes the current value from the dependencies. + protected abstract T computeValue(); + + @Override + public String toString() { + return valid ? "ObjectBinding [value: " + get() + "]" : "ObjectBinding [invalid]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/ObjectExpression.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/ObjectExpression.java new file mode 100644 index 00000000000..1b61a4eef24 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/ObjectExpression.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableObjectValue; + +/// The fluent operations of an observable object reference. +public abstract class ObjectExpression implements ObservableObjectValue { + + /// Creates the expression. + public ObjectExpression() { + } + + @Override + public T getValue() { + return get(); + } + + /// Returns the value as an expression: the value itself when it already + /// is one, otherwise a binding that follows it. + public static ObjectExpression objectExpression(final ObservableObjectValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + if (value instanceof ObjectExpression) { + return (ObjectExpression) value; + } + return new Fn.ObjectFn(() -> value.get(), value); + } + + /// Returns a binding telling whether this object equals another. + public BooleanBinding isEqualTo(final ObservableObjectValue other) { + return Bindings.equal(this, other); + } + + /// Returns a binding telling whether this object equals a constant. + public BooleanBinding isEqualTo(final Object other) { + return Bindings.equal(this, other); + } + + /// Returns a binding telling whether this object differs from another. + public BooleanBinding isNotEqualTo(final ObservableObjectValue other) { + return Bindings.notEqual(this, other); + } + + /// Returns a binding telling whether this object differs from a constant. + public BooleanBinding isNotEqualTo(final Object other) { + return Bindings.notEqual(this, other); + } + + /// Returns a binding telling whether the value is `null`. + public BooleanBinding isNull() { + return Bindings.isNull(this); + } + + /// Returns a binding telling whether the value is not `null`. + public BooleanBinding isNotNull() { + return Bindings.isNotNull(this); + } + + /// Returns a binding holding the text of the object; `"null"` for none. + public StringBinding asString() { + return Bindings.text(this); + } + + /// Returns a binding holding the object formatted with a `String.format` + /// pattern. + public StringBinding asString(String format) { + return Bindings.formatted(format, this); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/StringBinding.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/StringBinding.java new file mode 100644 index 00000000000..b78d8ca4084 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/StringBinding.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import com.codename1.fxcompat.runtime.BindingObserver; +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/// Base class for a lazily evaluated binding that computes a string. +/// +/// A subclass registers what it depends on with [#bind(Observable...)] and +/// implements [#computeValue()]. The value is computed on the first read after +/// an invalidation and cached until a dependency changes again; listeners are +/// told about an invalidation once, until the value is read. +public abstract class StringBinding extends StringExpression implements Binding { + + private String value; + private boolean valid; + private BindingObserver observer; + private ValueListeners helper; + + /// Creates an invalid binding with no dependencies. + public StringBinding() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Starts observing the dependencies; each one invalidates this binding. + protected final void bind(Observable... dependencies) { + if (dependencies != null && dependencies.length > 0) { + if (observer == null) { + observer = new BindingObserver(this); + } + for (Observable dependency : dependencies) { + dependency.addListener(observer); + } + } + } + + /// Stops observing the dependencies. + protected final void unbind(Observable... dependencies) { + if (observer != null && dependencies != null) { + for (Observable dependency : dependencies) { + dependency.removeListener(observer); + } + } + } + + /// Releases the dependencies. The default implementation does nothing. + @Override + public void dispose() { + } + + /// Returns the dependencies; the default implementation reports none. + @Override + public ObservableList getDependencies() { + return FXCollections.emptyObservableList(); + } + + /// Returns the value, computing it when the binding is invalid. + @Override + public final String get() { + if (!valid) { + value = computeValue(); + valid = true; + } + return value; + } + + /// Called when the binding turns invalid, before listeners are told. + protected void onInvalidating() { + } + + @Override + public final void invalidate() { + if (valid) { + valid = false; + onInvalidating(); + ValueListeners.fire(helper); + } + } + + @Override + public final boolean isValid() { + return valid; + } + + /// Computes the current value from the dependencies. + protected abstract String computeValue(); + + @Override + public String toString() { + return valid ? "StringBinding [value: " + get() + "]" : "StringBinding [invalid]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/StringExpression.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/StringExpression.java new file mode 100644 index 00000000000..c95ffa17804 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/StringExpression.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; + +/// The fluent operations of an observable string. Comparisons treat `null` +/// as the empty string. +public abstract class StringExpression implements ObservableStringValue { + + /// Creates the expression. + public StringExpression() { + } + + @Override + public String getValue() { + return get(); + } + + /// Returns the value, or the empty string when it is `null`. + public final String getValueSafe() { + final String value = get(); + return value == null ? "" : value; + } + + /// Returns any observable value as a string expression: the value itself + /// when it already is one, otherwise a binding holding its text. + public static StringExpression stringExpression(final ObservableValue value) { + if (value == null) { + throw new NullPointerException("Value must be specified."); + } + return Bindings.convert(value); + } + + /// Returns an expression holding this string followed by the text of + /// another object, which is observed when it is an observable value. + public StringExpression concat(Object other) { + return Bindings.concat(this, other); + } + + /// Returns a binding telling whether this string equals another. + public BooleanBinding isEqualTo(final ObservableStringValue other) { + return Bindings.equal(this, other); + } + + /// Returns a binding telling whether this string equals a constant. + public BooleanBinding isEqualTo(final String other) { + return Bindings.equal(this, other); + } + + /// Returns a binding telling whether this string differs from another. + public BooleanBinding isNotEqualTo(final ObservableStringValue other) { + return Bindings.notEqual(this, other); + } + + /// Returns a binding telling whether this string differs from a constant. + public BooleanBinding isNotEqualTo(final String other) { + return Bindings.notEqual(this, other); + } + + /// Returns a binding telling whether this string equals another, + /// ignoring case. + public BooleanBinding isEqualToIgnoreCase(final ObservableStringValue other) { + return Bindings.equalIgnoreCase(this, other); + } + + /// Returns a binding telling whether this string equals a constant, + /// ignoring case. + public BooleanBinding isEqualToIgnoreCase(final String other) { + return Bindings.equalIgnoreCase(this, other); + } + + /// Returns a binding telling whether this string differs from another + /// even ignoring case. + public BooleanBinding isNotEqualToIgnoreCase(final ObservableStringValue other) { + return Bindings.notEqualIgnoreCase(this, other); + } + + /// Returns a binding telling whether this string differs from a constant + /// even ignoring case. + public BooleanBinding isNotEqualToIgnoreCase(final String other) { + return Bindings.notEqualIgnoreCase(this, other); + } + + /// Returns a binding telling whether this string sorts after another. + public BooleanBinding greaterThan(final ObservableStringValue other) { + return Bindings.greaterThan(this, other); + } + + /// Returns a binding telling whether this string sorts after a constant. + public BooleanBinding greaterThan(final String other) { + return Bindings.greaterThan(this, other); + } + + /// Returns a binding telling whether this string sorts before another. + public BooleanBinding lessThan(final ObservableStringValue other) { + return Bindings.lessThan(this, other); + } + + /// Returns a binding telling whether this string sorts before a constant. + public BooleanBinding lessThan(final String other) { + return Bindings.lessThan(this, other); + } + + /// Returns a binding telling whether this string does not sort before + /// another. + public BooleanBinding greaterThanOrEqualTo(final ObservableStringValue other) { + return Bindings.greaterThanOrEqual(this, other); + } + + /// Returns a binding telling whether this string does not sort before a + /// constant. + public BooleanBinding greaterThanOrEqualTo(final String other) { + return Bindings.greaterThanOrEqual(this, other); + } + + /// Returns a binding telling whether this string does not sort after + /// another. + public BooleanBinding lessThanOrEqualTo(final ObservableStringValue other) { + return Bindings.lessThanOrEqual(this, other); + } + + /// Returns a binding telling whether this string does not sort after a + /// constant. + public BooleanBinding lessThanOrEqualTo(final String other) { + return Bindings.lessThanOrEqual(this, other); + } + + /// Returns a binding telling whether the value is `null`. + public BooleanBinding isNull() { + return Bindings.isNull(this); + } + + /// Returns a binding telling whether the value is not `null`. + public BooleanBinding isNotNull() { + return Bindings.isNotNull(this); + } + + /// Returns a binding holding the length of the string; zero for `null`. + public IntegerBinding length() { + return Bindings.length(this); + } + + /// Returns a binding telling whether the string is `null` or empty. + public BooleanBinding isEmpty() { + return Bindings.isEmpty(this); + } + + /// Returns a binding telling whether the string has any character. + public BooleanBinding isNotEmpty() { + return Bindings.isNotEmpty(this); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/When.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/When.java new file mode 100644 index 00000000000..3c8309c9ddc --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/When.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.binding; + +import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableNumberValue; +import javafx.beans.value.ObservableObjectValue; +import javafx.beans.value.ObservableStringValue; + +/// The start of a ternary binding: +/// `new When(condition).then(a).otherwise(b)` holds `a` while the condition +/// is true and `b` otherwise. Either branch may be an observable value or a +/// constant. +public class When { + + private final ObservableBooleanValue condition; + + /// Starts a ternary binding on a condition. + public When(final ObservableBooleanValue condition) { + if (condition == null) { + throw new NullPointerException("Condition must be specified."); + } + this.condition = condition; + } + + private static V required(V value) { + if (value == null) { + throw new NullPointerException("Value needs to be specified"); + } + return value; + } + + private static BooleanBinding booleans(final ObservableBooleanValue condition, final ObservableBooleanValue yes, + final boolean yesConstant, final ObservableBooleanValue no, final boolean noConstant) { + return new Fn.BooleanFn(() -> { + if (condition.get()) { + return yes == null ? yesConstant : yes.get(); + } + return no == null ? noConstant : no.get(); + }, Bindings.deps(condition, yes, no)); + } + + private static ObjectBinding objects(final ObservableBooleanValue condition, + final ObservableObjectValue yes, final T yesConstant, final ObservableObjectValue no, + final T noConstant) { + return new Fn.ObjectFn(() -> { + if (condition.get()) { + return yes == null ? yesConstant : yes.get(); + } + return no == null ? noConstant : no.get(); + }, Bindings.deps(condition, yes, no)); + } + + private static StringBinding strings(final ObservableBooleanValue condition, final ObservableStringValue yes, + final String yesConstant, final ObservableStringValue no, final String noConstant) { + return new Fn.StringFn(() -> { + if (condition.get()) { + return yes == null ? yesConstant : yes.get(); + } + return no == null ? noConstant : no.get(); + }, Bindings.deps(condition, yes, no)); + } + + /// Chooses a number for the true branch. + public NumberConditionBuilder then(final ObservableNumberValue thenValue) { + return new NumberConditionBuilder(required(thenValue)); + } + + /// Chooses a constant for the true branch. + public NumberConditionBuilder then(double thenValue) { + return new NumberConditionBuilder(Const.of(thenValue)); + } + + /// Chooses a constant for the true branch. + public NumberConditionBuilder then(float thenValue) { + return new NumberConditionBuilder(Const.of(thenValue)); + } + + /// Chooses a constant for the true branch. + public NumberConditionBuilder then(long thenValue) { + return new NumberConditionBuilder(Const.of(thenValue)); + } + + /// Chooses a constant for the true branch. + public NumberConditionBuilder then(int thenValue) { + return new NumberConditionBuilder(Const.of(thenValue)); + } + + /// Chooses a boolean for the true branch. + public BooleanConditionBuilder then(final ObservableBooleanValue thenValue) { + return new BooleanConditionBuilder(required(thenValue), false); + } + + /// Chooses a constant for the true branch. + public BooleanConditionBuilder then(final boolean thenValue) { + return new BooleanConditionBuilder(null, thenValue); + } + + /// Chooses a string for the true branch. + public StringConditionBuilder then(final ObservableStringValue thenValue) { + return new StringConditionBuilder(required(thenValue), null); + } + + /// Chooses a constant for the true branch. + public StringConditionBuilder then(final String thenValue) { + return new StringConditionBuilder(null, thenValue); + } + + /// Chooses an object for the true branch. + public ObjectConditionBuilder then(final ObservableObjectValue thenValue) { + return new ObjectConditionBuilder(required(thenValue), null); + } + + /// Chooses a constant for the true branch. + public ObjectConditionBuilder then(final T thenValue) { + return new ObjectConditionBuilder(null, thenValue); + } + + /// A ternary binding with a numeric true branch, waiting for the other + /// one. The result has the wider of the two branch types. + public class NumberConditionBuilder { + + private final ObservableNumberValue thenValue; + + private NumberConditionBuilder(final ObservableNumberValue thenValue) { + this.thenValue = thenValue; + } + + /// Completes the binding with a number for the false branch. + public NumberBinding otherwise(final ObservableNumberValue otherwiseValue) { + return Bindings.choose(condition, thenValue, required(otherwiseValue)); + } + + /// Completes the binding with a constant for the false branch. + public DoubleBinding otherwise(double otherwiseValue) { + return (DoubleBinding) Bindings.choose(condition, thenValue, Const.of(otherwiseValue)); + } + + /// Completes the binding with a constant for the false branch. + public NumberBinding otherwise(float otherwiseValue) { + return Bindings.choose(condition, thenValue, Const.of(otherwiseValue)); + } + + /// Completes the binding with a constant for the false branch. + public NumberBinding otherwise(long otherwiseValue) { + return Bindings.choose(condition, thenValue, Const.of(otherwiseValue)); + } + + /// Completes the binding with a constant for the false branch. + public NumberBinding otherwise(int otherwiseValue) { + return Bindings.choose(condition, thenValue, Const.of(otherwiseValue)); + } + } + + /// A ternary binding with a boolean true branch, waiting for the other + /// one. + public class BooleanConditionBuilder { + + private final ObservableBooleanValue thenValue; + private final boolean thenConstant; + + private BooleanConditionBuilder(final ObservableBooleanValue thenValue, boolean thenConstant) { + this.thenValue = thenValue; + this.thenConstant = thenConstant; + } + + /// Completes the binding with a boolean for the false branch. + public BooleanBinding otherwise(final ObservableBooleanValue otherwiseValue) { + return booleans(condition, thenValue, thenConstant, required(otherwiseValue), false); + } + + /// Completes the binding with a constant for the false branch. + public BooleanBinding otherwise(final boolean otherwiseValue) { + return booleans(condition, thenValue, thenConstant, null, otherwiseValue); + } + } + + /// A ternary binding with a string true branch, waiting for the other + /// one. + public class StringConditionBuilder { + + private final ObservableStringValue thenValue; + private final String thenConstant; + + private StringConditionBuilder(final ObservableStringValue thenValue, String thenConstant) { + this.thenValue = thenValue; + this.thenConstant = thenConstant; + } + + /// Completes the binding with a string for the false branch. + public StringBinding otherwise(final ObservableStringValue otherwiseValue) { + return strings(condition, thenValue, thenConstant, required(otherwiseValue), null); + } + + /// Completes the binding with a constant for the false branch. + public StringBinding otherwise(final String otherwiseValue) { + return strings(condition, thenValue, thenConstant, null, otherwiseValue); + } + } + + /// A ternary binding with an object true branch, waiting for the other + /// one. + public class ObjectConditionBuilder { + + private final ObservableObjectValue thenValue; + private final T thenConstant; + + private ObjectConditionBuilder(final ObservableObjectValue thenValue, T thenConstant) { + this.thenValue = thenValue; + this.thenConstant = thenConstant; + } + + /// Completes the binding with an object for the false branch. + public ObjectBinding otherwise(final ObservableObjectValue otherwiseValue) { + return objects(condition, thenValue, thenConstant, required(otherwiseValue), null); + } + + /// Completes the binding with a constant for the false branch. + public ObjectBinding otherwise(final T otherwiseValue) { + return objects(condition, thenValue, thenConstant, null, otherwiseValue); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/binding/package-info.java b/maven/javafx-compat/src/main/java/javafx/beans/binding/package-info.java new file mode 100644 index 00000000000..1ec66068b7e --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/binding/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Bindings: values computed from other observable values and recomputed +/// lazily when those change, the fluent expression classes that build them +/// and the [javafx.beans.binding.Bindings] operators. +package javafx.beans.binding; diff --git a/maven/javafx-compat/src/main/java/javafx/beans/package-info.java b/maven/javafx-compat/src/main/java/javafx/beans/package-info.java new file mode 100644 index 00000000000..fa25501a89a --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The base of the observable model: [javafx.beans.Observable], its +/// invalidation listener and the annotations declarative loaders read. +package javafx.beans; diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/BooleanProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/BooleanProperty.java new file mode 100644 index 00000000000..5431909328b --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/BooleanProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.BidirectionalBinding; +import com.codename1.fxcompat.runtime.PropertyText; +import java.util.function.Function; +import javafx.beans.binding.Bindings; +import javafx.beans.value.WritableBooleanValue; + +/// A property holding a boolean that can be read, written, observed and bound. +public abstract class BooleanProperty extends ReadOnlyBooleanProperty implements Property, WritableBooleanValue { + + /// Creates the property. + public BooleanProperty() { + } + + @Override + public void setValue(Boolean v) { + if (v == null) { + set(false); + } else { + set(v.booleanValue()); + } + } + + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /// Returns a boolean property kept in sync, in both directions, with a boxed + /// property. Neither keeps the other alive. + public static BooleanProperty booleanProperty(final Property property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + BooleanPropertyBase result = new BooleanPropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return property.getName(); + } + }; + BidirectionalBinding.bind(result, property, new Function() { + @Override + public Boolean apply(Boolean v) { + return v; + } + }, new Function() { + @Override + public Boolean apply(Boolean v) { + return Boolean.valueOf(v != null && v.booleanValue()); + } + }); + return result; + } + + /// Returns an object property holding the boxed value, kept in sync with + /// this one in both directions. + @Override + public ObjectProperty asObject() { + ObjectPropertyBase result = new ObjectPropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return BooleanProperty.this.getName(); + } + }; + Function box = v -> Boolean.valueOf(v != null && v.booleanValue()); + Function unbox = v -> v; + BidirectionalBinding.bind(result, this, box, unbox); + return result; + } + + @Override + public String toString() { + return PropertyText.describe("BooleanProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/BooleanPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/BooleanPropertyBase.java new file mode 100644 index 00000000000..bcd8ef4a005 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/BooleanPropertyBase.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import com.codename1.fxcompat.runtime.ValueListeners; +import java.lang.ref.WeakReference; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableValue; + +/// The full implementation of a boolean property except for its bean and name. +/// +/// Invalidation is lazy: after a change, listeners are told once and not +/// again until the value has been read. A change listener reads the value on +/// every invalidation, so registering one makes the property eager. +public abstract class BooleanPropertyBase extends BooleanProperty { + + private boolean value; + private ObservableValue observable; + private InvalidationListener listener; + private boolean valid = true; + private ValueListeners helper; + + /// Creates the property with the default value. + public BooleanPropertyBase() { + } + + /// Creates the property with an initial value. + public BooleanPropertyBase(boolean initialValue) { + this.value = initialValue; + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /// Called when the property turns invalid, before listeners are told. + protected void invalidated() { + } + + @Override + public boolean get() { + valid = true; + return observable == null ? value : boundValue(); + } + + private boolean boundValue() { + if (observable instanceof ObservableBooleanValue) { + return ((ObservableBooleanValue) observable).get(); + } + Boolean v = observable.getValue(); + return v != null && v.booleanValue(); + } + + /// Sets the value. + /// + /// #### Throws + /// + /// - `RuntimeException`: when the property is bound + @Override + public void set(boolean newValue) { + if (isBound()) { + throw new RuntimeException(PropertyText.owner(this) + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + /// Makes this property follow another value until [#unbind()]. The + /// property observes it weakly, so being bound does not keep it alive. + @Override + public void bind(final ObservableValue newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /// Stops following the bound value and keeps the value it had. + @Override + public void unbind() { + if (observable != null) { + value = boundValue(); + observable.removeListener(listener); + observable = null; + } + } + + @Override + public String toString() { + String tail; + if (isBound()) { + tail = valid ? "bound, value: " + get() : "bound, invalid"; + } else { + tail = "value: " + get(); + } + return PropertyText.describe("BooleanProperty", this, tail); + } + + /// Forwards invalidations of the bound value without keeping the + /// property reachable from it. + private static final class Listener implements InvalidationListener, WeakListener { + private final WeakReference ref; + + Listener(BooleanPropertyBase property) { + this.ref = new WeakReference(property); + } + + @Override + public void invalidated(Observable source) { + Object property = ref.get(); + if (property instanceof BooleanPropertyBase) { + ((BooleanPropertyBase) property).markInvalid(); + } else { + source.removeListener(this); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/DoubleProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/DoubleProperty.java new file mode 100644 index 00000000000..2241327b85a --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/DoubleProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.BidirectionalBinding; +import com.codename1.fxcompat.runtime.PropertyText; +import java.util.function.Function; +import javafx.beans.binding.Bindings; +import javafx.beans.value.WritableDoubleValue; + +/// A property holding a `double` that can be read, written, observed and bound. +public abstract class DoubleProperty extends ReadOnlyDoubleProperty implements Property, WritableDoubleValue { + + /// Creates the property. + public DoubleProperty() { + } + + @Override + public void setValue(Number v) { + if (v == null) { + set(0); + } else { + set(v.doubleValue()); + } + } + + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /// Returns a `double` property kept in sync, in both directions, with a boxed + /// property. Neither keeps the other alive. + public static DoubleProperty doubleProperty(final Property property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + DoublePropertyBase result = new DoublePropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return property.getName(); + } + }; + BidirectionalBinding.bind(result, property, new Function() { + @Override + public Number apply(Double v) { + return v; + } + }, new Function() { + @Override + public Double apply(Number v) { + return Double.valueOf(v == null ? 0 : v.doubleValue()); + } + }); + return result; + } + + /// Returns an object property holding the boxed value, kept in sync with + /// this one in both directions. + @Override + public ObjectProperty asObject() { + ObjectPropertyBase result = new ObjectPropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return DoubleProperty.this.getName(); + } + }; + Function box = v -> Double.valueOf(v == null ? 0 : v.doubleValue()); + Function unbox = v -> v; + BidirectionalBinding.bind(result, this, box, unbox); + return result; + } + + @Override + public String toString() { + return PropertyText.describe("DoubleProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/DoublePropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/DoublePropertyBase.java new file mode 100644 index 00000000000..f33440ba161 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/DoublePropertyBase.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import com.codename1.fxcompat.runtime.ValueListeners; +import java.lang.ref.WeakReference; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableDoubleValue; +import javafx.beans.value.ObservableValue; + +/// The full implementation of a `double` property except for its bean and name. +/// +/// Invalidation is lazy: after a change, listeners are told once and not +/// again until the value has been read. A change listener reads the value on +/// every invalidation, so registering one makes the property eager. +public abstract class DoublePropertyBase extends DoubleProperty { + + private double value; + private ObservableValue observable; + private InvalidationListener listener; + private boolean valid = true; + private ValueListeners helper; + + /// Creates the property with the default value. + public DoublePropertyBase() { + } + + /// Creates the property with an initial value. + public DoublePropertyBase(double initialValue) { + this.value = initialValue; + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /// Called when the property turns invalid, before listeners are told. + protected void invalidated() { + } + + @Override + public double get() { + valid = true; + return observable == null ? value : boundValue(); + } + + private double boundValue() { + if (observable instanceof ObservableDoubleValue) { + return ((ObservableDoubleValue) observable).get(); + } + Number v = observable.getValue(); + return v == null ? 0 : v.doubleValue(); + } + + /// Sets the value. + /// + /// #### Throws + /// + /// - `RuntimeException`: when the property is bound + @Override + public void set(double newValue) { + if (isBound()) { + throw new RuntimeException(PropertyText.owner(this) + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + /// Makes this property follow another value until [#unbind()]. The + /// property observes it weakly, so being bound does not keep it alive. + @Override + public void bind(final ObservableValue newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /// Stops following the bound value and keeps the value it had. + @Override + public void unbind() { + if (observable != null) { + value = boundValue(); + observable.removeListener(listener); + observable = null; + } + } + + @Override + public String toString() { + String tail; + if (isBound()) { + tail = valid ? "bound, value: " + get() : "bound, invalid"; + } else { + tail = "value: " + get(); + } + return PropertyText.describe("DoubleProperty", this, tail); + } + + /// Forwards invalidations of the bound value without keeping the + /// property reachable from it. + private static final class Listener implements InvalidationListener, WeakListener { + private final WeakReference ref; + + Listener(DoublePropertyBase property) { + this.ref = new WeakReference(property); + } + + @Override + public void invalidated(Observable source) { + Object property = ref.get(); + if (property instanceof DoublePropertyBase) { + ((DoublePropertyBase) property).markInvalid(); + } else { + source.removeListener(this); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/FloatProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/FloatProperty.java new file mode 100644 index 00000000000..8192d51b600 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/FloatProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.BidirectionalBinding; +import com.codename1.fxcompat.runtime.PropertyText; +import java.util.function.Function; +import javafx.beans.binding.Bindings; +import javafx.beans.value.WritableFloatValue; + +/// A property holding a `float` that can be read, written, observed and bound. +public abstract class FloatProperty extends ReadOnlyFloatProperty implements Property, WritableFloatValue { + + /// Creates the property. + public FloatProperty() { + } + + @Override + public void setValue(Number v) { + if (v == null) { + set(0); + } else { + set(v.floatValue()); + } + } + + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /// Returns a `float` property kept in sync, in both directions, with a boxed + /// property. Neither keeps the other alive. + public static FloatProperty floatProperty(final Property property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + FloatPropertyBase result = new FloatPropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return property.getName(); + } + }; + BidirectionalBinding.bind(result, property, new Function() { + @Override + public Number apply(Float v) { + return v; + } + }, new Function() { + @Override + public Float apply(Number v) { + return Float.valueOf(v == null ? 0 : v.floatValue()); + } + }); + return result; + } + + /// Returns an object property holding the boxed value, kept in sync with + /// this one in both directions. + @Override + public ObjectProperty asObject() { + ObjectPropertyBase result = new ObjectPropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return FloatProperty.this.getName(); + } + }; + Function box = v -> Float.valueOf(v == null ? 0 : v.floatValue()); + Function unbox = v -> v; + BidirectionalBinding.bind(result, this, box, unbox); + return result; + } + + @Override + public String toString() { + return PropertyText.describe("FloatProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/FloatPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/FloatPropertyBase.java new file mode 100644 index 00000000000..d64274627c9 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/FloatPropertyBase.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import com.codename1.fxcompat.runtime.ValueListeners; +import java.lang.ref.WeakReference; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableFloatValue; +import javafx.beans.value.ObservableValue; + +/// The full implementation of a `float` property except for its bean and name. +/// +/// Invalidation is lazy: after a change, listeners are told once and not +/// again until the value has been read. A change listener reads the value on +/// every invalidation, so registering one makes the property eager. +public abstract class FloatPropertyBase extends FloatProperty { + + private float value; + private ObservableValue observable; + private InvalidationListener listener; + private boolean valid = true; + private ValueListeners helper; + + /// Creates the property with the default value. + public FloatPropertyBase() { + } + + /// Creates the property with an initial value. + public FloatPropertyBase(float initialValue) { + this.value = initialValue; + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /// Called when the property turns invalid, before listeners are told. + protected void invalidated() { + } + + @Override + public float get() { + valid = true; + return observable == null ? value : boundValue(); + } + + private float boundValue() { + if (observable instanceof ObservableFloatValue) { + return ((ObservableFloatValue) observable).get(); + } + Number v = observable.getValue(); + return v == null ? 0 : v.floatValue(); + } + + /// Sets the value. + /// + /// #### Throws + /// + /// - `RuntimeException`: when the property is bound + @Override + public void set(float newValue) { + if (isBound()) { + throw new RuntimeException(PropertyText.owner(this) + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + /// Makes this property follow another value until [#unbind()]. The + /// property observes it weakly, so being bound does not keep it alive. + @Override + public void bind(final ObservableValue newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /// Stops following the bound value and keeps the value it had. + @Override + public void unbind() { + if (observable != null) { + value = boundValue(); + observable.removeListener(listener); + observable = null; + } + } + + @Override + public String toString() { + String tail; + if (isBound()) { + tail = valid ? "bound, value: " + get() : "bound, invalid"; + } else { + tail = "value: " + get(); + } + return PropertyText.describe("FloatProperty", this, tail); + } + + /// Forwards invalidations of the bound value without keeping the + /// property reachable from it. + private static final class Listener implements InvalidationListener, WeakListener { + private final WeakReference ref; + + Listener(FloatPropertyBase property) { + this.ref = new WeakReference(property); + } + + @Override + public void invalidated(Observable source) { + Object property = ref.get(); + if (property instanceof FloatPropertyBase) { + ((FloatPropertyBase) property).markInvalid(); + } else { + source.removeListener(this); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/IntegerProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/IntegerProperty.java new file mode 100644 index 00000000000..0418db78f3e --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/IntegerProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.BidirectionalBinding; +import com.codename1.fxcompat.runtime.PropertyText; +import java.util.function.Function; +import javafx.beans.binding.Bindings; +import javafx.beans.value.WritableIntegerValue; + +/// A property holding a `int` that can be read, written, observed and bound. +public abstract class IntegerProperty extends ReadOnlyIntegerProperty implements Property, WritableIntegerValue { + + /// Creates the property. + public IntegerProperty() { + } + + @Override + public void setValue(Number v) { + if (v == null) { + set(0); + } else { + set(v.intValue()); + } + } + + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /// Returns a `int` property kept in sync, in both directions, with a boxed + /// property. Neither keeps the other alive. + public static IntegerProperty integerProperty(final Property property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + IntegerPropertyBase result = new IntegerPropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return property.getName(); + } + }; + BidirectionalBinding.bind(result, property, new Function() { + @Override + public Number apply(Integer v) { + return v; + } + }, new Function() { + @Override + public Integer apply(Number v) { + return Integer.valueOf(v == null ? 0 : v.intValue()); + } + }); + return result; + } + + /// Returns an object property holding the boxed value, kept in sync with + /// this one in both directions. + @Override + public ObjectProperty asObject() { + ObjectPropertyBase result = new ObjectPropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return IntegerProperty.this.getName(); + } + }; + Function box = v -> Integer.valueOf(v == null ? 0 : v.intValue()); + Function unbox = v -> v; + BidirectionalBinding.bind(result, this, box, unbox); + return result; + } + + @Override + public String toString() { + return PropertyText.describe("IntegerProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/IntegerPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/IntegerPropertyBase.java new file mode 100644 index 00000000000..4ccf6fbbd91 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/IntegerPropertyBase.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import com.codename1.fxcompat.runtime.ValueListeners; +import java.lang.ref.WeakReference; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableIntegerValue; +import javafx.beans.value.ObservableValue; + +/// The full implementation of a `int` property except for its bean and name. +/// +/// Invalidation is lazy: after a change, listeners are told once and not +/// again until the value has been read. A change listener reads the value on +/// every invalidation, so registering one makes the property eager. +public abstract class IntegerPropertyBase extends IntegerProperty { + + private int value; + private ObservableValue observable; + private InvalidationListener listener; + private boolean valid = true; + private ValueListeners helper; + + /// Creates the property with the default value. + public IntegerPropertyBase() { + } + + /// Creates the property with an initial value. + public IntegerPropertyBase(int initialValue) { + this.value = initialValue; + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /// Called when the property turns invalid, before listeners are told. + protected void invalidated() { + } + + @Override + public int get() { + valid = true; + return observable == null ? value : boundValue(); + } + + private int boundValue() { + if (observable instanceof ObservableIntegerValue) { + return ((ObservableIntegerValue) observable).get(); + } + Number v = observable.getValue(); + return v == null ? 0 : v.intValue(); + } + + /// Sets the value. + /// + /// #### Throws + /// + /// - `RuntimeException`: when the property is bound + @Override + public void set(int newValue) { + if (isBound()) { + throw new RuntimeException(PropertyText.owner(this) + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + /// Makes this property follow another value until [#unbind()]. The + /// property observes it weakly, so being bound does not keep it alive. + @Override + public void bind(final ObservableValue newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /// Stops following the bound value and keeps the value it had. + @Override + public void unbind() { + if (observable != null) { + value = boundValue(); + observable.removeListener(listener); + observable = null; + } + } + + @Override + public String toString() { + String tail; + if (isBound()) { + tail = valid ? "bound, value: " + get() : "bound, invalid"; + } else { + tail = "value: " + get(); + } + return PropertyText.describe("IntegerProperty", this, tail); + } + + /// Forwards invalidations of the bound value without keeping the + /// property reachable from it. + private static final class Listener implements InvalidationListener, WeakListener { + private final WeakReference ref; + + Listener(IntegerPropertyBase property) { + this.ref = new WeakReference(property); + } + + @Override + public void invalidated(Observable source) { + Object property = ref.get(); + if (property instanceof IntegerPropertyBase) { + ((IntegerPropertyBase) property).markInvalid(); + } else { + source.removeListener(this); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ListProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ListProperty.java new file mode 100644 index 00000000000..eb239afedd4 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ListProperty.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; + +import javafx.beans.binding.Bindings; +import javafx.beans.value.WritableListValue; +import javafx.collections.ObservableList; + +/// A property holding an observable list that can be read, replaced, +/// observed and bound. +public abstract class ListProperty extends ReadOnlyListProperty + implements Property>, WritableListValue { + + /// Creates the property. + public ListProperty() { + } + + @Override + public void setValue(ObservableList v) { + set(v); + } + + @Override + public void bindBidirectional(Property> other) { + Bindings.bindBidirectional(this, other); + } + + @Override + public void unbindBidirectional(Property> other) { + Bindings.unbindBidirectional(this, other); + } + + @Override + public String toString() { + return PropertyText.describe("ListProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ListPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ListPropertyBase.java new file mode 100644 index 00000000000..8603b9dac47 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ListPropertyBase.java @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import com.codename1.fxcompat.runtime.Changes; +import com.codename1.fxcompat.runtime.ListChangeBuilder; +import com.codename1.fxcompat.runtime.ListenerSet; +import com.codename1.fxcompat.runtime.PropertyText; +import com.codename1.fxcompat.runtime.ValueListeners; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +/// The full implementation of a list property except for its bean and name. +/// +/// The property reports two kinds of change. Replacing the list it holds +/// tells the invalidation and change listeners, and tells the list +/// listeners that the whole content was replaced. A modification of the +/// list it holds tells the invalidation listeners and passes the report on +/// to the list listeners with this property as the list. +/// +/// The property follows the list it holds as soon as that list is set or +/// the bound value changes, so the size and empty properties and the list +/// listeners are always current. +public abstract class ListPropertyBase extends ListProperty { + + private ObservableList value; + private ObservableValue> observable; + private InvalidationListener listener; + private boolean valid = true; + private ValueListeners> helper; + private ListenerSet> listListeners; + private ObservableList watched; + private final ListChangeListener contentListener = new ListChangeListener() { + @Override + public void onChanged(ListChangeListener.Change change) { + contentChanged(change); + } + }; + private SizeProperty size0; + private EmptyProperty empty0; + + /// Creates the property holding no list. + public ListPropertyBase() { + } + + /// Creates the property with an initial list. + public ListPropertyBase(ObservableList initialValue) { + this.value = initialValue; + this.watched = initialValue; + if (initialValue != null) { + initialValue.addListener(contentListener); + } + } + + @Override + public ReadOnlyIntegerProperty sizeProperty() { + if (size0 == null) { + size0 = new SizeProperty(this); + } + return size0; + } + + @Override + public ReadOnlyBooleanProperty emptyProperty() { + if (empty0 == null) { + empty0 = new EmptyProperty(this); + } + return empty0; + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener> listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener> listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ListChangeListener listener) { + listListeners = ListenerSet.addChange(listListeners, listener); + } + + @Override + public void removeListener(ListChangeListener listener) { + listListeners = ListenerSet.removeChange(listListeners, listener); + } + + /// Tells the invalidation and change listeners the held list may have + /// been replaced. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } + + /// Tells the list listeners about a change of the content. + @SuppressWarnings({"unchecked", "rawtypes"}) + protected void fireValueChangedEvent(ListChangeListener.Change change) { + List> targets = ListenerSet.changeListeners(listListeners); + for (int i = 0; i < targets.size(); i++) { + change.reset(); + ((ListChangeListener) targets.get(i)).onChanged(change); + } + } + + /// Called when the held list was replaced or modified, before listeners + /// are told. + protected void invalidated() { + } + + private void contentChanged(ListChangeListener.Change change) { + fireDerived(); + invalidated(); + fireValueChangedEvent(); + fireValueChangedEvent(new Changes.Relayed(this, change)); + } + + private void fireDerived() { + if (size0 != null) { + size0.changed(); + } + if (empty0 != null) { + empty0.changed(); + } + } + + private ObservableList current() { + return observable == null ? value : observable.getValue(); + } + + private void markInvalid() { + ObservableList before = watched; + ObservableList now = current(); + if (before != now) { + if (before != null) { + before.removeListener(contentListener); + } + watched = now; + if (now != null) { + now.addListener(contentListener); + } + } + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + if (before != now) { + fireDerived(); + List removed = before == null ? new ArrayList() : new ArrayList(before); + int added = now == null ? 0 : now.size(); + if (!removed.isEmpty() || added > 0) { + ListChangeBuilder builder = new ListChangeBuilder(this); + builder.beginChange(); + builder.nextReplace(0, added, removed); + ListChangeListener.Change change = builder.endChange(); + if (change != null) { + fireValueChangedEvent(change); + } + } + } + } + + @Override + public ObservableList get() { + valid = true; + return current(); + } + + /// Replaces the held list. + /// + /// #### Throws + /// + /// - `RuntimeException`: when the property is bound + @Override + public void set(ObservableList newValue) { + if (isBound()) { + throw new RuntimeException(PropertyText.owner(this) + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + /// Makes this property follow another value until [#unbind()]. The + /// property observes it weakly, so being bound does not keep it alive. + @Override + public void bind(final ObservableValue> newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + if (!sameObservable(newObservable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /// An observable list value compares as a list, so the bound value is + /// recognized by identity. + private boolean sameObservable(Object candidate) { + return candidate == observable; + } + + /// Stops following the bound value and keeps the list it held. + @Override + public void unbind() { + if (observable != null) { + value = observable.getValue(); + observable.removeListener(listener); + observable = null; + } + } + + @Override + public String toString() { + String tail; + if (isBound()) { + tail = valid ? "bound, value: " + get() : "bound, invalid"; + } else { + tail = "value: " + get(); + } + return PropertyText.describe("ListProperty", this, tail); + } + + /// The number of elements of a list property. + private static final class SizeProperty extends ReadOnlyIntegerPropertyBase { + private final ListPropertyBase owner; + + SizeProperty(ListPropertyBase owner) { + this.owner = owner; + } + + void changed() { + fireValueChangedEvent(); + } + + @Override + public int get() { + return owner.size(); + } + + @Override + public Object getBean() { + return owner; + } + + @Override + public String getName() { + return "size"; + } + } + + /// Whether a list property is empty. + private static final class EmptyProperty extends ReadOnlyBooleanPropertyBase { + private final ListPropertyBase owner; + + EmptyProperty(ListPropertyBase owner) { + this.owner = owner; + } + + void changed() { + fireValueChangedEvent(); + } + + @Override + public boolean get() { + return owner.isEmpty(); + } + + @Override + public Object getBean() { + return owner; + } + + @Override + public String getName() { + return "empty"; + } + } + + /// Forwards invalidations of the bound value without keeping the + /// property reachable from it. + private static final class Listener implements InvalidationListener, WeakListener { + private final WeakReference ref; + + Listener(ListPropertyBase property) { + this.ref = new WeakReference(property); + } + + @Override + public void invalidated(Observable source) { + Object property = ref.get(); + if (property instanceof ListPropertyBase) { + ((ListPropertyBase) property).markInvalid(); + } else { + source.removeListener(this); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/LongProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/LongProperty.java new file mode 100644 index 00000000000..b7b6bc9f956 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/LongProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.BidirectionalBinding; +import com.codename1.fxcompat.runtime.PropertyText; +import java.util.function.Function; +import javafx.beans.binding.Bindings; +import javafx.beans.value.WritableLongValue; + +/// A property holding a `long` that can be read, written, observed and bound. +public abstract class LongProperty extends ReadOnlyLongProperty implements Property, WritableLongValue { + + /// Creates the property. + public LongProperty() { + } + + @Override + public void setValue(Number v) { + if (v == null) { + set(0); + } else { + set(v.longValue()); + } + } + + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /// Returns a `long` property kept in sync, in both directions, with a boxed + /// property. Neither keeps the other alive. + public static LongProperty longProperty(final Property property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + LongPropertyBase result = new LongPropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return property.getName(); + } + }; + BidirectionalBinding.bind(result, property, new Function() { + @Override + public Number apply(Long v) { + return v; + } + }, new Function() { + @Override + public Long apply(Number v) { + return Long.valueOf(v == null ? 0 : v.longValue()); + } + }); + return result; + } + + /// Returns an object property holding the boxed value, kept in sync with + /// this one in both directions. + @Override + public ObjectProperty asObject() { + ObjectPropertyBase result = new ObjectPropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return LongProperty.this.getName(); + } + }; + Function box = v -> Long.valueOf(v == null ? 0 : v.longValue()); + Function unbox = v -> v; + BidirectionalBinding.bind(result, this, box, unbox); + return result; + } + + @Override + public String toString() { + return PropertyText.describe("LongProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/LongPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/LongPropertyBase.java new file mode 100644 index 00000000000..e6ff4104e4e --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/LongPropertyBase.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import com.codename1.fxcompat.runtime.ValueListeners; +import java.lang.ref.WeakReference; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableLongValue; +import javafx.beans.value.ObservableValue; + +/// The full implementation of a `long` property except for its bean and name. +/// +/// Invalidation is lazy: after a change, listeners are told once and not +/// again until the value has been read. A change listener reads the value on +/// every invalidation, so registering one makes the property eager. +public abstract class LongPropertyBase extends LongProperty { + + private long value; + private ObservableValue observable; + private InvalidationListener listener; + private boolean valid = true; + private ValueListeners helper; + + /// Creates the property with the default value. + public LongPropertyBase() { + } + + /// Creates the property with an initial value. + public LongPropertyBase(long initialValue) { + this.value = initialValue; + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /// Called when the property turns invalid, before listeners are told. + protected void invalidated() { + } + + @Override + public long get() { + valid = true; + return observable == null ? value : boundValue(); + } + + private long boundValue() { + if (observable instanceof ObservableLongValue) { + return ((ObservableLongValue) observable).get(); + } + Number v = observable.getValue(); + return v == null ? 0 : v.longValue(); + } + + /// Sets the value. + /// + /// #### Throws + /// + /// - `RuntimeException`: when the property is bound + @Override + public void set(long newValue) { + if (isBound()) { + throw new RuntimeException(PropertyText.owner(this) + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + /// Makes this property follow another value until [#unbind()]. The + /// property observes it weakly, so being bound does not keep it alive. + @Override + public void bind(final ObservableValue newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /// Stops following the bound value and keeps the value it had. + @Override + public void unbind() { + if (observable != null) { + value = boundValue(); + observable.removeListener(listener); + observable = null; + } + } + + @Override + public String toString() { + String tail; + if (isBound()) { + tail = valid ? "bound, value: " + get() : "bound, invalid"; + } else { + tail = "value: " + get(); + } + return PropertyText.describe("LongProperty", this, tail); + } + + /// Forwards invalidations of the bound value without keeping the + /// property reachable from it. + private static final class Listener implements InvalidationListener, WeakListener { + private final WeakReference ref; + + Listener(LongPropertyBase property) { + this.ref = new WeakReference(property); + } + + @Override + public void invalidated(Observable source) { + Object property = ref.get(); + if (property instanceof LongPropertyBase) { + ((LongPropertyBase) property).markInvalid(); + } else { + source.removeListener(this); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ObjectProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ObjectProperty.java new file mode 100644 index 00000000000..f3d1b4cc440 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ObjectProperty.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import javafx.beans.binding.Bindings; +import javafx.beans.value.WritableObjectValue; + +/// A property holding an object that can be read, written, observed and bound. +public abstract class ObjectProperty extends ReadOnlyObjectProperty implements Property, WritableObjectValue { + + /// Creates the property. + public ObjectProperty() { + } + + @Override + public void setValue(T v) { + set(v); + } + + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + @Override + public String toString() { + return PropertyText.describe("ObjectProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ObjectPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ObjectPropertyBase.java new file mode 100644 index 00000000000..f6aae6a6dd6 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ObjectPropertyBase.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import com.codename1.fxcompat.runtime.ValueListeners; +import java.lang.ref.WeakReference; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; + +/// The full implementation of an object property except for its bean and name. +/// +/// Invalidation is lazy: after a change, listeners are told once and not +/// again until the value has been read. A change listener reads the value on +/// every invalidation, so registering one makes the property eager. +public abstract class ObjectPropertyBase extends ObjectProperty { + + private T value; + private ObservableValue observable; + private InvalidationListener listener; + private boolean valid = true; + private ValueListeners helper; + + /// Creates the property with the default value. + public ObjectPropertyBase() { + } + + /// Creates the property with an initial value. + public ObjectPropertyBase(T initialValue) { + this.value = initialValue; + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /// Called when the property turns invalid, before listeners are told. + protected void invalidated() { + } + + @Override + public T get() { + valid = true; + return observable == null ? value : boundValue(); + } + + private T boundValue() { + T v = observable.getValue(); + return v; + } + + /// Sets the value. + /// + /// #### Throws + /// + /// - `RuntimeException`: when the property is bound + @Override + public void set(T newValue) { + if (isBound()) { + throw new RuntimeException(PropertyText.owner(this) + "A bound value cannot be set."); + } + if (value != newValue) { + value = newValue; + markInvalid(); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + /// Makes this property follow another value until [#unbind()]. The + /// property observes it weakly, so being bound does not keep it alive. + @Override + public void bind(final ObservableValue newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /// Stops following the bound value and keeps the value it had. + @Override + public void unbind() { + if (observable != null) { + value = boundValue(); + observable.removeListener(listener); + observable = null; + } + } + + @Override + public String toString() { + String tail; + if (isBound()) { + tail = valid ? "bound, value: " + get() : "bound, invalid"; + } else { + tail = "value: " + get(); + } + return PropertyText.describe("ObjectProperty", this, tail); + } + + /// Forwards invalidations of the bound value without keeping the + /// property reachable from it. + private static final class Listener implements InvalidationListener, WeakListener { + private final WeakReference ref; + + Listener(ObjectPropertyBase property) { + this.ref = new WeakReference(property); + } + + @Override + public void invalidated(Observable source) { + Object property = ref.get(); + if (property instanceof ObjectPropertyBase) { + ((ObjectPropertyBase) property).markInvalid(); + } else { + source.removeListener(this); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/Property.java b/maven/javafx-compat/src/main/java/javafx/beans/property/Property.java new file mode 100644 index 00000000000..367c0691fb5 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/Property.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import javafx.beans.value.ObservableValue; +import javafx.beans.value.WritableValue; + +/// A value that can be written, observed and bound to other values. +/// +/// A property bound with [#bind(ObservableValue)] follows its source and +/// rejects every attempt to set it. Two properties bound with +/// [#bindBidirectional(Property)] follow each other and both stay writable. +public interface Property extends ReadOnlyProperty, WritableValue { + + /// Makes this property follow another value. + void bind(ObservableValue observable); + + /// Stops following the bound value; does nothing when not bound. + void unbind(); + + /// Returns whether the property currently follows another value. + boolean isBound(); + + /// Keeps this property and another one equal, starting with the value + /// of the other. + void bindBidirectional(Property other); + + /// Removes a bidirectional binding with another property. + void unbindBidirectional(Property other); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanProperty.java new file mode 100644 index 00000000000..15c3b54a897 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.binding.BooleanExpression; + +/// A property holding a boolean that can be read and observed but not written. +public abstract class ReadOnlyBooleanProperty extends BooleanExpression implements ReadOnlyProperty { + + /// Creates the property. + public ReadOnlyBooleanProperty() { + } + + /// Returns a read-only view of a boxed property as a boolean property. A `null` + /// value reads as the default value. + public static ReadOnlyBooleanProperty readOnlyBooleanProperty(final ReadOnlyProperty property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + if (property instanceof ReadOnlyBooleanProperty) { + return (ReadOnlyBooleanProperty) property; + } + return new ReadOnlyBooleanPropertyBase() { + private final InvalidationListener listener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + fireValueChangedEvent(); + } + }; + + { + property.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public boolean get() { + Boolean v = property.getValue(); + return v != null && v.booleanValue(); + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + /// Returns a read-only object property holding the boxed value of this one. + @Override + public ReadOnlyObjectProperty asObject() { + return new ReadOnlyObjectPropertyBase() { + private final InvalidationListener listener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + fireValueChangedEvent(); + } + }; + + { + ReadOnlyBooleanProperty.this.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public Boolean get() { + return ReadOnlyBooleanProperty.this.getValue(); + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return ReadOnlyBooleanProperty.this.getName(); + } + }; + } + + @Override + public String toString() { + return PropertyText.describe("ReadOnlyBooleanProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanPropertyBase.java new file mode 100644 index 00000000000..61a7b081b02 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanPropertyBase.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; + +/// Base class for a read-only property holding a boolean: it keeps the listeners +/// and leaves the value to the subclass, which calls +/// [#fireValueChangedEvent()] whenever that value may have changed. +public abstract class ReadOnlyBooleanPropertyBase extends ReadOnlyBooleanProperty { + + private ValueListeners helper; + + /// Creates the property. + public ReadOnlyBooleanPropertyBase() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanWrapper.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanWrapper.java new file mode 100644 index 00000000000..93c6046876e --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyBooleanWrapper.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A writable property that hands out a read-only view of itself: the owner +/// keeps the wrapper and writes to it, and publishes +/// [#getReadOnlyProperty()], which always shows the same value. +public class ReadOnlyBooleanWrapper extends SimpleBooleanProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /// Creates a wrapper with no bean, an empty name and the default value. + public ReadOnlyBooleanWrapper() { + } + + /// Creates a wrapper with no bean and an empty name. + public ReadOnlyBooleanWrapper(boolean initialValue) { + super(initialValue); + } + + /// Creates a wrapper of a bean with the default value. + public ReadOnlyBooleanWrapper(Object bean, String name) { + super(bean, name); + } + + /// Creates a wrapper of a bean. + public ReadOnlyBooleanWrapper(Object bean, String name, boolean initialValue) { + super(bean, name, initialValue); + } + + /// Returns the read-only view; the same object on every call. + public ReadOnlyBooleanProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private final class ReadOnlyPropertyImpl extends ReadOnlyBooleanPropertyBase { + + @Override + public boolean get() { + return ReadOnlyBooleanWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyBooleanWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyBooleanWrapper.this.getName(); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoubleProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoubleProperty.java new file mode 100644 index 00000000000..88955d393fd --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoubleProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.binding.DoubleExpression; + +/// A property holding a `double` that can be read and observed but not written. +public abstract class ReadOnlyDoubleProperty extends DoubleExpression implements ReadOnlyProperty { + + /// Creates the property. + public ReadOnlyDoubleProperty() { + } + + /// Returns a read-only view of a boxed property as a `double` property. A `null` + /// value reads as the default value. + public static ReadOnlyDoubleProperty readOnlyDoubleProperty(final ReadOnlyProperty property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + if (property instanceof ReadOnlyDoubleProperty) { + return (ReadOnlyDoubleProperty) property; + } + return new ReadOnlyDoublePropertyBase() { + private final InvalidationListener listener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + fireValueChangedEvent(); + } + }; + + { + property.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public double get() { + T v = property.getValue(); + return v == null ? 0 : v.doubleValue(); + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + /// Returns a read-only object property holding the boxed value of this one. + @Override + public ReadOnlyObjectProperty asObject() { + return new ReadOnlyObjectPropertyBase() { + private final InvalidationListener listener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + fireValueChangedEvent(); + } + }; + + { + ReadOnlyDoubleProperty.this.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public Double get() { + return ReadOnlyDoubleProperty.this.getValue(); + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return ReadOnlyDoubleProperty.this.getName(); + } + }; + } + + @Override + public String toString() { + return PropertyText.describe("ReadOnlyDoubleProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoublePropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoublePropertyBase.java new file mode 100644 index 00000000000..067bc3fccbb --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoublePropertyBase.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; + +/// Base class for a read-only property holding a `double`: it keeps the listeners +/// and leaves the value to the subclass, which calls +/// [#fireValueChangedEvent()] whenever that value may have changed. +public abstract class ReadOnlyDoublePropertyBase extends ReadOnlyDoubleProperty { + + private ValueListeners helper; + + /// Creates the property. + public ReadOnlyDoublePropertyBase() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoubleWrapper.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoubleWrapper.java new file mode 100644 index 00000000000..d200630449a --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyDoubleWrapper.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A writable property that hands out a read-only view of itself: the owner +/// keeps the wrapper and writes to it, and publishes +/// [#getReadOnlyProperty()], which always shows the same value. +public class ReadOnlyDoubleWrapper extends SimpleDoubleProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /// Creates a wrapper with no bean, an empty name and the default value. + public ReadOnlyDoubleWrapper() { + } + + /// Creates a wrapper with no bean and an empty name. + public ReadOnlyDoubleWrapper(double initialValue) { + super(initialValue); + } + + /// Creates a wrapper of a bean with the default value. + public ReadOnlyDoubleWrapper(Object bean, String name) { + super(bean, name); + } + + /// Creates a wrapper of a bean. + public ReadOnlyDoubleWrapper(Object bean, String name, double initialValue) { + super(bean, name, initialValue); + } + + /// Returns the read-only view; the same object on every call. + public ReadOnlyDoubleProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private final class ReadOnlyPropertyImpl extends ReadOnlyDoublePropertyBase { + + @Override + public double get() { + return ReadOnlyDoubleWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyDoubleWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyDoubleWrapper.this.getName(); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatProperty.java new file mode 100644 index 00000000000..17509786d68 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.binding.FloatExpression; + +/// A property holding a `float` that can be read and observed but not written. +public abstract class ReadOnlyFloatProperty extends FloatExpression implements ReadOnlyProperty { + + /// Creates the property. + public ReadOnlyFloatProperty() { + } + + /// Returns a read-only view of a boxed property as a `float` property. A `null` + /// value reads as the default value. + public static ReadOnlyFloatProperty readOnlyFloatProperty(final ReadOnlyProperty property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + if (property instanceof ReadOnlyFloatProperty) { + return (ReadOnlyFloatProperty) property; + } + return new ReadOnlyFloatPropertyBase() { + private final InvalidationListener listener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + fireValueChangedEvent(); + } + }; + + { + property.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public float get() { + T v = property.getValue(); + return v == null ? 0 : v.floatValue(); + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + /// Returns a read-only object property holding the boxed value of this one. + @Override + public ReadOnlyObjectProperty asObject() { + return new ReadOnlyObjectPropertyBase() { + private final InvalidationListener listener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + fireValueChangedEvent(); + } + }; + + { + ReadOnlyFloatProperty.this.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public Float get() { + return ReadOnlyFloatProperty.this.getValue(); + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return ReadOnlyFloatProperty.this.getName(); + } + }; + } + + @Override + public String toString() { + return PropertyText.describe("ReadOnlyFloatProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatPropertyBase.java new file mode 100644 index 00000000000..74bbfcbfa16 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatPropertyBase.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; + +/// Base class for a read-only property holding a `float`: it keeps the listeners +/// and leaves the value to the subclass, which calls +/// [#fireValueChangedEvent()] whenever that value may have changed. +public abstract class ReadOnlyFloatPropertyBase extends ReadOnlyFloatProperty { + + private ValueListeners helper; + + /// Creates the property. + public ReadOnlyFloatPropertyBase() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatWrapper.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatWrapper.java new file mode 100644 index 00000000000..b4194939d54 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyFloatWrapper.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A writable property that hands out a read-only view of itself: the owner +/// keeps the wrapper and writes to it, and publishes +/// [#getReadOnlyProperty()], which always shows the same value. +public class ReadOnlyFloatWrapper extends SimpleFloatProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /// Creates a wrapper with no bean, an empty name and the default value. + public ReadOnlyFloatWrapper() { + } + + /// Creates a wrapper with no bean and an empty name. + public ReadOnlyFloatWrapper(float initialValue) { + super(initialValue); + } + + /// Creates a wrapper of a bean with the default value. + public ReadOnlyFloatWrapper(Object bean, String name) { + super(bean, name); + } + + /// Creates a wrapper of a bean. + public ReadOnlyFloatWrapper(Object bean, String name, float initialValue) { + super(bean, name, initialValue); + } + + /// Returns the read-only view; the same object on every call. + public ReadOnlyFloatProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private final class ReadOnlyPropertyImpl extends ReadOnlyFloatPropertyBase { + + @Override + public float get() { + return ReadOnlyFloatWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyFloatWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyFloatWrapper.this.getName(); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerProperty.java new file mode 100644 index 00000000000..6212173cd73 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.binding.IntegerExpression; + +/// A property holding a `int` that can be read and observed but not written. +public abstract class ReadOnlyIntegerProperty extends IntegerExpression implements ReadOnlyProperty { + + /// Creates the property. + public ReadOnlyIntegerProperty() { + } + + /// Returns a read-only view of a boxed property as a `int` property. A `null` + /// value reads as the default value. + public static ReadOnlyIntegerProperty readOnlyIntegerProperty(final ReadOnlyProperty property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + if (property instanceof ReadOnlyIntegerProperty) { + return (ReadOnlyIntegerProperty) property; + } + return new ReadOnlyIntegerPropertyBase() { + private final InvalidationListener listener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + fireValueChangedEvent(); + } + }; + + { + property.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public int get() { + T v = property.getValue(); + return v == null ? 0 : v.intValue(); + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + /// Returns a read-only object property holding the boxed value of this one. + @Override + public ReadOnlyObjectProperty asObject() { + return new ReadOnlyObjectPropertyBase() { + private final InvalidationListener listener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + fireValueChangedEvent(); + } + }; + + { + ReadOnlyIntegerProperty.this.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public Integer get() { + return ReadOnlyIntegerProperty.this.getValue(); + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return ReadOnlyIntegerProperty.this.getName(); + } + }; + } + + @Override + public String toString() { + return PropertyText.describe("ReadOnlyIntegerProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerPropertyBase.java new file mode 100644 index 00000000000..c79df8db669 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerPropertyBase.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; + +/// Base class for a read-only property holding a `int`: it keeps the listeners +/// and leaves the value to the subclass, which calls +/// [#fireValueChangedEvent()] whenever that value may have changed. +public abstract class ReadOnlyIntegerPropertyBase extends ReadOnlyIntegerProperty { + + private ValueListeners helper; + + /// Creates the property. + public ReadOnlyIntegerPropertyBase() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerWrapper.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerWrapper.java new file mode 100644 index 00000000000..30de24efa82 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyIntegerWrapper.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A writable property that hands out a read-only view of itself: the owner +/// keeps the wrapper and writes to it, and publishes +/// [#getReadOnlyProperty()], which always shows the same value. +public class ReadOnlyIntegerWrapper extends SimpleIntegerProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /// Creates a wrapper with no bean, an empty name and the default value. + public ReadOnlyIntegerWrapper() { + } + + /// Creates a wrapper with no bean and an empty name. + public ReadOnlyIntegerWrapper(int initialValue) { + super(initialValue); + } + + /// Creates a wrapper of a bean with the default value. + public ReadOnlyIntegerWrapper(Object bean, String name) { + super(bean, name); + } + + /// Creates a wrapper of a bean. + public ReadOnlyIntegerWrapper(Object bean, String name, int initialValue) { + super(bean, name, initialValue); + } + + /// Returns the read-only view; the same object on every call. + public ReadOnlyIntegerProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private final class ReadOnlyPropertyImpl extends ReadOnlyIntegerPropertyBase { + + @Override + public int get() { + return ReadOnlyIntegerWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyIntegerWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyIntegerWrapper.this.getName(); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyListProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyListProperty.java new file mode 100644 index 00000000000..f022e915ba0 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyListProperty.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import java.util.List; +import java.util.ListIterator; + +import com.codename1.fxcompat.runtime.PropertyText; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.ListExpression; +import javafx.collections.ObservableList; + +/// A property holding an observable list that can be read and observed but +/// not replaced. +public abstract class ReadOnlyListProperty extends ListExpression + implements ReadOnlyProperty> { + + /// Creates the property. + public ReadOnlyListProperty() { + } + + /// Keeps the content of this property and of a list equal in both + /// directions. + public void bindContentBidirectional(ObservableList list) { + Bindings.bindContentBidirectional(this, list); + } + + /// Removes a bidirectional content binding. + public void unbindContentBidirectional(Object object) { + Bindings.unbindContentBidirectional(this, object); + } + + /// Makes the content of this property follow a list. + public void bindContent(ObservableList list) { + Bindings.bindContent(this, list); + } + + /// Removes a content binding. + public void unbindContent(Object object) { + Bindings.unbindContent(this, object); + } + + /// Compares as a list: equal to any list of equal elements in the same + /// order. + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof List)) { + return false; + } + List other = (List) obj; + if (size() != other.size()) { + return false; + } + ListIterator mine = listIterator(); + ListIterator theirs = other.listIterator(); + while (mine.hasNext() && theirs.hasNext()) { + E first = mine.next(); + Object second = theirs.next(); + if (!(first == null ? second == null : first.equals(second))) { + return false; + } + } + return true; + } + + /// Returns the hash code of the content, as a list defines it. + @Override + public int hashCode() { + int hashCode = 1; + for (E element : this) { + hashCode = 31 * hashCode + (element == null ? 0 : element.hashCode()); + } + return hashCode; + } + + @Override + public String toString() { + return PropertyText.describe("ReadOnlyListProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongProperty.java new file mode 100644 index 00000000000..de5b0c8b943 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.binding.LongExpression; + +/// A property holding a `long` that can be read and observed but not written. +public abstract class ReadOnlyLongProperty extends LongExpression implements ReadOnlyProperty { + + /// Creates the property. + public ReadOnlyLongProperty() { + } + + /// Returns a read-only view of a boxed property as a `long` property. A `null` + /// value reads as the default value. + public static ReadOnlyLongProperty readOnlyLongProperty(final ReadOnlyProperty property) { + if (property == null) { + throw new NullPointerException("Property cannot be null"); + } + if (property instanceof ReadOnlyLongProperty) { + return (ReadOnlyLongProperty) property; + } + return new ReadOnlyLongPropertyBase() { + private final InvalidationListener listener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + fireValueChangedEvent(); + } + }; + + { + property.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public long get() { + T v = property.getValue(); + return v == null ? 0 : v.longValue(); + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return property.getName(); + } + }; + } + + /// Returns a read-only object property holding the boxed value of this one. + @Override + public ReadOnlyObjectProperty asObject() { + return new ReadOnlyObjectPropertyBase() { + private final InvalidationListener listener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + fireValueChangedEvent(); + } + }; + + { + ReadOnlyLongProperty.this.addListener(new WeakInvalidationListener(listener)); + } + + @Override + public Long get() { + return ReadOnlyLongProperty.this.getValue(); + } + + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return ReadOnlyLongProperty.this.getName(); + } + }; + } + + @Override + public String toString() { + return PropertyText.describe("ReadOnlyLongProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongPropertyBase.java new file mode 100644 index 00000000000..28c2eb36cac --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongPropertyBase.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; + +/// Base class for a read-only property holding a `long`: it keeps the listeners +/// and leaves the value to the subclass, which calls +/// [#fireValueChangedEvent()] whenever that value may have changed. +public abstract class ReadOnlyLongPropertyBase extends ReadOnlyLongProperty { + + private ValueListeners helper; + + /// Creates the property. + public ReadOnlyLongPropertyBase() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongWrapper.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongWrapper.java new file mode 100644 index 00000000000..4b4bf63694d --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyLongWrapper.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A writable property that hands out a read-only view of itself: the owner +/// keeps the wrapper and writes to it, and publishes +/// [#getReadOnlyProperty()], which always shows the same value. +public class ReadOnlyLongWrapper extends SimpleLongProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /// Creates a wrapper with no bean, an empty name and the default value. + public ReadOnlyLongWrapper() { + } + + /// Creates a wrapper with no bean and an empty name. + public ReadOnlyLongWrapper(long initialValue) { + super(initialValue); + } + + /// Creates a wrapper of a bean with the default value. + public ReadOnlyLongWrapper(Object bean, String name) { + super(bean, name); + } + + /// Creates a wrapper of a bean. + public ReadOnlyLongWrapper(Object bean, String name, long initialValue) { + super(bean, name, initialValue); + } + + /// Returns the read-only view; the same object on every call. + public ReadOnlyLongProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private final class ReadOnlyPropertyImpl extends ReadOnlyLongPropertyBase { + + @Override + public long get() { + return ReadOnlyLongWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyLongWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyLongWrapper.this.getName(); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectProperty.java new file mode 100644 index 00000000000..33cb96e4518 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectProperty.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.binding.ObjectExpression; + +/// A property holding an object that can be read and observed but not written. +public abstract class ReadOnlyObjectProperty extends ObjectExpression implements ReadOnlyProperty { + + /// Creates the property. + public ReadOnlyObjectProperty() { + } + + @Override + public String toString() { + return PropertyText.describe("ReadOnlyObjectProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectPropertyBase.java new file mode 100644 index 00000000000..217f498ff13 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectPropertyBase.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; + +/// Base class for a read-only property holding an object: it keeps the listeners +/// and leaves the value to the subclass, which calls +/// [#fireValueChangedEvent()] whenever that value may have changed. +public abstract class ReadOnlyObjectPropertyBase extends ReadOnlyObjectProperty { + + private ValueListeners helper; + + /// Creates the property. + public ReadOnlyObjectPropertyBase() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectWrapper.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectWrapper.java new file mode 100644 index 00000000000..59644c9bf1f --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyObjectWrapper.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A writable property that hands out a read-only view of itself: the owner +/// keeps the wrapper and writes to it, and publishes +/// [#getReadOnlyProperty()], which always shows the same value. +public class ReadOnlyObjectWrapper extends SimpleObjectProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /// Creates a wrapper with no bean, an empty name and the default value. + public ReadOnlyObjectWrapper() { + } + + /// Creates a wrapper with no bean and an empty name. + public ReadOnlyObjectWrapper(T initialValue) { + super(initialValue); + } + + /// Creates a wrapper of a bean with the default value. + public ReadOnlyObjectWrapper(Object bean, String name) { + super(bean, name); + } + + /// Creates a wrapper of a bean. + public ReadOnlyObjectWrapper(Object bean, String name, T initialValue) { + super(bean, name, initialValue); + } + + /// Returns the read-only view; the same object on every call. + public ReadOnlyObjectProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private final class ReadOnlyPropertyImpl extends ReadOnlyObjectPropertyBase { + + @Override + public T get() { + return ReadOnlyObjectWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyObjectWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyObjectWrapper.this.getName(); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyProperty.java new file mode 100644 index 00000000000..7468ec12e2f --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyProperty.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import javafx.beans.value.ObservableValue; + +/// An observable value that belongs to a bean and has a name there. +public interface ReadOnlyProperty extends ObservableValue { + + /// Returns the object owning the property, or `null`. + Object getBean(); + + /// Returns the name of the property; empty when it has none. + String getName(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringProperty.java new file mode 100644 index 00000000000..a6f2b9276c6 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringProperty.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.binding.StringExpression; + +/// A property holding a string that can be read and observed but not written. +public abstract class ReadOnlyStringProperty extends StringExpression implements ReadOnlyProperty { + + /// Creates the property. + public ReadOnlyStringProperty() { + } + + @Override + public String toString() { + return PropertyText.describe("ReadOnlyStringProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringPropertyBase.java new file mode 100644 index 00000000000..79bbcd6ad66 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringPropertyBase.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.ValueListeners; +import javafx.beans.InvalidationListener; +import javafx.beans.value.ChangeListener; + +/// Base class for a read-only property holding a string: it keeps the listeners +/// and leaves the value to the subclass, which calls +/// [#fireValueChangedEvent()] whenever that value may have changed. +public abstract class ReadOnlyStringPropertyBase extends ReadOnlyStringProperty { + + private ValueListeners helper; + + /// Creates the property. + public ReadOnlyStringPropertyBase() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringWrapper.java b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringWrapper.java new file mode 100644 index 00000000000..c5916fa81b3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/ReadOnlyStringWrapper.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A writable property that hands out a read-only view of itself: the owner +/// keeps the wrapper and writes to it, and publishes +/// [#getReadOnlyProperty()], which always shows the same value. +public class ReadOnlyStringWrapper extends SimpleStringProperty { + + private ReadOnlyPropertyImpl readOnlyProperty; + + /// Creates a wrapper with no bean, an empty name and the default value. + public ReadOnlyStringWrapper() { + } + + /// Creates a wrapper with no bean and an empty name. + public ReadOnlyStringWrapper(String initialValue) { + super(initialValue); + } + + /// Creates a wrapper of a bean with the default value. + public ReadOnlyStringWrapper(Object bean, String name) { + super(bean, name); + } + + /// Creates a wrapper of a bean. + public ReadOnlyStringWrapper(Object bean, String name, String initialValue) { + super(bean, name, initialValue); + } + + /// Returns the read-only view; the same object on every call. + public ReadOnlyStringProperty getReadOnlyProperty() { + if (readOnlyProperty == null) { + readOnlyProperty = new ReadOnlyPropertyImpl(); + } + return readOnlyProperty; + } + + @Override + protected void fireValueChangedEvent() { + super.fireValueChangedEvent(); + if (readOnlyProperty != null) { + readOnlyProperty.fireValueChangedEvent(); + } + } + + private final class ReadOnlyPropertyImpl extends ReadOnlyStringPropertyBase { + + @Override + public String get() { + return ReadOnlyStringWrapper.this.get(); + } + + @Override + public Object getBean() { + return ReadOnlyStringWrapper.this.getBean(); + } + + @Override + public String getName() { + return ReadOnlyStringWrapper.this.getName(); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleBooleanProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleBooleanProperty.java new file mode 100644 index 00000000000..18cabf5a7c4 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleBooleanProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A boolean property with a bean and a name, ready to use as a field. +public class SimpleBooleanProperty extends BooleanPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /// Creates a property with no bean, an empty name and the default value. + public SimpleBooleanProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /// Creates a property with no bean and an empty name. + public SimpleBooleanProperty(boolean initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /// Creates a property of a bean with the default value. + public SimpleBooleanProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /// Creates a property of a bean. + public SimpleBooleanProperty(Object bean, String name, boolean initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + @Override + public Object getBean() { + return bean; + } + + @Override + public String getName() { + return name; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleDoubleProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleDoubleProperty.java new file mode 100644 index 00000000000..5999a2683b0 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleDoubleProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A `double` property with a bean and a name, ready to use as a field. +public class SimpleDoubleProperty extends DoublePropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /// Creates a property with no bean, an empty name and the default value. + public SimpleDoubleProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /// Creates a property with no bean and an empty name. + public SimpleDoubleProperty(double initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /// Creates a property of a bean with the default value. + public SimpleDoubleProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /// Creates a property of a bean. + public SimpleDoubleProperty(Object bean, String name, double initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + @Override + public Object getBean() { + return bean; + } + + @Override + public String getName() { + return name; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleFloatProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleFloatProperty.java new file mode 100644 index 00000000000..d8e48db1aa5 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleFloatProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A `float` property with a bean and a name, ready to use as a field. +public class SimpleFloatProperty extends FloatPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /// Creates a property with no bean, an empty name and the default value. + public SimpleFloatProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /// Creates a property with no bean and an empty name. + public SimpleFloatProperty(float initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /// Creates a property of a bean with the default value. + public SimpleFloatProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /// Creates a property of a bean. + public SimpleFloatProperty(Object bean, String name, float initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + @Override + public Object getBean() { + return bean; + } + + @Override + public String getName() { + return name; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleIntegerProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleIntegerProperty.java new file mode 100644 index 00000000000..75970ea365e --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleIntegerProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A `int` property with a bean and a name, ready to use as a field. +public class SimpleIntegerProperty extends IntegerPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /// Creates a property with no bean, an empty name and the default value. + public SimpleIntegerProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /// Creates a property with no bean and an empty name. + public SimpleIntegerProperty(int initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /// Creates a property of a bean with the default value. + public SimpleIntegerProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /// Creates a property of a bean. + public SimpleIntegerProperty(Object bean, String name, int initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + @Override + public Object getBean() { + return bean; + } + + @Override + public String getName() { + return name; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleListProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleListProperty.java new file mode 100644 index 00000000000..ea719becc3b --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleListProperty.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import javafx.collections.ObservableList; + +/// A list property with a bean and a name, ready to use as a field. +public class SimpleListProperty extends ListPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /// Creates a property with no bean, an empty name and no list. + public SimpleListProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /// Creates a property with no bean and an empty name. + public SimpleListProperty(ObservableList initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /// Creates a property of a bean holding no list. + public SimpleListProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /// Creates a property of a bean. + public SimpleListProperty(Object bean, String name, ObservableList initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + @Override + public Object getBean() { + return bean; + } + + @Override + public String getName() { + return name; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleLongProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleLongProperty.java new file mode 100644 index 00000000000..0262af781b8 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleLongProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A `long` property with a bean and a name, ready to use as a field. +public class SimpleLongProperty extends LongPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /// Creates a property with no bean, an empty name and the default value. + public SimpleLongProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /// Creates a property with no bean and an empty name. + public SimpleLongProperty(long initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /// Creates a property of a bean with the default value. + public SimpleLongProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /// Creates a property of a bean. + public SimpleLongProperty(Object bean, String name, long initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + @Override + public Object getBean() { + return bean; + } + + @Override + public String getName() { + return name; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleObjectProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleObjectProperty.java new file mode 100644 index 00000000000..a2f81aa94d3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleObjectProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// An object property with a bean and a name, ready to use as a field. +public class SimpleObjectProperty extends ObjectPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /// Creates a property with no bean, an empty name and the default value. + public SimpleObjectProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /// Creates a property with no bean and an empty name. + public SimpleObjectProperty(T initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /// Creates a property of a bean with the default value. + public SimpleObjectProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /// Creates a property of a bean. + public SimpleObjectProperty(Object bean, String name, T initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + @Override + public Object getBean() { + return bean; + } + + @Override + public String getName() { + return name; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleStringProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleStringProperty.java new file mode 100644 index 00000000000..9312197c7d3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/SimpleStringProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +/// A string property with a bean and a name, ready to use as a field. +public class SimpleStringProperty extends StringPropertyBase { + + private static final Object DEFAULT_BEAN = null; + private static final String DEFAULT_NAME = ""; + + private final Object bean; + private final String name; + + /// Creates a property with no bean, an empty name and the default value. + public SimpleStringProperty() { + this(DEFAULT_BEAN, DEFAULT_NAME); + } + + /// Creates a property with no bean and an empty name. + public SimpleStringProperty(String initialValue) { + this(DEFAULT_BEAN, DEFAULT_NAME, initialValue); + } + + /// Creates a property of a bean with the default value. + public SimpleStringProperty(Object bean, String name) { + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + /// Creates a property of a bean. + public SimpleStringProperty(Object bean, String name, String initialValue) { + super(initialValue); + this.bean = bean; + this.name = (name == null) ? DEFAULT_NAME : name; + } + + @Override + public Object getBean() { + return bean; + } + + @Override + public String getName() { + return name; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/StringProperty.java b/maven/javafx-compat/src/main/java/javafx/beans/property/StringProperty.java new file mode 100644 index 00000000000..55f59202b17 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/StringProperty.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import javafx.beans.binding.Bindings; +import javafx.beans.value.WritableStringValue; +import javafx.util.StringConverter; + +/// A property holding a string that can be read, written, observed and bound. +public abstract class StringProperty extends ReadOnlyStringProperty implements Property, WritableStringValue { + + /// Creates the property. + public StringProperty() { + } + + @Override + public void setValue(String v) { + set(v); + } + + @Override + public void bindBidirectional(Property other) { + Bindings.bindBidirectional(this, other); + } + + @Override + public void unbindBidirectional(Property other) { + Bindings.unbindBidirectional(this, other); + } + + /// Keeps this property and another one of any type in sync through a + /// converter: this side holds the text, the other the converted value. + public void bindBidirectional(Property other, StringConverter converter) { + Bindings.bindBidirectional(this, other, converter); + } + + /// Removes a bidirectional binding to any object, converter based or not. + public void unbindBidirectional(Object other) { + Bindings.unbindBidirectional(this, other); + } + + @Override + public String toString() { + return PropertyText.describe("StringProperty", this, "value: " + get()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/StringPropertyBase.java b/maven/javafx-compat/src/main/java/javafx/beans/property/StringPropertyBase.java new file mode 100644 index 00000000000..c28105d2cb5 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/StringPropertyBase.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.property; + +import com.codename1.fxcompat.runtime.PropertyText; +import com.codename1.fxcompat.runtime.ValueListeners; +import java.lang.ref.WeakReference; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakListener; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; + +/// The full implementation of a string property except for its bean and name. +/// +/// Invalidation is lazy: after a change, listeners are told once and not +/// again until the value has been read. A change listener reads the value on +/// every invalidation, so registering one makes the property eager. +public abstract class StringPropertyBase extends StringProperty { + + private String value; + private ObservableValue observable; + private InvalidationListener listener; + private boolean valid = true; + private ValueListeners helper; + + /// Creates the property with the default value. + public StringPropertyBase() { + } + + /// Creates the property with an initial value. + public StringPropertyBase(String initialValue) { + this.value = initialValue; + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } + + private void markInvalid() { + if (valid) { + valid = false; + invalidated(); + fireValueChangedEvent(); + } + } + + /// Called when the property turns invalid, before listeners are told. + protected void invalidated() { + } + + @Override + public String get() { + valid = true; + return observable == null ? value : boundValue(); + } + + private String boundValue() { + String v = observable.getValue(); + return v; + } + + /// Sets the value. + /// + /// #### Throws + /// + /// - `RuntimeException`: when the property is bound + @Override + public void set(String newValue) { + if (isBound()) { + throw new RuntimeException(PropertyText.owner(this) + "A bound value cannot be set."); + } + if ((value == null) ? newValue != null : !value.equals(newValue)) { + value = newValue; + markInvalid(); + } + } + + @Override + public boolean isBound() { + return observable != null; + } + + /// Makes this property follow another value until [#unbind()]. The + /// property observes it weakly, so being bound does not keep it alive. + @Override + public void bind(final ObservableValue newObservable) { + if (newObservable == null) { + throw new NullPointerException("Cannot bind to null"); + } + if (!newObservable.equals(observable)) { + unbind(); + observable = newObservable; + if (listener == null) { + listener = new Listener(this); + } + observable.addListener(listener); + markInvalid(); + } + } + + /// Stops following the bound value and keeps the value it had. + @Override + public void unbind() { + if (observable != null) { + value = boundValue(); + observable.removeListener(listener); + observable = null; + } + } + + @Override + public String toString() { + String tail; + if (isBound()) { + tail = valid ? "bound, value: " + get() : "bound, invalid"; + } else { + tail = "value: " + get(); + } + return PropertyText.describe("StringProperty", this, tail); + } + + /// Forwards invalidations of the bound value without keeping the + /// property reachable from it. + private static final class Listener implements InvalidationListener, WeakListener { + private final WeakReference ref; + + Listener(StringPropertyBase property) { + this.ref = new WeakReference(property); + } + + @Override + public void invalidated(Observable source) { + Object property = ref.get(); + if (property instanceof StringPropertyBase) { + ((StringPropertyBase) property).markInvalid(); + } else { + source.removeListener(this); + } + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/property/package-info.java b/maven/javafx-compat/src/main/java/javafx/beans/property/package-info.java new file mode 100644 index 00000000000..19886158465 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/property/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Properties: observable, writable, bindable values that belong to a bean, +/// in a typed family for every primitive kind, strings, objects and lists. +package javafx.beans.property; diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ChangeListener.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ChangeListener.java new file mode 100644 index 00000000000..5ca931c3096 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ChangeListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// Told when the value of an [ObservableValue] changes. +@FunctionalInterface +public interface ChangeListener { + + /// Called after the value changed. + void changed(ObservableValue observable, T oldValue, T newValue); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableBooleanValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableBooleanValue.java new file mode 100644 index 00000000000..bce16a5a5d5 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableBooleanValue.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// An observable value holding a boolean. +public interface ObservableBooleanValue extends ObservableValue { + + /// Returns the current value, validating a lazily evaluated one. + boolean get(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableDoubleValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableDoubleValue.java new file mode 100644 index 00000000000..bab0b34f259 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableDoubleValue.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// An observable value holding a `double`. +public interface ObservableDoubleValue extends ObservableNumberValue { + + /// Returns the current value, validating a lazily evaluated one. + double get(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableFloatValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableFloatValue.java new file mode 100644 index 00000000000..582a2dd29c3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableFloatValue.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// An observable value holding a `float`. +public interface ObservableFloatValue extends ObservableNumberValue { + + /// Returns the current value, validating a lazily evaluated one. + float get(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableIntegerValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableIntegerValue.java new file mode 100644 index 00000000000..3753d4296a7 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableIntegerValue.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// An observable value holding a `int`. +public interface ObservableIntegerValue extends ObservableNumberValue { + + /// Returns the current value, validating a lazily evaluated one. + int get(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableListValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableListValue.java new file mode 100644 index 00000000000..26c912e9bfb --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableListValue.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +import javafx.collections.ObservableList; + +/// An observable reference to an [ObservableList] that is itself usable as +/// that list. +public interface ObservableListValue extends ObservableObjectValue>, ObservableList { +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableLongValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableLongValue.java new file mode 100644 index 00000000000..baaa5a0f017 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableLongValue.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// An observable value holding a `long`. +public interface ObservableLongValue extends ObservableNumberValue { + + /// Returns the current value, validating a lazily evaluated one. + long get(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableNumberValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableNumberValue.java new file mode 100644 index 00000000000..65631d4d002 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableNumberValue.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// An observable numeric value, readable as any primitive number type. +public interface ObservableNumberValue extends ObservableValue { + + /// Returns the value as an `int`. + int intValue(); + + /// Returns the value as a `long`. + long longValue(); + + /// Returns the value as a `float`. + float floatValue(); + + /// Returns the value as a `double`. + double doubleValue(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableObjectValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableObjectValue.java new file mode 100644 index 00000000000..2b1c200e2d3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableObjectValue.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// An observable value holding an object. +public interface ObservableObjectValue extends ObservableValue { + + /// Returns the current value, validating a lazily evaluated one. + T get(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableStringValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableStringValue.java new file mode 100644 index 00000000000..751e917bc90 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableStringValue.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// An observable value holding a string. +public interface ObservableStringValue extends ObservableObjectValue { +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableValue.java new file mode 100644 index 00000000000..84e38803318 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableValue.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +import javafx.beans.Observable; + +/// A value that can be read and observed. +/// +/// An [javafx.beans.InvalidationListener] hears that the value may have +/// changed; a [ChangeListener] is handed the old and the new value, which +/// forces the value to be computed on every invalidation. +public interface ObservableValue extends Observable { + + /// Adds a listener told whenever the value changes. + void addListener(ChangeListener listener); + + /// Removes one registration of the listener; unknown listeners are ignored. + void removeListener(ChangeListener listener); + + /// Returns the current value. + T getValue(); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableValueBase.java b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableValueBase.java new file mode 100644 index 00000000000..b78e89b340d --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/ObservableValueBase.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +import com.codename1.fxcompat.runtime.ValueListeners; + +import javafx.beans.InvalidationListener; + +/// A convenient base for an [ObservableValue]: it keeps the listeners, and +/// the subclass supplies the value and calls [#fireValueChangedEvent()] when +/// it changes. +public abstract class ObservableValueBase implements ObservableValue { + + private ValueListeners helper; + + /// Creates the value. + public ObservableValueBase() { + } + + @Override + public void addListener(InvalidationListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void addListener(ChangeListener listener) { + helper = ValueListeners.add(helper, this, listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + @Override + public void removeListener(ChangeListener listener) { + helper = ValueListeners.remove(helper, listener); + } + + /// Tells the listeners the value may have changed. + protected void fireValueChangedEvent() { + ValueListeners.fire(helper); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WeakChangeListener.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WeakChangeListener.java new file mode 100644 index 00000000000..9af5b0e4ab1 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WeakChangeListener.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +import java.lang.ref.WeakReference; + +import javafx.beans.WeakListener; + +/// A [ChangeListener] that forwards to another one without keeping it alive. +/// Whoever creates it must hold the wrapped listener; once that is collected, +/// this listener removes itself on the next change. +public final class WeakChangeListener implements ChangeListener, WeakListener { + + private final WeakReference ref; + + /// Wraps a listener. + public WeakChangeListener(ChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + this.ref = new WeakReference(listener); + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + + @Override + @SuppressWarnings("unchecked") + public void changed(ObservableValue observable, T oldValue, T newValue) { + Object listener = ref.get(); + if (listener instanceof ChangeListener) { + ((ChangeListener) listener).changed(observable, oldValue, newValue); + } else { + observable.removeListener(this); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WritableBooleanValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableBooleanValue.java new file mode 100644 index 00000000000..ff930166a89 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableBooleanValue.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// A writable value holding a boolean. +public interface WritableBooleanValue extends WritableValue { + + /// Returns the current value. + boolean get(); + + /// Sets the value. + void set(boolean value); + + /// Sets the value from its boxed form; `null` stores the default value. + @Override + void setValue(Boolean value); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WritableDoubleValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableDoubleValue.java new file mode 100644 index 00000000000..07ad691cfe5 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableDoubleValue.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// A writable value holding a `double`. +public interface WritableDoubleValue extends WritableNumberValue { + + /// Returns the current value. + double get(); + + /// Sets the value. + void set(double value); + + /// Sets the value from its boxed form; `null` stores the default value. + @Override + void setValue(Number value); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WritableFloatValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableFloatValue.java new file mode 100644 index 00000000000..f142a442172 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableFloatValue.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// A writable value holding a `float`. +public interface WritableFloatValue extends WritableNumberValue { + + /// Returns the current value. + float get(); + + /// Sets the value. + void set(float value); + + /// Sets the value from its boxed form; `null` stores the default value. + @Override + void setValue(Number value); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WritableIntegerValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableIntegerValue.java new file mode 100644 index 00000000000..3b05c184b64 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableIntegerValue.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// A writable value holding a `int`. +public interface WritableIntegerValue extends WritableNumberValue { + + /// Returns the current value. + int get(); + + /// Sets the value. + void set(int value); + + /// Sets the value from its boxed form; `null` stores the default value. + @Override + void setValue(Number value); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WritableListValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableListValue.java new file mode 100644 index 00000000000..512577d8d12 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableListValue.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +import javafx.collections.ObservableList; + +/// A writable reference to an [ObservableList] that is itself usable as that +/// list. +public interface WritableListValue extends WritableObjectValue>, ObservableList { +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WritableLongValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableLongValue.java new file mode 100644 index 00000000000..6643864fce2 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableLongValue.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// A writable value holding a `long`. +public interface WritableLongValue extends WritableNumberValue { + + /// Returns the current value. + long get(); + + /// Sets the value. + void set(long value); + + /// Sets the value from its boxed form; `null` stores the default value. + @Override + void setValue(Number value); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WritableNumberValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableNumberValue.java new file mode 100644 index 00000000000..1b3745071b6 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableNumberValue.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// A writable numeric value. +public interface WritableNumberValue extends WritableValue { +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WritableObjectValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableObjectValue.java new file mode 100644 index 00000000000..3521ec964ce --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableObjectValue.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// A writable value holding an object. +public interface WritableObjectValue extends WritableValue { + + /// Returns the current value. + T get(); + + /// Sets the value. + void set(T value); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WritableStringValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableStringValue.java new file mode 100644 index 00000000000..c37ec25a961 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableStringValue.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// A writable value holding a string. +public interface WritableStringValue extends WritableObjectValue { +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/WritableValue.java b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableValue.java new file mode 100644 index 00000000000..58d46781f97 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/WritableValue.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.beans.value; + +/// A value that can be read and written. +public interface WritableValue { + + /// Returns the current value. + T getValue(); + + /// Sets the value. + void setValue(T value); +} diff --git a/maven/javafx-compat/src/main/java/javafx/beans/value/package-info.java b/maven/javafx-compat/src/main/java/javafx/beans/value/package-info.java new file mode 100644 index 00000000000..f0971b815e3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/beans/value/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Observable and writable values, their listeners and the typed variants +/// that read a primitive without boxing it. +package javafx.beans.value; diff --git a/maven/javafx-compat/src/main/java/javafx/collections/FXCollections.java b/maven/javafx-compat/src/main/java/javafx/collections/FXCollections.java new file mode 100644 index 00000000000..c7708dfa89c --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/FXCollections.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import com.codename1.fxcompat.runtime.ArrayObservableList; +import com.codename1.fxcompat.runtime.ConstantObservableList; +import com.codename1.fxcompat.runtime.ObservableMapWrapper; +import com.codename1.fxcompat.runtime.ObservableSetWrapper; +import com.codename1.fxcompat.runtime.UnmodifiableViews; + +import javafx.beans.Observable; +import javafx.util.Callback; + +/// Creates observable collections and applies the bulk operations of +/// `java.util.Collections` to them so that each is reported as one change. +public class FXCollections { + + private FXCollections() { + } + + /// Wraps a list; changes made through the wrapper are reported, changes + /// made to the list directly are not. + public static ObservableList observableList(List list) { + if (list == null) { + throw new NullPointerException(); + } + return new ArrayObservableList(list, null); + } + + /// Wraps a list and also reports, as an update, a change of any + /// observable the extractor returns for an element. + public static ObservableList observableList(List list, Callback extractor) { + if (list == null || extractor == null) { + throw new NullPointerException(); + } + return new ArrayObservableList(list, extractor); + } + + /// Creates an empty observable list backed by an array list. + public static ObservableList observableArrayList() { + return observableList(new ArrayList()); + } + + /// Creates an empty observable list that also reports, as an update, a + /// change of any observable the extractor returns for an element. + public static ObservableList observableArrayList(Callback extractor) { + return observableList(new ArrayList(), extractor); + } + + /// Creates an observable list holding the given elements. + public static ObservableList observableArrayList(E... items) { + ObservableList list = observableArrayList(); + list.addAll(items); + return list; + } + + /// Creates an observable list holding a copy of a collection. + public static ObservableList observableArrayList(Collection col) { + ObservableList list = observableArrayList(); + list.addAll(col); + return list; + } + + /// Wraps a map; changes made through the wrapper are reported. + public static ObservableMap observableMap(Map map) { + if (map == null) { + throw new NullPointerException(); + } + return new ObservableMapWrapper(map); + } + + /// Creates an empty observable map backed by a hash map. + public static ObservableMap observableHashMap() { + return observableMap(new HashMap()); + } + + /// Wraps a set; changes made through the wrapper are reported. + public static ObservableSet observableSet(Set set) { + if (set == null) { + throw new NullPointerException(); + } + return new ObservableSetWrapper(set); + } + + /// Creates an observable set holding the given elements. + public static ObservableSet observableSet(E... elements) { + if (elements == null) { + throw new NullPointerException(); + } + Set set = new HashSet(elements.length); + Collections.addAll(set, elements); + return new ObservableSetWrapper(set); + } + + /// Returns a read-only view of a list that reports its changes. + public static ObservableList unmodifiableObservableList(ObservableList list) { + if (list == null) { + throw new NullPointerException(); + } + return new UnmodifiableViews.OfList(list); + } + + /// Returns a read-only view of a map that reports its changes. + public static ObservableMap unmodifiableObservableMap(ObservableMap map) { + if (map == null) { + throw new NullPointerException(); + } + return new UnmodifiableViews.OfMap(map); + } + + /// Returns a read-only view of a set that reports its changes. + public static ObservableSet unmodifiableObservableSet(ObservableSet set) { + if (set == null) { + throw new NullPointerException(); + } + return new UnmodifiableViews.OfSet(set); + } + + /// Returns an empty list that can never change. + public static ObservableList emptyObservableList() { + return new ConstantObservableList(Collections.emptyList()); + } + + /// Returns an empty map that can never change. + public static ObservableMap emptyObservableMap() { + return new UnmodifiableViews.OfMap(FXCollections.observableHashMap()); + } + + /// Returns an empty set that can never change. + public static ObservableSet emptyObservableSet() { + return new UnmodifiableViews.OfSet(observableSet(new HashSet())); + } + + /// Returns a list of one element that can never change. + public static ObservableList singletonObservableList(E e) { + return new ConstantObservableList(Collections.singletonList(e)); + } + + /// Creates an observable list holding the content of several lists in + /// a row. It is a copy and does not follow them. + public static ObservableList concat(ObservableList... lists) { + ObservableList result = observableArrayList(); + if (lists != null) { + for (ObservableList list : lists) { + result.addAll(list); + } + } + return result; + } + + /// Sorts a list into its natural order. A list created by this class + /// reports a permutation, any other a replacement of its content. + public static > void sort(ObservableList list) { + sort(list, null); + } + + /// Sorts a list with a comparator. A list created by this class reports + /// a permutation, any other a replacement of its content. + public static void sort(ObservableList list, Comparator c) { + if (list instanceof ArrayObservableList) { + list.sort(c); + } else { + List copy = new ArrayList(list); + Collections.sort(copy, c); + list.setAll(copy); + } + } + + /// Reverses a list, reported as a replacement of its content. + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void reverse(ObservableList list) { + List copy = new ArrayList(list); + Collections.reverse(copy); + list.setAll(copy); + } + + /// Shuffles a list with the given source of randomness, reported as a + /// replacement of its content. + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void shuffle(ObservableList list, Random rnd) { + List copy = new ArrayList(list); + for (int i = copy.size() - 1; i > 0; i--) { + int other = rnd.nextInt(i + 1); + Object swapped = copy.get(i); + copy.set(i, copy.get(other)); + copy.set(other, swapped); + } + list.setAll(copy); + } + + /// Sets every element of a list to one object, reported as one change. + public static void fill(ObservableList list, T obj) { + List copy = new ArrayList(list.size()); + for (int i = list.size(); i > 0; i--) { + copy.add(obj); + } + list.setAll(copy); + } + + /// Copies a list over the start of another, which must be at least as + /// long; reported as one change. + public static void copy(ObservableList dest, List src) { + final int srcSize = src.size(); + if (srcSize > dest.size()) { + throw new IndexOutOfBoundsException("Source does not fit in dest"); + } + List copy = new ArrayList(dest); + for (int i = 0; i < srcSize; i++) { + copy.set(i, src.get(i)); + } + replaceContent(dest, copy); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void replaceContent(ObservableList dest, List content) { + dest.setAll(content); + } + + /// Replaces every occurrence of a value in a list; returns whether + /// there was one. + public static boolean replaceAll(ObservableList list, T oldVal, T newVal) { + List copy = new ArrayList(list); + boolean modified = false; + for (int i = 0; i < copy.size(); i++) { + T current = copy.get(i); + if (current == null ? oldVal == null : current.equals(oldVal)) { + copy.set(i, newVal); + modified = true; + } + } + if (modified) { + list.setAll(copy); + } + return modified; + } + + /// Rotates a list by a distance, reported as a replacement of its + /// content. + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void rotate(ObservableList list, int distance) { + List copy = new ArrayList(list); + Collections.rotate(copy, distance); + list.setAll(copy); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/ListChangeListener.java b/maven/javafx-compat/src/main/java/javafx/collections/ListChangeListener.java new file mode 100644 index 00000000000..88a8d11949f --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/ListChangeListener.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +import java.util.Collections; +import java.util.List; + +/// Told about the changes made to an [ObservableList]. +@FunctionalInterface +public interface ListChangeListener { + + /// Called after the list changed. The change must be walked with + /// [Change#next()] and is only valid during the call. + void onChanged(Change c); + + /// A report of what happened to a list, as a sequence of steps. + /// + /// The steps are ordered by position and each describes the list as it + /// is once the steps before it have been applied, so a listener can + /// replay them in order on a copy. A step is one of: a range of added + /// elements, possibly replacing removed ones ([#wasAdded()], + /// [#wasRemoved()], both for [#wasReplaced()]); a reordering of a range + /// ([#wasPermutated()]); or a range of elements whose own state changed + /// ([#wasUpdated()]). + /// + /// A new change stands before its first step: nothing may be read until + /// [#next()] returned `true`. + public abstract static class Change { + + private final ObservableList list; + + /// Creates a change of a list. + public Change(ObservableList list) { + this.list = list; + } + + /// Moves to the next step; returns `false` when there is none. + public abstract boolean next(); + + /// Goes back to before the first step. + public abstract void reset(); + + /// Returns the list that changed. + public ObservableList getList() { + return list; + } + + /// Returns where the step starts. + public abstract int getFrom(); + + /// Returns the end, exclusive, of the range the step added, + /// reordered or updated; equal to [#getFrom()] for a pure removal. + public abstract int getTo(); + + /// Returns the elements the step removed or replaced, in the order + /// they had; empty when it removed nothing. + public abstract List getRemoved(); + + /// Returns whether the step reordered its range. + public boolean wasPermutated() { + return getPermutation().length != 0; + } + + /// Returns whether the step added elements. + public boolean wasAdded() { + return !wasPermutated() && !wasUpdated() && getFrom() < getTo(); + } + + /// Returns whether the step removed elements. + public boolean wasRemoved() { + return !getRemoved().isEmpty(); + } + + /// Returns whether the step removed elements and added others in + /// their place. + public boolean wasReplaced() { + return wasAdded() && wasRemoved(); + } + + /// Returns whether the step reports elements whose state changed. + public boolean wasUpdated() { + return false; + } + + /// Returns the added elements as a view of the list; empty when the + /// step added nothing. + public List getAddedSubList() { + return wasAdded() ? getList().subList(getFrom(), getTo()) : Collections.emptyList(); + } + + /// Returns how many elements the step removed. + public int getRemovedSize() { + return getRemoved().size(); + } + + /// Returns how many elements the step added. + public int getAddedSize() { + return wasAdded() ? getTo() - getFrom() : 0; + } + + /// Returns the reordering: entry `i` is the new position of the + /// element that was at `getFrom() + i`. Empty when the step is not + /// a permutation. + protected abstract int[] getPermutation(); + + /// Returns the new position of the element that was at `i`. + /// + /// #### Throws + /// + /// - `IllegalStateException`: when the step is not a permutation + public int getPermutation(int i) { + if (!wasPermutated()) { + throw new IllegalStateException("Not a permutation change"); + } + return getPermutation()[i - getFrom()]; + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/MapChangeListener.java b/maven/javafx-compat/src/main/java/javafx/collections/MapChangeListener.java new file mode 100644 index 00000000000..6c3a77e1b46 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/MapChangeListener.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +/// Told about the changes made to an [ObservableMap]. +@FunctionalInterface +public interface MapChangeListener { + + /// Called after one key of the map changed. + void onChanged(Change change); + + /// What happened to one key: a value was added, removed, or -- when + /// both [#wasAdded()] and [#wasRemoved()] hold -- replaced. + public abstract static class Change { + + private final ObservableMap map; + + /// Creates a change of a map. + public Change(ObservableMap map) { + this.map = map; + } + + /// Returns the map that changed. + public ObservableMap getMap() { + return map; + } + + /// Returns whether a value was put for the key. + public abstract boolean wasAdded(); + + /// Returns whether a value was removed for the key. + public abstract boolean wasRemoved(); + + /// Returns the key that changed. + public abstract K getKey(); + + /// Returns the value now stored, or `null` for a removal. + public abstract V getValueAdded(); + + /// Returns the value that was stored, or `null` for an addition. + public abstract V getValueRemoved(); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/ModifiableObservableListBase.java b/maven/javafx-compat/src/main/java/javafx/collections/ModifiableObservableListBase.java new file mode 100644 index 00000000000..6dcbc692ccd --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/ModifiableObservableListBase.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/// Base class for a modifiable observable list over any storage: the +/// subclass implements `get`, `size` and the three `do...` methods, and this +/// class turns every `List` operation into them and reports the changes. +public abstract class ModifiableObservableListBase extends ObservableListBase { + + /// Creates the list. + public ModifiableObservableListBase() { + } + + /// Stores an element at a position, shifting the ones after it. + protected abstract void doAdd(int index, E element); + + /// Replaces the element at a position and returns the old one. + protected abstract E doSet(int index, E element); + + /// Removes the element at a position and returns it. + protected abstract E doRemove(int index); + + @Override + public boolean setAll(Collection col) { + List copy = new ArrayList(col); + beginChange(); + try { + clear(); + addAll(copy); + } finally { + endChange(); + } + return true; + } + + @Override + public boolean addAll(Collection c) { + beginChange(); + try { + return super.addAll(c); + } finally { + endChange(); + } + } + + @Override + public boolean addAll(int index, Collection c) { + beginChange(); + try { + return super.addAll(index, c); + } finally { + endChange(); + } + } + + @Override + protected void removeRange(int fromIndex, int toIndex) { + beginChange(); + try { + for (int i = fromIndex; i < toIndex; i++) { + remove(fromIndex); + } + } finally { + endChange(); + } + } + + @Override + public void clear() { + removeRange(0, size()); + } + + @Override + public boolean removeAll(Collection c) { + if (c == null) { + throw new NullPointerException(); + } + boolean modified = false; + beginChange(); + try { + for (int i = size() - 1; i >= 0; i--) { + if (c.contains(get(i))) { + remove(i); + modified = true; + } + } + } finally { + endChange(); + } + return modified; + } + + @Override + public boolean retainAll(Collection c) { + if (c == null) { + throw new NullPointerException(); + } + boolean modified = false; + beginChange(); + try { + for (int i = size() - 1; i >= 0; i--) { + if (!c.contains(get(i))) { + remove(i); + modified = true; + } + } + } finally { + endChange(); + } + return modified; + } + + @Override + public void add(int index, E element) { + doAdd(index, element); + beginChange(); + nextAdd(index, index + 1); + ++modCount; + endChange(); + } + + @Override + public E set(int index, E element) { + E old = doSet(index, element); + beginChange(); + nextSet(index, old); + endChange(); + return old; + } + + @Override + public boolean remove(Object o) { + int index = indexOf(o); + if (index != -1) { + remove(index); + return true; + } + return false; + } + + @Override + public E remove(int index) { + E old = doRemove(index); + beginChange(); + nextRemove(index, old); + ++modCount; + endChange(); + return old; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/ObservableList.java b/maven/javafx-compat/src/main/java/javafx/collections/ObservableList.java new file mode 100644 index 00000000000..78410077f34 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/ObservableList.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + +import javafx.beans.Observable; +import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; + +/// A list that reports every change to its content. +public interface ObservableList extends List, Observable { + + /// Adds a listener told about every change to the content. + void addListener(ListChangeListener listener); + + /// Removes one registration of the listener; unknown listeners are ignored. + void removeListener(ListChangeListener listener); + + /// Appends the elements, reported as one change. + boolean addAll(E... elements); + + /// Replaces the whole content, reported as one change. + boolean setAll(E... elements); + + /// Replaces the whole content, reported as one change. + boolean setAll(Collection col); + + /// Removes every occurrence of the elements, reported as one change. + boolean removeAll(E... elements); + + /// Removes everything but the elements, reported as one change. + boolean retainAll(E... elements); + + /// Removes the elements from `from` up to but excluding `to`. + void remove(int from, int to); + + /// Returns a live view of the elements a predicate accepts. + default FilteredList filtered(Predicate predicate) { + return new FilteredList(this, predicate); + } + + /// Returns a live view of the content ordered by a comparator. + default SortedList sorted(Comparator comparator) { + return new SortedList(this, comparator); + } + + /// Returns a live view of the content in its natural order. + default SortedList sorted() { + Comparator natural = new Comparator() { + @Override + @SuppressWarnings("unchecked") + public int compare(E first, E second) { + if (first == null && second == null) { + return 0; + } + if (first == null) { + return -1; + } + if (second == null) { + return 1; + } + if (first instanceof Comparable) { + return ((Comparable) first).compareTo(second); + } + // Elements with no order of their own keep the order they have. + return 0; + } + }; + return sorted(natural); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/ObservableListBase.java b/maven/javafx-compat/src/main/java/javafx/collections/ObservableListBase.java new file mode 100644 index 00000000000..60e9a545479 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/ObservableListBase.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +import java.util.AbstractList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.codename1.fxcompat.runtime.ListChangeBuilder; +import com.codename1.fxcompat.runtime.ListenerSet; + +import javafx.beans.InvalidationListener; + +/// Base class for an observable list: it keeps the listeners and assembles +/// the change reports. +/// +/// A subclass wraps every modification in [#beginChange()] and +/// [#endChange()] and describes it in between with the `next...` methods, +/// each called right after the step it describes was applied. The steps are +/// merged and ordered, and one change is sent when the outermost +/// `endChange()` returns; the pairs nest, so a bulk operation built from +/// single ones is still reported once. +public abstract class ObservableListBase extends AbstractList implements ObservableList { + + private ListenerSet> listeners; + private final ListChangeBuilder changeBuilder = new ListChangeBuilder(this); + + /// Creates the list. + public ObservableListBase() { + } + + /// Records that the element at a position changed its own state. + protected final void nextUpdate(int pos) { + changeBuilder.nextUpdate(pos); + } + + /// Records that the element at a position was replaced. + protected final void nextSet(int idx, E old) { + changeBuilder.nextSet(idx, old); + } + + /// Records that the range now at `from` to `to` replaced the given + /// elements. + protected final void nextReplace(int from, int to, List removed) { + changeBuilder.nextReplace(from, to, removed); + } + + /// Records that the given elements were removed from a position. + protected final void nextRemove(int idx, List removed) { + changeBuilder.nextRemove(idx, removed); + } + + /// Records that an element was removed from a position. + protected final void nextRemove(int idx, E removed) { + changeBuilder.nextRemove(idx, removed); + } + + /// Records that a range was reordered; entry `i` of the permutation is + /// the new position of the element that was at `from + i`. + protected final void nextPermutation(int from, int to, int[] perm) { + changeBuilder.nextPermutation(from, to, perm); + } + + /// Records that the range from `from` to `to` was added. + protected final void nextAdd(int from, int to) { + changeBuilder.nextAdd(from, to); + } + + /// Opens a change; must be matched by [#endChange()]. + protected final void beginChange() { + changeBuilder.beginChange(); + } + + /// Closes a change and, when it is the outermost one and anything was + /// recorded, tells the listeners. + protected final void endChange() { + ListChangeListener.Change change = changeBuilder.endChange(); + if (change != null) { + fireChange(change); + } + } + + @Override + public final void addListener(InvalidationListener listener) { + listeners = ListenerSet.addInvalidation(listeners, listener); + } + + @Override + public final void removeListener(InvalidationListener listener) { + listeners = ListenerSet.removeInvalidation(listeners, listener); + } + + @Override + public final void addListener(ListChangeListener listener) { + listeners = ListenerSet.addChange(listeners, listener); + } + + @Override + public final void removeListener(ListChangeListener listener) { + listeners = ListenerSet.removeChange(listeners, listener); + } + + /// Sends a change to every listener, each starting before its first + /// step. + @SuppressWarnings({"unchecked", "rawtypes"}) + protected final void fireChange(ListChangeListener.Change change) { + ListenerSet> current = listeners; + ListenerSet.fireInvalidation(current, this); + List> targets = ListenerSet.changeListeners(current); + for (int i = 0; i < targets.size(); i++) { + change.reset(); + ((ListChangeListener) targets.get(i)).onChanged(change); + } + } + + /// Returns whether anything listens to this list. + protected final boolean hasListeners() { + return ListenerSet.hasListeners(listeners); + } + + @Override + public boolean addAll(E... elements) { + return addAll(Arrays.asList(elements)); + } + + @Override + public boolean setAll(E... elements) { + return setAll(Arrays.asList(elements)); + } + + @Override + public boolean setAll(Collection col) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(E... elements) { + return removeAll(Arrays.asList(elements)); + } + + @Override + public boolean retainAll(E... elements) { + return retainAll(Arrays.asList(elements)); + } + + @Override + public void remove(int from, int to) { + removeRange(from, to); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/ObservableMap.java b/maven/javafx-compat/src/main/java/javafx/collections/ObservableMap.java new file mode 100644 index 00000000000..77f96f76c15 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/ObservableMap.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +import java.util.Map; + +import javafx.beans.Observable; + +/// A map that reports every change to its content. +public interface ObservableMap extends Map, Observable { + + /// Adds a listener told about every added, removed or replaced entry. + void addListener(MapChangeListener listener); + + /// Removes one registration of the listener; unknown listeners are ignored. + void removeListener(MapChangeListener listener); +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/ObservableSet.java b/maven/javafx-compat/src/main/java/javafx/collections/ObservableSet.java new file mode 100644 index 00000000000..e0ed4435611 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/ObservableSet.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +import java.util.Set; + +import javafx.beans.Observable; + +/// A set that reports every change to its content. +public interface ObservableSet extends Set, Observable { + + /// Adds a listener told about every added or removed element. + void addListener(SetChangeListener listener); + + /// Removes one registration of the listener; unknown listeners are ignored. + void removeListener(SetChangeListener listener); +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/SetChangeListener.java b/maven/javafx-compat/src/main/java/javafx/collections/SetChangeListener.java new file mode 100644 index 00000000000..29ff3602bd3 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/SetChangeListener.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +/// Told about the changes made to an [ObservableSet]. +@FunctionalInterface +public interface SetChangeListener { + + /// Called after one element was added or removed. + void onChanged(Change change); + + /// What happened to a set: one element was added or removed. + public abstract static class Change { + + private final ObservableSet set; + + /// Creates a change of a set. + public Change(ObservableSet set) { + this.set = set; + } + + /// Returns the set that changed. + public ObservableSet getSet() { + return set; + } + + /// Returns whether an element was added. + public abstract boolean wasAdded(); + + /// Returns whether an element was removed. + public abstract boolean wasRemoved(); + + /// Returns the added element, or `null` for a removal. + public abstract E getElementAdded(); + + /// Returns the removed element, or `null` for an addition. + public abstract E getElementRemoved(); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/WeakListChangeListener.java b/maven/javafx-compat/src/main/java/javafx/collections/WeakListChangeListener.java new file mode 100644 index 00000000000..02d15f4780d --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/WeakListChangeListener.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +import java.lang.ref.WeakReference; + +import javafx.beans.WeakListener; + +/// A [ListChangeListener] that forwards to another one without keeping it +/// alive. Whoever creates it must hold the wrapped listener; once that is +/// collected, this listener removes itself on the next change. +public final class WeakListChangeListener implements ListChangeListener, WeakListener { + + private final WeakReference ref; + + /// Wraps a listener. + public WeakListChangeListener(ListChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + this.ref = new WeakReference(listener); + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + + @Override + @SuppressWarnings("unchecked") + public void onChanged(Change change) { + Object listener = ref.get(); + if (listener instanceof ListChangeListener) { + ((ListChangeListener) listener).onChanged(change); + } else { + change.getList().removeListener(this); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/WeakMapChangeListener.java b/maven/javafx-compat/src/main/java/javafx/collections/WeakMapChangeListener.java new file mode 100644 index 00000000000..4429b2fe040 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/WeakMapChangeListener.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +import java.lang.ref.WeakReference; + +import javafx.beans.WeakListener; + +/// A [MapChangeListener] that forwards to another one without keeping it +/// alive. Whoever creates it must hold the wrapped listener; once that is +/// collected, this listener removes itself on the next change. +public final class WeakMapChangeListener implements MapChangeListener, WeakListener { + + private final WeakReference ref; + + /// Wraps a listener. + public WeakMapChangeListener(MapChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + this.ref = new WeakReference(listener); + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + + @Override + @SuppressWarnings("unchecked") + public void onChanged(Change change) { + Object listener = ref.get(); + if (listener instanceof MapChangeListener) { + ((MapChangeListener) listener).onChanged(change); + } else { + change.getMap().removeListener(this); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/WeakSetChangeListener.java b/maven/javafx-compat/src/main/java/javafx/collections/WeakSetChangeListener.java new file mode 100644 index 00000000000..793690093e4 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/WeakSetChangeListener.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections; + +import java.lang.ref.WeakReference; + +import javafx.beans.WeakListener; + +/// A [SetChangeListener] that forwards to another one without keeping it +/// alive. Whoever creates it must hold the wrapped listener; once that is +/// collected, this listener removes itself on the next change. +public final class WeakSetChangeListener implements SetChangeListener, WeakListener { + + private final WeakReference ref; + + /// Wraps a listener. + public WeakSetChangeListener(SetChangeListener listener) { + if (listener == null) { + throw new NullPointerException("Listener must be specified."); + } + this.ref = new WeakReference(listener); + } + + @Override + public boolean wasGarbageCollected() { + return ref.get() == null; + } + + @Override + @SuppressWarnings("unchecked") + public void onChanged(Change change) { + Object listener = ref.get(); + if (listener instanceof SetChangeListener) { + ((SetChangeListener) listener).onChanged(change); + } else { + change.getSet().removeListener(this); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/package-info.java b/maven/javafx-compat/src/main/java/javafx/collections/package-info.java new file mode 100644 index 00000000000..cb3a01f28b5 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Observable collections: lists, maps and sets that report every change to +/// their content, the listeners that receive those reports and the +/// [javafx.collections.FXCollections] factory. +package javafx.collections; diff --git a/maven/javafx-compat/src/main/java/javafx/collections/transformation/FilteredList.java b/maven/javafx-compat/src/main/java/javafx/collections/transformation/FilteredList.java new file mode 100644 index 00000000000..5ce7a504e55 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/transformation/FilteredList.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections.transformation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import javafx.beans.NamedArg; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ObjectPropertyBase; +import javafx.collections.ListChangeListener.Change; +import javafx.collections.ObservableList; + +/// A live view of the elements of a list that a predicate accepts, in their +/// source order. With no predicate it shows everything. +/// +/// The view follows the source: elements appear and disappear as they are +/// added, removed, or -- when the source reports an update -- start or stop +/// matching. Changing the predicate is reported as a replacement of the +/// whole content. +public final class FilteredList extends TransformationList { + + private int[] filtered; + private int size; + private final ObjectProperty> predicate; + + /// Creates a view showing the elements a predicate accepts. + public FilteredList(@NamedArg("source") ObservableList source, + @NamedArg("predicate") Predicate predicate) { + super(source); + this.filtered = new int[source.size() * 3 / 2 + 1]; + this.predicate = new ObjectPropertyBase>(predicate) { + @Override + protected void invalidated() { + refilter(); + } + + @Override + public Object getBean() { + return FilteredList.this; + } + + @Override + public String getName() { + return "predicate"; + } + }; + refilter(); + } + + /// Creates a view showing every element. + public FilteredList(@NamedArg("source") ObservableList source) { + this(source, null); + } + + /// The predicate an element must satisfy to be shown; `null` shows all. + public final ObjectProperty> predicateProperty() { + return predicate; + } + + /// Returns the predicate, or `null` when everything is shown. + public final Predicate getPredicate() { + return predicate.get(); + } + + /// Sets the predicate; `null` shows everything. + public final void setPredicate(Predicate predicate) { + this.predicate.set(predicate); + } + + private boolean accepts(E element) { + Predicate current = getPredicate(); + return current == null || current.test(element); + } + + private void ensureCapacity(int capacity) { + if (filtered.length < capacity) { + filtered = Arrays.copyOf(filtered, capacity * 3 / 2 + 1); + } + } + + /// Returns the first view position whose source index is not below the + /// given one. + private int findPosition(int sourceIndex) { + int low = 0; + int high = size; + while (low < high) { + int mid = (low + high) >>> 1; + if (filtered[mid] < sourceIndex) { + low = mid + 1; + } else { + high = mid; + } + } + return low; + } + + private void refilter() { + ObservableList source = getSource(); + List removed = hasListeners() ? new ArrayList(this) : null; + int count = source.size(); + ensureCapacity(count); + size = 0; + for (int i = 0; i < count; i++) { + if (accepts(source.get(i))) { + filtered[size++] = i; + } + } + modCount++; + if (removed != null) { + beginChange(); + nextReplace(0, size, removed); + endChange(); + } + } + + @Override + protected void sourceChanged(Change c) { + beginChange(); + try { + while (c.next()) { + if (c.wasPermutated()) { + permutate(c); + } else if (c.wasUpdated()) { + update(c); + } else { + addRemove(c); + } + } + modCount++; + } finally { + endChange(); + } + } + + private void permutate(Change c) { + int first = findPosition(c.getFrom()); + int last = findPosition(c.getTo()); + int count = last - first; + if (count == 0) { + return; + } + // Each key is the new source index with the old view offset below it, + // so sorting the keys orders the shown elements by where they are now. + long[] keys = new long[count]; + for (int i = 0; i < count; i++) { + keys[i] = ((long) c.getPermutation(filtered[first + i]) << 32) | i; + } + Arrays.sort(keys); + int[] perm = new int[count]; + boolean moved = false; + for (int i = 0; i < count; i++) { + int oldOffset = (int) (keys[i] & 0xffffffffL); + perm[oldOffset] = first + i; + filtered[first + i] = (int) (keys[i] >>> 32); + moved |= oldOffset != i; + } + if (moved) { + nextPermutation(first, last, perm); + } + } + + private void update(Change c) { + ObservableList source = getSource(); + for (int i = c.getFrom(); i < c.getTo(); i++) { + E element = source.get(i); + int position = findPosition(i); + boolean shown = position < size && filtered[position] == i; + boolean matches = accepts(element); + if (shown && matches) { + nextUpdate(position); + } else if (shown) { + System.arraycopy(filtered, position + 1, filtered, position, size - position - 1); + size--; + nextRemove(position, element); + } else if (matches) { + ensureCapacity(size + 1); + System.arraycopy(filtered, position, filtered, position + 1, size - position); + filtered[position] = i; + size++; + nextAdd(position, position + 1); + } + } + } + + private void addRemove(Change c) { + ObservableList source = getSource(); + int from = c.getFrom(); + int removedSize = c.getRemovedSize(); + int addedSize = c.getAddedSize(); + if (removedSize > 0) { + int first = findPosition(from); + int last = findPosition(from + removedSize); + if (last > first) { + List removed = new ArrayList(last - first); + for (int p = first; p < last; p++) { + removed.add(c.getRemoved().get(filtered[p] - from)); + } + System.arraycopy(filtered, last, filtered, first, size - last); + size -= last - first; + nextRemove(first, removed); + } + for (int p = first; p < size; p++) { + filtered[p] -= removedSize; + } + } + if (addedSize > 0) { + int position = findPosition(from); + int[] accepted = new int[addedSize]; + int count = 0; + for (int i = from; i < from + addedSize; i++) { + if (accepts(source.get(i))) { + accepted[count++] = i; + } + } + ensureCapacity(size + count); + System.arraycopy(filtered, position, filtered, position + count, size - position); + System.arraycopy(accepted, 0, filtered, position, count); + size += count; + for (int p = position + count; p < size; p++) { + filtered[p] += addedSize; + } + if (count > 0) { + nextAdd(position, position + count); + } + } + } + + @Override + public int size() { + return size; + } + + @Override + public E get(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(); + } + return getSource().get(filtered[index]); + } + + @Override + public int getSourceIndex(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(); + } + return filtered[index]; + } + + @Override + public int getViewIndex(int index) { + int position = findPosition(index); + return position < size && filtered[position] == index ? position : -1; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/transformation/SortedList.java b/maven/javafx-compat/src/main/java/javafx/collections/transformation/SortedList.java new file mode 100644 index 00000000000..31f7dc24a7e --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/transformation/SortedList.java @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections.transformation; + +import java.util.Arrays; +import java.util.Comparator; + +import javafx.beans.NamedArg; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ObjectPropertyBase; +import javafx.collections.ListChangeListener.Change; +import javafx.collections.ObservableList; + +/// A live view of a list ordered by a comparator. With no comparator it +/// shows the source order. +/// +/// Elements the comparator calls equal keep their source order. The view +/// follows the source: an added element is inserted where it belongs, and an +/// element the source reports as updated moves if its order changed. +/// Changing the comparator is reported as a permutation. +public final class SortedList extends TransformationList { + + private Object[] elements; + private int[] indexes; + private int size; + private final ObjectProperty> comparator; + + /// Creates a view ordered by a comparator. + public SortedList(@NamedArg("source") ObservableList source, + @NamedArg("comparator") Comparator comparator) { + super(source); + size = source.size(); + elements = new Object[size * 3 / 2 + 1]; + indexes = new int[elements.length]; + for (int i = 0; i < size; i++) { + elements[i] = source.get(i); + indexes[i] = i; + } + this.comparator = new ObjectPropertyBase>(comparator) { + @Override + protected void invalidated() { + resort(); + } + + @Override + public Object getBean() { + return SortedList.this; + } + + @Override + public String getName() { + return "comparator"; + } + }; + resort(); + } + + /// Creates a view in source order. + public SortedList(@NamedArg("source") ObservableList source) { + this(source, null); + } + + /// The comparator ordering the view; `null` keeps the source order. + public final ObjectProperty> comparatorProperty() { + return comparator; + } + + /// Returns the comparator, or `null` when the view is in source order. + public final Comparator getComparator() { + return comparator.get(); + } + + /// Sets the comparator; `null` shows the source order. + public final void setComparator(Comparator comparator) { + this.comparator.set(comparator); + } + + /// Orders two entries by the comparator and then by source position, so + /// the order is total and equal elements stay in source order. + @SuppressWarnings("unchecked") + private int order(Object first, int firstIndex, Object second, int secondIndex) { + Comparator current = getComparator(); + if (current != null) { + int result = current.compare((E) first, (E) second); + if (result != 0) { + return result; + } + } + return firstIndex < secondIndex ? -1 : (firstIndex == secondIndex ? 0 : 1); + } + + private void ensureCapacity(int capacity) { + if (elements.length < capacity) { + int grown = capacity * 3 / 2 + 1; + elements = Arrays.copyOf(elements, grown); + indexes = Arrays.copyOf(indexes, grown); + } + } + + /// Sorts the whole view again and reports the reordering, if any. + private void resort() { + final Object[] oldElements = elements; + final int[] oldIndexes = indexes; + Integer[] positions = new Integer[size]; + for (int i = 0; i < size; i++) { + positions[i] = Integer.valueOf(i); + } + Arrays.sort(positions, new Comparator() { + @Override + public int compare(Integer a, Integer b) { + int x = a.intValue(); + int y = b.intValue(); + return order(oldElements[x], oldIndexes[x], oldElements[y], oldIndexes[y]); + } + }); + Object[] newElements = new Object[oldElements.length]; + int[] newIndexes = new int[oldIndexes.length]; + int[] perm = new int[size]; + boolean moved = false; + for (int i = 0; i < size; i++) { + int old = positions[i].intValue(); + newElements[i] = oldElements[old]; + newIndexes[i] = oldIndexes[old]; + perm[old] = i; + moved |= old != i; + } + elements = newElements; + indexes = newIndexes; + if (moved) { + modCount++; + if (hasListeners()) { + beginChange(); + nextPermutation(0, size, perm); + endChange(); + } + } + } + + /// Returns where an entry belongs among the first `count` entries. + private int insertionPoint(Object element, int sourceIndex, int count) { + int low = 0; + int high = count; + while (low < high) { + int mid = (low + high) >>> 1; + if (order(elements[mid], indexes[mid], element, sourceIndex) < 0) { + low = mid + 1; + } else { + high = mid; + } + } + return low; + } + + private void insert(int position, Object element, int sourceIndex) { + ensureCapacity(size + 1); + System.arraycopy(elements, position, elements, position + 1, size - position); + System.arraycopy(indexes, position, indexes, position + 1, size - position); + elements[position] = element; + indexes[position] = sourceIndex; + size++; + } + + private void delete(int position) { + System.arraycopy(elements, position + 1, elements, position, size - position - 1); + System.arraycopy(indexes, position + 1, indexes, position, size - position - 1); + size--; + elements[size] = null; + } + + @Override + protected void sourceChanged(Change c) { + beginChange(); + try { + while (c.next()) { + if (c.wasPermutated()) { + for (int i = 0; i < size; i++) { + if (indexes[i] >= c.getFrom() && indexes[i] < c.getTo()) { + indexes[i] = c.getPermutation(indexes[i]); + } + } + // Equal elements are ordered by source position, which + // just changed; with no comparator the whole view did. + resort(); + } else if (c.wasUpdated()) { + update(c); + } else { + addRemove(c); + } + } + modCount++; + } finally { + endChange(); + } + } + + private boolean inOrder() { + for (int i = 1; i < size; i++) { + if (order(elements[i - 1], indexes[i - 1], elements[i], indexes[i]) > 0) { + return false; + } + } + return true; + } + + @SuppressWarnings("unchecked") + private void update(Change c) { + int from = c.getFrom(); + int to = c.getTo(); + if (inOrder()) { + for (int i = 0; i < size; i++) { + if (indexes[i] >= from && indexes[i] < to) { + nextUpdate(i); + } + } + return; + } + // Take the updated elements out, which leaves a sorted remainder, + // and put each back where it now belongs. + int count = to - from; + Object[] moved = new Object[count]; + for (int i = size - 1; i >= 0; i--) { + if (indexes[i] >= from && indexes[i] < to) { + Object element = elements[i]; + moved[indexes[i] - from] = element; + delete(i); + nextRemove(i, (E) element); + } + } + for (int i = 0; i < count; i++) { + int position = insertionPoint(moved[i], from + i, size); + insert(position, moved[i], from + i); + nextAdd(position, position + 1); + } + } + + @SuppressWarnings("unchecked") + private void addRemove(Change c) { + ObservableList source = getSource(); + int from = c.getFrom(); + int removedSize = c.getRemovedSize(); + int addedSize = c.getAddedSize(); + if (removedSize > 0) { + for (int i = size - 1; i >= 0; i--) { + if (indexes[i] >= from && indexes[i] < from + removedSize) { + Object element = elements[i]; + delete(i); + nextRemove(i, (E) element); + } + } + for (int i = 0; i < size; i++) { + if (indexes[i] >= from) { + indexes[i] -= removedSize; + } + } + } + if (addedSize > 0) { + for (int i = 0; i < size; i++) { + if (indexes[i] >= from) { + indexes[i] += addedSize; + } + } + if (size == 0) { + // Filling an empty view: sort the lot once instead of + // inserting element by element. + ensureCapacity(addedSize); + for (int i = 0; i < addedSize; i++) { + elements[i] = source.get(from + i); + indexes[i] = from + i; + } + size = addedSize; + boolean listening = hasListeners(); + resortQuietly(); + if (listening) { + nextAdd(0, size); + } + } else { + for (int i = from; i < from + addedSize; i++) { + Object element = source.get(i); + int position = insertionPoint(element, i, size); + insert(position, element, i); + nextAdd(position, position + 1); + } + } + } + } + + /// Sorts without reporting; for content nobody has been told about yet. + private void resortQuietly() { + final Object[] oldElements = elements; + final int[] oldIndexes = indexes; + Integer[] positions = new Integer[size]; + for (int i = 0; i < size; i++) { + positions[i] = Integer.valueOf(i); + } + Arrays.sort(positions, new Comparator() { + @Override + public int compare(Integer a, Integer b) { + int x = a.intValue(); + int y = b.intValue(); + return order(oldElements[x], oldIndexes[x], oldElements[y], oldIndexes[y]); + } + }); + Object[] newElements = new Object[oldElements.length]; + int[] newIndexes = new int[oldIndexes.length]; + for (int i = 0; i < size; i++) { + int old = positions[i].intValue(); + newElements[i] = oldElements[old]; + newIndexes[i] = oldIndexes[old]; + } + elements = newElements; + indexes = newIndexes; + } + + @Override + public int size() { + return size; + } + + @Override + @SuppressWarnings("unchecked") + public E get(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(); + } + return (E) elements[index]; + } + + @Override + public int getSourceIndex(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(); + } + return indexes[index]; + } + + @Override + public int getViewIndex(int index) { + for (int i = 0; i < size; i++) { + if (indexes[i] == index) { + return i; + } + } + return -1; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/transformation/TransformationList.java b/maven/javafx-compat/src/main/java/javafx/collections/transformation/TransformationList.java new file mode 100644 index 00000000000..a1e20528218 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/transformation/TransformationList.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.collections.transformation; + +import java.util.List; + +import javafx.collections.ListChangeListener; +import javafx.collections.ListChangeListener.Change; +import javafx.collections.ObservableList; +import javafx.collections.ObservableListBase; +import javafx.collections.WeakListChangeListener; + +/// Base class for a list that presents another observable list differently +/// and follows its changes. The view observes its source weakly, so an +/// unused view is collected while the source lives on. +public abstract class TransformationList extends ObservableListBase { + + private final ObservableList source; + private final ListChangeListener sourceListener; + + /// Creates a view of a source list. + protected TransformationList(ObservableList source) { + if (source == null) { + throw new NullPointerException(); + } + this.source = source; + this.sourceListener = new ListChangeListener() { + @Override + public void onChanged(Change change) { + TransformationList.this.sourceChanged(change); + } + }; + source.addListener(new WeakListChangeListener(sourceListener)); + } + + /// Returns the list this one is a view of. + public final ObservableList getSource() { + return source; + } + + /// Returns whether a list is this view's source, or the source of its + /// source, and so on. + public final boolean isInTransformationChain(ObservableList list) { + List current = source; + while (current != list) { + if (!(current instanceof TransformationList)) { + return false; + } + current = ((TransformationList) current).source; + } + return true; + } + + /// Called with every change of the source. + protected abstract void sourceChanged(Change c); + + /// Returns the position in the source of the element at a position of + /// this view. + public abstract int getSourceIndex(int index); + + /// Returns the position, in a list somewhere down the chain of sources, + /// of the element at a position of this view. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when the list is not in the chain + public final int getSourceIndexFor(ObservableList list, int index) { + if (!isInTransformationChain(list)) { + throw new IllegalArgumentException( + "Provided list is not in the transformation chain of this transformation list"); + } + List current = source; + int result = getSourceIndex(index); + while (current != list && current instanceof TransformationList) { + TransformationList view = (TransformationList) current; + result = view.getSourceIndex(result); + current = view.source; + } + return result; + } + + /// Returns the position in this view of the element at a position of + /// the source, or -1 when the view does not show it. + public abstract int getViewIndex(int index); +} diff --git a/maven/javafx-compat/src/main/java/javafx/collections/transformation/package-info.java b/maven/javafx-compat/src/main/java/javafx/collections/transformation/package-info.java new file mode 100644 index 00000000000..205290a645c --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/collections/transformation/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Live views of an observable list: a filtered one, a sorted one and the +/// base class both are built on. +package javafx.collections.transformation; diff --git a/maven/javafx-compat/src/main/java/javafx/event/ActionEvent.java b/maven/javafx-compat/src/main/java/javafx/event/ActionEvent.java new file mode 100644 index 00000000000..a37f71da52f --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/event/ActionEvent.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.event; + +/// The event of a control being activated: a button pressed, a menu item +/// chosen, a text field committed. +public class ActionEvent extends Event { + + private static final long serialVersionUID = 1L; + + /// The only type of action event. + public static final EventType ACTION = new EventType(Event.ANY, "ACTION"); + + /// Every action event. + public static final EventType ANY = ACTION; + + /// Creates an action event with no source and no target. + public ActionEvent() { + super(ACTION); + } + + /// Creates an action event. + public ActionEvent(Object source, EventTarget target) { + super(source, target, ACTION); + } + + @Override + public ActionEvent copyFor(Object newSource, EventTarget newTarget) { + if (getClass() == ActionEvent.class) { + ActionEvent copy = new ActionEvent(newSource, newTarget); + copy.consumed = consumed; + return copy; + } + super.copyFor(newSource, newTarget); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public EventType getEventType() { + return (EventType) super.getEventType(); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/event/Event.java b/maven/javafx-compat/src/main/java/javafx/event/Event.java new file mode 100644 index 00000000000..76c4f326bba --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/event/Event.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.event; + +import java.util.EventObject; + +import com.codename1.fxcompat.runtime.EventDispatchChainImpl; + +import javafx.beans.NamedArg; + +/// Base class of every event: a type, the source the handler sees it coming +/// from, and the target it is being delivered to. +/// +/// An event is copied for each object it is delivered through. A subclass +/// that adds state must override [#copyFor(Object, EventTarget)] to build +/// the copy with its own constructor; one that does not is re-addressed in +/// place instead, because this runtime cannot clone an object of a class it +/// does not know. +public class Event extends EventObject { + + private static final long serialVersionUID = 1L; + + /// The target of an event that has none. + public static final EventTarget NULL_SOURCE_TARGET = new EventTarget() { + @Override + public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) { + return tail; + } + }; + + /// The type every event has. + public static final EventType ANY = EventType.ROOT; + + /// The type of the event. + protected EventType eventType; + + /// The target the event is being delivered to. + protected transient EventTarget target; + + /// Whether a handler consumed the event. + protected boolean consumed; + + /// Creates an event of a type with no source and no target. + public Event(final @NamedArg("eventType") EventType eventType) { + this(null, null, eventType); + } + + /// Creates an event. A missing source or target is replaced by + /// [#NULL_SOURCE_TARGET]. + public Event(final @NamedArg("source") Object source, final @NamedArg("target") EventTarget target, + final @NamedArg("eventType") EventType eventType) { + super((source != null) ? source : NULL_SOURCE_TARGET); + this.target = (target != null) ? target : NULL_SOURCE_TARGET; + this.eventType = eventType; + } + + /// Returns the target the event is being delivered to. + public EventTarget getTarget() { + return target; + } + + /// Returns the type of the event. + public EventType getEventType() { + return eventType; + } + + /// Returns this event addressed to a new source and target. A missing + /// source or target is replaced by [#NULL_SOURCE_TARGET]. + public Event copyFor(final Object newSource, final EventTarget newTarget) { + Object resolvedSource = (newSource != null) ? newSource : NULL_SOURCE_TARGET; + EventTarget resolvedTarget = (newTarget != null) ? newTarget : NULL_SOURCE_TARGET; + if (getClass() == Event.class) { + Event copy = new Event(resolvedSource, resolvedTarget, eventType); + copy.consumed = consumed; + return copy; + } + this.source = resolvedSource; + this.target = resolvedTarget; + return this; + } + + /// Returns whether a handler consumed the event. + public boolean isConsumed() { + return consumed; + } + + /// Marks the event consumed, which stops its delivery. + public void consume() { + consumed = true; + } + + /// Delivers an event to a target, through the dispatch chain the + /// target builds. + public static void fireEvent(EventTarget eventTarget, Event event) { + if (eventTarget == null) { + throw new NullPointerException("Event target must not be null!"); + } + if (event == null) { + throw new NullPointerException("Event must not be null!"); + } + Event addressed = event.getTarget() == eventTarget ? event : event.copyFor(eventTarget, eventTarget); + EventDispatchChain chain = eventTarget.buildEventDispatchChain(new EventDispatchChainImpl()); + chain.dispatchEvent(addressed); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/event/EventDispatchChain.java b/maven/javafx-compat/src/main/java/javafx/event/EventDispatchChain.java new file mode 100644 index 00000000000..af374820702 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/event/EventDispatchChain.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.event; + +/// The ordered dispatchers an event travels through. +public interface EventDispatchChain { + + /// Adds a dispatcher after the others and returns the resulting chain. + EventDispatchChain append(EventDispatcher eventDispatcher); + + /// Adds a dispatcher before the others and returns the resulting chain. + EventDispatchChain prepend(EventDispatcher eventDispatcher); + + /// Sends an event through the chain, starting at its first dispatcher. + /// Returns the resulting event, or `null` when it was consumed. + Event dispatchEvent(Event event); +} diff --git a/maven/javafx-compat/src/main/java/javafx/event/EventDispatcher.java b/maven/javafx-compat/src/main/java/javafx/event/EventDispatcher.java new file mode 100644 index 00000000000..18cda384320 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/event/EventDispatcher.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.event; + +/// One stop on the path of an event. It may handle the event, pass it on +/// through the rest of the chain, replace it or swallow it. +public interface EventDispatcher { + + /// Handles an event and returns the event to continue with, or `null` + /// when it was consumed. To let the event travel on, the dispatcher + /// passes it to the tail. + Event dispatchEvent(Event event, EventDispatchChain tail); +} diff --git a/maven/javafx-compat/src/main/java/javafx/event/EventHandler.java b/maven/javafx-compat/src/main/java/javafx/event/EventHandler.java new file mode 100644 index 00000000000..442be74b68e --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/event/EventHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.event; + +import java.util.EventListener; + +/// Receives the events of one kind. +@FunctionalInterface +public interface EventHandler extends EventListener { + + /// Called with an event. + void handle(T event); +} diff --git a/maven/javafx-compat/src/main/java/javafx/event/EventTarget.java b/maven/javafx-compat/src/main/java/javafx/event/EventTarget.java new file mode 100644 index 00000000000..356ba293f8c --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/event/EventTarget.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.event; + +/// Something events can be delivered to. It decides the path an event takes +/// by adding its dispatchers to the chain. +public interface EventTarget { + + /// Adds this target's dispatchers in front of a chain and returns the + /// chain to deliver through. + EventDispatchChain buildEventDispatchChain(EventDispatchChain tail); +} diff --git a/maven/javafx-compat/src/main/java/javafx/event/EventType.java b/maven/javafx-compat/src/main/java/javafx/event/EventType.java new file mode 100644 index 00000000000..284db95a5b4 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/event/EventType.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.event; + +import java.io.Serializable; + +/// The kind of an event. Types form a tree under [#ROOT]: a handler +/// registered for a type also receives the events of its sub types. +public final class EventType implements Serializable { + + private static final long serialVersionUID = 1L; + + /// The type every other type descends from. + public static final EventType ROOT = new EventType("EVENT", null); + + private final EventType superType; + private final String name; + + /// Creates an unnamed type directly under [#ROOT]. + /// + /// #### Deprecated + /// + /// A type without a name cannot be told apart in a log; name it. + @Deprecated + public EventType() { + this(ROOT, null); + } + + /// Creates a named type directly under [#ROOT]. + public EventType(final String name) { + this(ROOT, name); + } + + /// Creates an unnamed sub type. + public EventType(final EventType superType) { + this(superType, null); + } + + /// Creates a named sub type. + public EventType(final EventType superType, final String name) { + if (superType == null) { + throw new NullPointerException("Event super type must not be null!"); + } + this.superType = superType; + this.name = name; + } + + private EventType(final String name, final EventType superType) { + this.superType = superType; + this.name = name; + } + + /// Returns the type this one is a sub type of; `null` for [#ROOT]. + public final EventType getSuperType() { + return superType; + } + + /// Returns the name; `null` for an unnamed type. + public final String getName() { + return name; + } + + /// Returns the name, or the default text for an unnamed type. + @Override + public String toString() { + return (name != null) ? name : super.toString(); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/event/WeakEventHandler.java b/maven/javafx-compat/src/main/java/javafx/event/WeakEventHandler.java new file mode 100644 index 00000000000..70e97605eb2 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/event/WeakEventHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.event; + +import java.lang.ref.WeakReference; + +/// An [EventHandler] that forwards to another one without keeping it alive. +/// Whoever creates it must hold the wrapped handler. +public final class WeakEventHandler implements EventHandler { + + private final WeakReference ref; + + /// Wraps a handler. + public WeakEventHandler(final @javafx.beans.NamedArg("eventHandler") EventHandler eventHandler) { + this.ref = new WeakReference(eventHandler); + } + + /// Returns whether the wrapped handler has been garbage collected. + public boolean wasGarbageCollected() { + return ref.get() == null; + } + + @Override + @SuppressWarnings("unchecked") + public void handle(final T event) { + Object handler = ref.get(); + if (handler instanceof EventHandler) { + ((EventHandler) handler).handle(event); + } + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/event/package-info.java b/maven/javafx-compat/src/main/java/javafx/event/package-info.java new file mode 100644 index 00000000000..ee1bb3c54b0 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/event/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Events and their delivery: event types arranged in a hierarchy, the +/// handlers that receive events and the dispatch chain an event travels +/// along to reach its target. +package javafx.event; diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/BoundingBox.java b/maven/javafx-compat/src/main/java/javafx/geometry/BoundingBox.java new file mode 100644 index 00000000000..88da642b5f8 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/BoundingBox.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +import javafx.beans.NamedArg; + +/// Bounds given directly by a corner and a size. +public class BoundingBox extends Bounds { + + /// Creates a box in space. + public BoundingBox(@NamedArg("minX") double minX, @NamedArg("minY") double minY, @NamedArg("minZ") double minZ, + @NamedArg("width") double width, @NamedArg("height") double height, @NamedArg("depth") double depth) { + super(minX, minY, minZ, width, height, depth); + } + + /// Creates a flat box. + public BoundingBox(@NamedArg("minX") double minX, @NamedArg("minY") double minY, + @NamedArg("width") double width, @NamedArg("height") double height) { + super(minX, minY, 0, width, height, 0); + } + + @Override + public boolean isEmpty() { + return getMaxX() < getMinX() || getMaxY() < getMinY() || getMaxZ() < getMinZ(); + } + + @Override + public boolean contains(Point2D p) { + if (p == null) { + return false; + } + return contains(p.getX(), p.getY(), 0.0f); + } + + @Override + public boolean contains(double x, double y) { + return contains(x, y, 0.0f); + } + + @Override + public boolean contains(double x, double y, double z) { + if (isEmpty()) { + return false; + } + return x >= getMinX() && x <= getMaxX() && y >= getMinY() && y <= getMaxY() && z >= getMinZ() + && z <= getMaxZ(); + } + + @Override + public boolean contains(Bounds b) { + if (b == null || b.isEmpty()) { + return false; + } + return contains(b.getMinX(), b.getMinY(), b.getMinZ(), b.getWidth(), b.getHeight(), b.getDepth()); + } + + @Override + public boolean contains(double x, double y, double w, double h) { + return contains(x, y) && contains(x + w, y + h); + } + + @Override + public boolean contains(double x, double y, double z, double w, double h, double d) { + return contains(x, y, z) && contains(x + w, y + h, z + d); + } + + @Override + public boolean intersects(Bounds b) { + if (b == null || b.isEmpty()) { + return false; + } + return intersects(b.getMinX(), b.getMinY(), b.getMinZ(), b.getWidth(), b.getHeight(), b.getDepth()); + } + + @Override + public boolean intersects(double x, double y, double w, double h) { + return intersects(x, y, 0, w, h, 0); + } + + @Override + public boolean intersects(double x, double y, double z, double w, double h, double d) { + if (isEmpty() || w < 0 || h < 0 || d < 0) { + return false; + } + return x + w >= getMinX() && y + h >= getMinY() && z + d >= getMinZ() && x <= getMaxX() && y <= getMaxY() + && z <= getMaxZ(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof BoundingBox) { + BoundingBox other = (BoundingBox) obj; + return Geometry.same(getMinX(), other.getMinX()) && Geometry.same(getMinY(), other.getMinY()) + && Geometry.same(getMinZ(), other.getMinZ()) && Geometry.same(getWidth(), other.getWidth()) + && Geometry.same(getHeight(), other.getHeight()) && Geometry.same(getDepth(), other.getDepth()); + } + return false; + } + + @Override + public int hashCode() { + int hash = Geometry.hash(Geometry.hash(Geometry.hash(7, getMinX()), getMinY()), getMinZ()); + return Geometry.hash(Geometry.hash(Geometry.hash(hash, getWidth()), getHeight()), getDepth()); + } + + @Override + public String toString() { + return "BoundingBox [minX:" + getMinX() + ", minY:" + getMinY() + ", minZ:" + getMinZ() + ", width:" + + getWidth() + ", height:" + getHeight() + ", depth:" + getDepth() + ", maxX:" + getMaxX() + + ", maxY:" + getMaxY() + ", maxZ:" + getMaxZ() + "]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/Bounds.java b/maven/javafx-compat/src/main/java/javafx/geometry/Bounds.java new file mode 100644 index 00000000000..edb7b4715ae --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/Bounds.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +/// The immutable box around a node or any other object: a corner, a size +/// and, for three dimensional content, a depth. +public abstract class Bounds { + + private final double minX; + private final double minY; + private final double minZ; + private final double width; + private final double height; + private final double depth; + private final double maxX; + private final double maxY; + private final double maxZ; + + /// Creates bounds from a corner and a size. + protected Bounds(double minX, double minY, double minZ, double width, double height, double depth) { + this.minX = minX; + this.minY = minY; + this.minZ = minZ; + this.width = width; + this.height = height; + this.depth = depth; + this.maxX = minX + width; + this.maxY = minY + height; + this.maxZ = minZ + depth; + } + + /// Returns the left edge. + public final double getMinX() { + return minX; + } + + /// Returns the top edge. + public final double getMinY() { + return minY; + } + + /// Returns the near edge. + public final double getMinZ() { + return minZ; + } + + /// Returns the width. + public final double getWidth() { + return width; + } + + /// Returns the height. + public final double getHeight() { + return height; + } + + /// Returns the depth. + public final double getDepth() { + return depth; + } + + /// Returns the right edge. + public final double getMaxX() { + return maxX; + } + + /// Returns the bottom edge. + public final double getMaxY() { + return maxY; + } + + /// Returns the far edge. + public final double getMaxZ() { + return maxZ; + } + + /// Returns the horizontal center. + public final double getCenterX() { + return (getMaxX() + getMinX()) * 0.5; + } + + /// Returns the vertical center. + public final double getCenterY() { + return (getMaxY() + getMinY()) * 0.5; + } + + /// Returns the center in depth. + public final double getCenterZ() { + return (getMaxZ() + getMinZ()) * 0.5; + } + + /// Returns whether a size is negative, which marks bounds of nothing. + public abstract boolean isEmpty(); + + /// Returns whether a point lies inside or on the edge. + public abstract boolean contains(Point2D p); + + /// Returns whether a location lies inside or on the edge. + public abstract boolean contains(double x, double y); + + /// Returns whether a location in space lies inside or on the edge. + public abstract boolean contains(double x, double y, double z); + + /// Returns whether other bounds lie entirely inside these. + public abstract boolean contains(Bounds b); + + /// Returns whether a rectangle lies entirely inside these bounds. + public abstract boolean contains(double x, double y, double w, double h); + + /// Returns whether a box lies entirely inside these bounds. + public abstract boolean contains(double x, double y, double z, double w, double h, double d); + + /// Returns whether other bounds overlap or touch these. + public abstract boolean intersects(Bounds b); + + /// Returns whether a rectangle overlaps or touches these bounds. + public abstract boolean intersects(double x, double y, double w, double h); + + /// Returns whether a box overlaps or touches these bounds. + public abstract boolean intersects(double x, double y, double z, double w, double h, double d); +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/Dimension2D.java b/maven/javafx-compat/src/main/java/javafx/geometry/Dimension2D.java new file mode 100644 index 00000000000..6aa226f5d0a --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/Dimension2D.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +import javafx.beans.NamedArg; + +/// An immutable width and height. +public class Dimension2D { + + private final double width; + private final double height; + + /// Creates a size. + public Dimension2D(@NamedArg("width") double width, @NamedArg("height") double height) { + this.width = width; + this.height = height; + } + + /// Returns the width. + public final double getWidth() { + return width; + } + + /// Returns the height. + public final double getHeight() { + return height; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Dimension2D) { + Dimension2D other = (Dimension2D) obj; + return Geometry.same(getWidth(), other.getWidth()) && Geometry.same(getHeight(), other.getHeight()); + } + return false; + } + + @Override + public int hashCode() { + return Geometry.hash(Geometry.hash(7, getWidth()), getHeight()); + } + + @Override + public String toString() { + return "Dimension2D [width = " + getWidth() + ", height = " + getHeight() + "]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/Geometry.java b/maven/javafx-compat/src/main/java/javafx/geometry/Geometry.java new file mode 100644 index 00000000000..21b246d0017 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/Geometry.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +/// Value comparison and hashing of coordinates, shared by the geometry +/// classes. Coordinates are equal when `==` says so, as in JavaFX: positive +/// and negative zero are the same coordinate and NaN equals nothing. +final class Geometry { + + private Geometry() { + } + + static boolean same(double first, double second) { + return first == second; + } + + static int hash(int seed, double value) { + // Negative zero equals zero, so both must hash alike. + long bits = Double.doubleToLongBits(value == 0.0 ? 0.0 : value); + return 37 * seed + (int) (bits ^ (bits >>> 32)); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/HPos.java b/maven/javafx-compat/src/main/java/javafx/geometry/HPos.java new file mode 100644 index 00000000000..3aa355e1162 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/HPos.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +/// A horizontal position. +public enum HPos { + /// At the left. + LEFT, + /// In the middle. + CENTER, + /// At the right. + RIGHT +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/HorizontalDirection.java b/maven/javafx-compat/src/main/java/javafx/geometry/HorizontalDirection.java new file mode 100644 index 00000000000..62f776f6ad6 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/HorizontalDirection.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +/// A direction along the horizontal axis. +public enum HorizontalDirection { + /// Towards the left. + LEFT, + /// Towards the right. + RIGHT +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/Insets.java b/maven/javafx-compat/src/main/java/javafx/geometry/Insets.java new file mode 100644 index 00000000000..74d21c25b9a --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/Insets.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +import javafx.beans.NamedArg; + +/// The four distances inside the edges of a rectangle. +public class Insets { + + /// No distance on any side. + public static final Insets EMPTY = new Insets(0, 0, 0, 0); + + private final double top; + private final double right; + private final double bottom; + private final double left; + + /// Creates insets with a distance for each side. + public Insets(@NamedArg("top") double top, @NamedArg("right") double right, @NamedArg("bottom") double bottom, + @NamedArg("left") double left) { + this.top = top; + this.right = right; + this.bottom = bottom; + this.left = left; + } + + /// Creates insets with the same distance on every side. + public Insets(@NamedArg("topRightBottomLeft") double topRightBottomLeft) { + this(topRightBottomLeft, topRightBottomLeft, topRightBottomLeft, topRightBottomLeft); + } + + /// Returns the distance at the top. + public final double getTop() { + return top; + } + + /// Returns the distance at the right. + public final double getRight() { + return right; + } + + /// Returns the distance at the bottom. + public final double getBottom() { + return bottom; + } + + /// Returns the distance at the left. + public final double getLeft() { + return left; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Insets) { + Insets other = (Insets) obj; + return Geometry.same(top, other.top) && Geometry.same(right, other.right) + && Geometry.same(bottom, other.bottom) && Geometry.same(left, other.left); + } + return false; + } + + @Override + public int hashCode() { + return Geometry.hash(Geometry.hash(Geometry.hash(Geometry.hash(17, top), right), bottom), left); + } + + @Override + public String toString() { + return "Insets [top=" + top + ", right=" + right + ", bottom=" + bottom + ", left=" + left + "]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/NodeOrientation.java b/maven/javafx-compat/src/main/java/javafx/geometry/NodeOrientation.java new file mode 100644 index 00000000000..29df9bd8cad --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/NodeOrientation.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +/// The reading direction a node lays its content out in. +public enum NodeOrientation { + /// Content starts at the left. + LEFT_TO_RIGHT, + /// Content starts at the right. + RIGHT_TO_LEFT, + /// The direction of the parent. + INHERIT +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/Orientation.java b/maven/javafx-compat/src/main/java/javafx/geometry/Orientation.java new file mode 100644 index 00000000000..a5c1a603f50 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/Orientation.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +/// The direction something is laid out in. +public enum Orientation { + /// Left to right. + HORIZONTAL, + /// Top to bottom. + VERTICAL +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/Point2D.java b/maven/javafx-compat/src/main/java/javafx/geometry/Point2D.java new file mode 100644 index 00000000000..fade19a981d --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/Point2D.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +import com.codename1.util.MathUtil; + +import javafx.beans.NamedArg; + +/// An immutable point in the plane, also used as a vector. +public class Point2D { + + /// The origin. + public static final Point2D ZERO = new Point2D(0.0, 0.0); + + private final double x; + private final double y; + + /// Creates a point. + public Point2D(@NamedArg("x") double x, @NamedArg("y") double y) { + this.x = x; + this.y = y; + } + + /// Returns the horizontal coordinate. + public final double getX() { + return x; + } + + /// Returns the vertical coordinate. + public final double getY() { + return y; + } + + /// Returns the distance to a location. + public double distance(double x1, double y1) { + double a = getX() - x1; + double b = getY() - y1; + return Math.sqrt(a * a + b * b); + } + + /// Returns the distance to another point. + public double distance(Point2D point) { + return distance(point.getX(), point.getY()); + } + + /// Returns this point moved by an offset. + public Point2D add(double x, double y) { + return new Point2D(getX() + x, getY() + y); + } + + /// Returns the sum of this vector and another. + public Point2D add(Point2D point) { + return add(point.getX(), point.getY()); + } + + /// Returns this point moved back by an offset. + public Point2D subtract(double x, double y) { + return new Point2D(getX() - x, getY() - y); + } + + /// Returns this vector less another. + public Point2D subtract(Point2D point) { + return subtract(point.getX(), point.getY()); + } + + /// Returns this vector scaled by a factor. + public Point2D multiply(double factor) { + return new Point2D(getX() * factor, getY() * factor); + } + + /// Returns the vector of the same direction and length one; the zero + /// vector for the zero vector. + public Point2D normalize() { + final double mag = magnitude(); + if (mag == 0.0) { + return new Point2D(0.0, 0.0); + } + return new Point2D(getX() / mag, getY() / mag); + } + + /// Returns the point halfway to a location. + public Point2D midpoint(double x, double y) { + return new Point2D(x + (getX() - x) / 2.0, y + (getY() - y) / 2.0); + } + + /// Returns the point halfway to another point. + public Point2D midpoint(Point2D point) { + return midpoint(point.getX(), point.getY()); + } + + /// Returns the angle in degrees, from 0 to 180, between this vector and + /// the vector to a location. + public double angle(double x, double y) { + final double ax = getX(); + final double ay = getY(); + final double delta = (ax * x + ay * y) / Math.sqrt((ax * ax + ay * ay) * (x * x + y * y)); + if (delta > 1.0) { + return 0.0; + } + if (delta < -1.0) { + return 180.0; + } + return Math.toDegrees(MathUtil.acos(delta)); + } + + /// Returns the angle in degrees between this vector and another. + public double angle(Point2D point) { + return angle(point.getX(), point.getY()); + } + + /// Returns the angle in degrees at this point between the directions + /// to two others. + public double angle(Point2D p1, Point2D p2) { + final double x = getX(); + final double y = getY(); + final double ax = p1.getX() - x; + final double ay = p1.getY() - y; + final double bx = p2.getX() - x; + final double by = p2.getY() - y; + final double delta = (ax * bx + ay * by) / Math.sqrt((ax * ax + ay * ay) * (bx * bx + by * by)); + if (delta > 1.0) { + return 0.0; + } + if (delta < -1.0) { + return 180.0; + } + return Math.toDegrees(MathUtil.acos(delta)); + } + + /// Returns the length of this vector. + public double magnitude() { + final double x = getX(); + final double y = getY(); + return Math.sqrt(x * x + y * y); + } + + /// Returns the dot product of this vector and another. + public double dotProduct(double x, double y) { + return getX() * x + getY() * y; + } + + /// Returns the dot product of this vector and another. + public double dotProduct(Point2D vector) { + return dotProduct(vector.getX(), vector.getY()); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Point2D) { + Point2D other = (Point2D) obj; + return Geometry.same(getX(), other.getX()) && Geometry.same(getY(), other.getY()); + } + return false; + } + + @Override + public int hashCode() { + return Geometry.hash(Geometry.hash(7, getX()), getY()); + } + + @Override + public String toString() { + return "Point2D [x = " + getX() + ", y = " + getY() + "]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/Pos.java b/maven/javafx-compat/src/main/java/javafx/geometry/Pos.java new file mode 100644 index 00000000000..d62753a71f7 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/Pos.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +/// A vertical and a horizontal position combined. +public enum Pos { + /// Top, left. + TOP_LEFT(VPos.TOP, HPos.LEFT), + /// Top, centered horizontally. + TOP_CENTER(VPos.TOP, HPos.CENTER), + /// Top, right. + TOP_RIGHT(VPos.TOP, HPos.RIGHT), + /// Centered vertically, left. + CENTER_LEFT(VPos.CENTER, HPos.LEFT), + /// Centered both ways. + CENTER(VPos.CENTER, HPos.CENTER), + /// Centered vertically, right. + CENTER_RIGHT(VPos.CENTER, HPos.RIGHT), + /// Bottom, left. + BOTTOM_LEFT(VPos.BOTTOM, HPos.LEFT), + /// Bottom, centered horizontally. + BOTTOM_CENTER(VPos.BOTTOM, HPos.CENTER), + /// Bottom, right. + BOTTOM_RIGHT(VPos.BOTTOM, HPos.RIGHT), + /// On the baseline, left. + BASELINE_LEFT(VPos.BASELINE, HPos.LEFT), + /// On the baseline, centered horizontally. + BASELINE_CENTER(VPos.BASELINE, HPos.CENTER), + /// On the baseline, right. + BASELINE_RIGHT(VPos.BASELINE, HPos.RIGHT); + + private final VPos vpos; + private final HPos hpos; + + Pos(VPos vpos, HPos hpos) { + this.vpos = vpos; + this.hpos = hpos; + } + + /// Returns the vertical part. + public VPos getVpos() { + return vpos; + } + + /// Returns the horizontal part. + public HPos getHpos() { + return hpos; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/Rectangle2D.java b/maven/javafx-compat/src/main/java/javafx/geometry/Rectangle2D.java new file mode 100644 index 00000000000..2447487c6e8 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/Rectangle2D.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +import javafx.beans.NamedArg; + +/// An immutable rectangle given by its upper left corner and its size. +public class Rectangle2D { + + /// The rectangle at the origin with no size. + public static final Rectangle2D EMPTY = new Rectangle2D(0, 0, 0, 0); + + private final double minX; + private final double minY; + private final double width; + private final double height; + private final double maxX; + private final double maxY; + + /// Creates a rectangle. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when the width or the height is negative + public Rectangle2D(@NamedArg("minX") double minX, @NamedArg("minY") double minY, + @NamedArg("width") double width, @NamedArg("height") double height) { + if (width < 0 || height < 0) { + throw new IllegalArgumentException("Both width and height must be >= 0"); + } + this.minX = minX; + this.minY = minY; + this.width = width; + this.height = height; + this.maxX = minX + width; + this.maxY = minY + height; + } + + /// Returns the left edge. + public double getMinX() { + return minX; + } + + /// Returns the top edge. + public double getMinY() { + return minY; + } + + /// Returns the width. + public double getWidth() { + return width; + } + + /// Returns the height. + public double getHeight() { + return height; + } + + /// Returns the right edge. + public double getMaxX() { + return maxX; + } + + /// Returns the bottom edge. + public double getMaxY() { + return maxY; + } + + /// Returns whether a point lies inside or on the edge. + public boolean contains(Point2D p) { + if (p == null) { + return false; + } + return contains(p.getX(), p.getY()); + } + + /// Returns whether a location lies inside or on the edge. + public boolean contains(double x, double y) { + return x >= minX && x <= maxX && y >= minY && y <= maxY; + } + + /// Returns whether another rectangle lies entirely inside this one. + public boolean contains(Rectangle2D r) { + if (r == null) { + return false; + } + return r.minX >= minX && r.minY >= minY && r.maxX <= maxX && r.maxY <= maxY; + } + + /// Returns whether a rectangle lies entirely inside this one. + public boolean contains(double x, double y, double w, double h) { + return x >= minX && y >= minY && w <= maxX - x && h <= maxY - y; + } + + /// Returns whether another rectangle overlaps or touches this one. + public boolean intersects(Rectangle2D r) { + if (r == null) { + return false; + } + return r.maxX > minX && r.maxY > minY && r.minX < maxX && r.minY < maxY; + } + + /// Returns whether a rectangle overlaps this one. + public boolean intersects(double x, double y, double w, double h) { + return x < maxX && y < maxY && x + w > minX && y + h > minY; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Rectangle2D) { + Rectangle2D other = (Rectangle2D) obj; + return Geometry.same(minX, other.minX) && Geometry.same(minY, other.minY) + && Geometry.same(width, other.width) && Geometry.same(height, other.height); + } + return false; + } + + @Override + public int hashCode() { + return Geometry.hash(Geometry.hash(Geometry.hash(Geometry.hash(7, minX), minY), width), height); + } + + @Override + public String toString() { + return "Rectangle2D [minX = " + minX + ", minY=" + minY + ", maxX=" + maxX + ", maxY=" + maxY + ", width=" + + width + ", height=" + height + "]"; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/Side.java b/maven/javafx-compat/src/main/java/javafx/geometry/Side.java new file mode 100644 index 00000000000..fddb4b47023 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/Side.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +/// A side of a rectangle. +public enum Side { + /// The top side. + TOP, + /// The bottom side. + BOTTOM, + /// The left side. + LEFT, + /// The right side. + RIGHT; + + /// Returns whether this is the left or the right side. + public boolean isVertical() { + return this == LEFT || this == RIGHT; + } + + /// Returns whether this is the top or the bottom side. + public boolean isHorizontal() { + return this == TOP || this == BOTTOM; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/VPos.java b/maven/javafx-compat/src/main/java/javafx/geometry/VPos.java new file mode 100644 index 00000000000..59f0320dce5 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/VPos.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +/// A vertical position. +public enum VPos { + /// At the top. + TOP, + /// In the middle. + CENTER, + /// On the text baseline. + BASELINE, + /// At the bottom. + BOTTOM +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/VerticalDirection.java b/maven/javafx-compat/src/main/java/javafx/geometry/VerticalDirection.java new file mode 100644 index 00000000000..94427c05f07 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/VerticalDirection.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.geometry; + +/// A direction along the vertical axis. +public enum VerticalDirection { + /// Towards the top. + UP, + /// Towards the bottom. + DOWN +} diff --git a/maven/javafx-compat/src/main/java/javafx/geometry/package-info.java b/maven/javafx-compat/src/main/java/javafx/geometry/package-info.java new file mode 100644 index 00000000000..a2f8f296d4f --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/geometry/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Immutable geometry values -- points, sizes, rectangles, bounds, insets -- +/// and the enumerations that name positions and directions. +package javafx.geometry; diff --git a/maven/javafx-compat/src/main/java/javafx/util/Callback.java b/maven/javafx-compat/src/main/java/javafx/util/Callback.java new file mode 100644 index 00000000000..7cf2b88012a --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/Callback.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.util; + +/// A function of one argument, used wherever the API asks the application +/// to produce something on demand. +@FunctionalInterface +public interface Callback { + + /// Computes the result for an argument. + R call(P param); +} diff --git a/maven/javafx-compat/src/main/java/javafx/util/Duration.java b/maven/javafx-compat/src/main/java/javafx/util/Duration.java new file mode 100644 index 00000000000..56e07164a86 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/Duration.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.util; + +import java.io.Serializable; + +import javafx.beans.NamedArg; + +/// An immutable length of time, kept in milliseconds. It may be negative, +/// [#INDEFINITE] (infinitely long) or [#UNKNOWN] (not a number). +public class Duration implements Comparable, Serializable { + + private static final long serialVersionUID = 1L; + + /// No time at all. + public static final Duration ZERO = new Duration(0); + + /// One millisecond. + public static final Duration ONE = new Duration(1); + + /// An infinitely long time. + public static final Duration INDEFINITE = new Duration(Double.POSITIVE_INFINITY); + + /// A time that is not known. + public static final Duration UNKNOWN = new Duration(Double.NaN); + + private final double millis; + + /// Creates a duration of a number of milliseconds. + public Duration(@NamedArg("millis") double millis) { + this.millis = millis; + } + + /// Parses a number followed directly by a unit: `ms`, `s`, `m` or `h`, + /// in either case. `"1.5s"` is one and a half seconds. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: when the text is not such a value + public static Duration valueOf(String time) { + if (time == null) { + throw new NullPointerException("time"); + } + int split = -1; + for (int i = 0; i < time.length(); i++) { + char c = time.charAt(i); + if (!(c >= '0' && c <= '9') && c != '.' && c != '-') { + split = i; + break; + } + } + if (split == -1) { + throw new IllegalArgumentException("The time parameter must have a suffix of [ms|s|m|h]"); + } + double value; + try { + value = Double.parseDouble(time.substring(0, split)); + } catch (NumberFormatException malformed) { + throw new IllegalArgumentException("The time parameter must be a number followed by [ms|s|m|h]"); + } + // Units are ASCII by definition, so they are folded by hand: the + // locale sensitive String.toLowerCase() would turn "S" into something + // else on a Turkish device. + String unit = asciiLower(time.substring(split)); + if ("ms".equals(unit)) { + return millis(value); + } + if ("s".equals(unit)) { + return seconds(value); + } + if ("m".equals(unit)) { + return minutes(value); + } + if ("h".equals(unit)) { + return hours(value); + } + throw new IllegalArgumentException("The time parameter must have a suffix of [ms|s|m|h]"); + } + + private static String asciiLower(String text) { + StringBuilder result = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + result.append(c >= 'A' && c <= 'Z' ? (char) (c + ('a' - 'A')) : c); + } + return result.toString(); + } + + /// Returns a duration of a number of milliseconds. + public static Duration millis(double ms) { + if (ms == 0) { + return ZERO; + } + if (ms == 1) { + return ONE; + } + if (ms == Double.POSITIVE_INFINITY) { + return INDEFINITE; + } + if (Double.isNaN(ms)) { + return UNKNOWN; + } + return new Duration(ms); + } + + /// Returns a duration of a number of seconds. + public static Duration seconds(double s) { + return millis(s * 1000.0); + } + + /// Returns a duration of a number of minutes. + public static Duration minutes(double m) { + return millis(m * (1000.0 * 60.0)); + } + + /// Returns a duration of a number of hours. + public static Duration hours(double h) { + return millis(h * (1000.0 * 60.0 * 60.0)); + } + + /// Returns the length in milliseconds. + public double toMillis() { + return millis; + } + + /// Returns the length in seconds. + public double toSeconds() { + return millis / 1000.0; + } + + /// Returns the length in minutes. + public double toMinutes() { + return millis / (60 * 1000.0); + } + + /// Returns the length in hours. + public double toHours() { + return millis / (60 * 60 * 1000.0); + } + + /// Returns the sum of this duration and another. + public Duration add(Duration other) { + return millis(millis + other.millis); + } + + /// Returns this duration less another. + public Duration subtract(Duration other) { + return millis(millis - other.millis); + } + + /// Returns the product of the two lengths in milliseconds. + /// + /// #### Deprecated + /// + /// Multiplying two durations has no meaning; use [#multiply(double)]. + @Deprecated + public Duration multiply(Duration other) { + return millis(millis * other.millis); + } + + /// Returns this duration scaled by a factor. + public Duration multiply(double n) { + return millis(millis * n); + } + + /// Returns this duration divided by a number. + public Duration divide(double n) { + return millis(millis / n); + } + + /// Returns the quotient of the two lengths in milliseconds. + /// + /// #### Deprecated + /// + /// The result is not a duration; divide [#toMillis()] values instead. + @Deprecated + public Duration divide(Duration other) { + return millis(millis / other.millis); + } + + /// Returns the duration of the same length and opposite sign. + public Duration negate() { + return millis(-millis); + } + + /// Returns whether this is infinitely long. + public boolean isIndefinite() { + return millis == Double.POSITIVE_INFINITY; + } + + /// Returns whether the length is not a number. + public boolean isUnknown() { + return Double.isNaN(millis); + } + + /// Returns whether this is shorter than another duration. + public boolean lessThan(Duration other) { + return millis < other.millis; + } + + /// Returns whether this is not longer than another duration. + public boolean lessThanOrEqualTo(Duration other) { + return millis <= other.millis; + } + + /// Returns whether this is longer than another duration. + public boolean greaterThan(Duration other) { + return millis > other.millis; + } + + /// Returns whether this is not shorter than another duration. + public boolean greaterThanOrEqualTo(Duration other) { + return millis >= other.millis; + } + + /// Returns `INDEFINITE`, `UNKNOWN` or the milliseconds followed by + /// ` ms`. + @Override + public String toString() { + return isIndefinite() ? "INDEFINITE" : (isUnknown() ? "UNKNOWN" : millis + " ms"); + } + + @Override + public int compareTo(Duration d) { + return Double.compare(millis, d.millis); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + return obj instanceof Duration && Double.compare(((Duration) obj).millis, millis) == 0; + } + + @Override + public int hashCode() { + long bits = Double.doubleToLongBits(millis); + return (int) (bits ^ (bits >>> 32)); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/util/Pair.java b/maven/javafx-compat/src/main/java/javafx/util/Pair.java new file mode 100644 index 00000000000..f000698a7e7 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/Pair.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.util; + +import java.io.Serializable; + +import javafx.beans.NamedArg; + +/// An immutable key and value. +public class Pair implements Serializable { + + private static final long serialVersionUID = 1L; + + private final K key; + private final V value; + + /// Creates a pair. + public Pair(@NamedArg("key") K key, @NamedArg("value") V value) { + this.key = key; + this.value = value; + } + + /// Returns the key. + public K getKey() { + return key; + } + + /// Returns the value. + public V getValue() { + return value; + } + + /// Returns the pair as `key=value`. + @Override + public String toString() { + return key + "=" + value; + } + + @Override + public int hashCode() { + return 31 * (key == null ? 0 : key.hashCode()) + (value == null ? 0 : value.hashCode()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof Pair) { + Pair other = (Pair) o; + return (key == null ? other.key == null : key.equals(other.key)) + && (value == null ? other.value == null : value.equals(other.value)); + } + return false; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/util/StringConverter.java b/maven/javafx-compat/src/main/java/javafx/util/StringConverter.java new file mode 100644 index 00000000000..e6bbf4dfa5c --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/StringConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.util; + +/// Converts between objects of one type and their text. +public abstract class StringConverter { + + /// Creates the converter. + public StringConverter() { + } + + /// Returns the text of an object. + public abstract String toString(T object); + + /// Returns the object a text stands for. + public abstract T fromString(String string); +} diff --git a/maven/javafx-compat/src/main/java/javafx/util/converter/BooleanStringConverter.java b/maven/javafx-compat/src/main/java/javafx/util/converter/BooleanStringConverter.java new file mode 100644 index 00000000000..5bdb8b80438 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/converter/BooleanStringConverter.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.util.converter; + +import javafx.util.StringConverter; + +/// Converts between `Boolean` and `"true"` or `"false"`. Parsing ignores +/// surrounding blanks and case; empty text is `null`, anything but `true` +/// is `false`. +public class BooleanStringConverter extends StringConverter { + + /// Creates the converter. + public BooleanStringConverter() { + } + + @Override + public Boolean fromString(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + if (text.length() < 1) { + return null; + } + return Boolean.valueOf("true".equalsIgnoreCase(text)); + } + + @Override + public String toString(Boolean value) { + if (value == null) { + return ""; + } + return value.toString(); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/util/converter/DefaultStringConverter.java b/maven/javafx-compat/src/main/java/javafx/util/converter/DefaultStringConverter.java new file mode 100644 index 00000000000..99f83c08cce --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/converter/DefaultStringConverter.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.util.converter; + +import javafx.util.StringConverter; + +/// Converts a string to itself; `null` becomes the empty string when +/// formatted and stays `null` when parsed. +public class DefaultStringConverter extends StringConverter { + + /// Creates the converter. + public DefaultStringConverter() { + } + + @Override + public String toString(String value) { + return value != null ? value : ""; + } + + @Override + public String fromString(String value) { + return value; + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/util/converter/DoubleStringConverter.java b/maven/javafx-compat/src/main/java/javafx/util/converter/DoubleStringConverter.java new file mode 100644 index 00000000000..2eac8efec53 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/converter/DoubleStringConverter.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.util.converter; + +import javafx.util.StringConverter; + +/// Converts between `Double` and the text `Double.toString` produces. +/// Parsing ignores surrounding blanks, turns empty text into `null` and +/// throws `NumberFormatException` for anything else that is not a number. +public class DoubleStringConverter extends StringConverter { + + /// Creates the converter. + public DoubleStringConverter() { + } + + @Override + public Double fromString(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + if (text.length() < 1) { + return null; + } + return Double.valueOf(text); + } + + @Override + public String toString(Double value) { + if (value == null) { + return ""; + } + return Double.toString(value.doubleValue()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/util/converter/IntegerStringConverter.java b/maven/javafx-compat/src/main/java/javafx/util/converter/IntegerStringConverter.java new file mode 100644 index 00000000000..35a35a081ec --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/converter/IntegerStringConverter.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.util.converter; + +import javafx.util.StringConverter; + +/// Converts between `Integer` and its decimal text. Parsing ignores +/// surrounding blanks, turns empty text into `null` and throws +/// `NumberFormatException` for anything else that is not an integer. +public class IntegerStringConverter extends StringConverter { + + /// Creates the converter. + public IntegerStringConverter() { + } + + @Override + public Integer fromString(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + if (text.length() < 1) { + return null; + } + return Integer.valueOf(text); + } + + @Override + public String toString(Integer value) { + if (value == null) { + return ""; + } + return Integer.toString(value.intValue()); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/util/converter/NumberStringConverter.java b/maven/javafx-compat/src/main/java/javafx/util/converter/NumberStringConverter.java new file mode 100644 index 00000000000..8961cb91f8f --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/converter/NumberStringConverter.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package javafx.util.converter; + +import javafx.util.StringConverter; + +/// Converts between `Number` and its text. +/// +/// Numbers are written the way the default number format of an English +/// locale writes them, on every device: digits grouped in threes with a +/// comma, a dot before the fraction, at most three fraction digits rounded +/// half to even, and no trailing zeros. Parsing accepts the same form and +/// returns a `Long` for a whole number and a `Double` otherwise. +public class NumberStringConverter extends StringConverter { + + /// Creates the converter. + public NumberStringConverter() { + } + + /// Parses a number. Surrounding blanks are ignored and empty text is + /// `null`. + /// + /// #### Throws + /// + /// - `RuntimeException`: when the text is not a number + @Override + public Number fromString(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + if (text.length() < 1) { + return null; + } + StringBuilder plain = new StringBuilder(text.length()); + boolean fraction = false; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == ',' && !fraction) { + continue; + } + if (c == '.' || c == 'e' || c == 'E') { + fraction = true; + } + plain.append(c); + } + try { + if (!fraction) { + return Long.valueOf(Long.parseLong(plain.toString())); + } + double parsed = Double.parseDouble(plain.toString()); + if (parsed == Math.floor(parsed) && Math.abs(parsed) < 9.0e18) { + return Long.valueOf((long) parsed); + } + return Double.valueOf(parsed); + } catch (NumberFormatException malformed) { + throw new RuntimeException("Unparseable number: \"" + value + "\"", malformed); + } + } + + @Override + public String toString(Number value) { + if (value == null) { + return ""; + } + if (value instanceof Integer || value instanceof Long || value instanceof Short || value instanceof Byte) { + long whole = value.longValue(); + if (whole == Long.MIN_VALUE) { + return "-9,223,372,036,854,775,808"; + } + return (whole < 0 ? "-" : "") + group(Long.toString(Math.abs(whole))); + } + double number = value.doubleValue(); + if (Double.isNaN(number)) { + return "NaN"; + } + if (Double.isInfinite(number)) { + return number > 0 ? "Infinity" : "-Infinity"; + } + double magnitude = Math.abs(number); + if (magnitude >= 9.0e15) { + // Beyond this a double has no fraction digits left to print. + return (number < 0 ? "-" : "") + group(plainDigits(magnitude)); + } + long whole = (long) Math.floor(magnitude); + long thousandths = roundHalfEven((magnitude - whole) * 1000.0); + if (thousandths == 1000) { + whole++; + thousandths = 0; + } + StringBuilder result = new StringBuilder(); + if (number < 0 && (whole != 0 || thousandths != 0)) { + result.append('-'); + } + result.append(group(Long.toString(whole))); + if (thousandths != 0) { + String digits = Long.toString(thousandths); + while (digits.length() < 3) { + digits = "0" + digits; + } + int end = digits.length(); + while (digits.charAt(end - 1) == '0') { + end--; + } + result.append('.').append(digits.substring(0, end)); + } + return result.toString(); + } + + private static long roundHalfEven(double value) { + double floor = Math.floor(value); + double rest = value - floor; + long result = (long) floor; + if (rest > 0.5 || (rest == 0.5 && (result & 1) == 1)) { + result++; + } + return result; + } + + /// The digits of a whole number too large for a `long`. + private static String plainDigits(double magnitude) { + if (magnitude < 9.0e18) { + return Long.toString((long) magnitude); + } + StringBuilder digits = new StringBuilder(); + double rest = magnitude; + while (rest >= 1.0) { + double tenth = Math.floor(rest / 10.0); + digits.insert(0, (char) ('0' + (int) (rest - tenth * 10.0))); + rest = tenth; + } + return digits.toString(); + } + + private static String group(String digits) { + StringBuilder result = new StringBuilder(digits.length() + digits.length() / 3); + int lead = digits.length() % 3; + for (int i = 0; i < digits.length(); i++) { + if (i > 0 && (i - lead) % 3 == 0) { + result.append(','); + } + result.append(digits.charAt(i)); + } + return result.toString(); + } +} diff --git a/maven/javafx-compat/src/main/java/javafx/util/converter/package-info.java b/maven/javafx-compat/src/main/java/javafx/util/converter/package-info.java new file mode 100644 index 00000000000..7d1c2d0a114 --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/converter/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The standard string converters for text, booleans and numbers. +package javafx.util.converter; diff --git a/maven/javafx-compat/src/main/java/javafx/util/package-info.java b/maven/javafx-compat/src/main/java/javafx/util/package-info.java new file mode 100644 index 00000000000..c3d093a9d6d --- /dev/null +++ b/maven/javafx-compat/src/main/java/javafx/util/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Small general purpose types used across the API: callbacks, pairs, +/// durations and string converters. +package javafx.util; diff --git a/maven/javafx-compat/src/test/java/.gitkeep b/maven/javafx-compat/src/test/java/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/maven/javafx-compat/src/test/java/com/codename1/fxcompat/BindingsTest.java b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/BindingsTest.java new file mode 100644 index 00000000000..e6f05dddf78 --- /dev/null +++ b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/BindingsTest.java @@ -0,0 +1,693 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; + +import org.junit.Test; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.DoubleBinding; +import javafx.beans.binding.FloatBinding; +import javafx.beans.binding.IntegerBinding; +import javafx.beans.binding.LongBinding; +import javafx.beans.binding.NumberBinding; +import javafx.beans.binding.ObjectBinding; +import javafx.beans.binding.StringBinding; +import javafx.beans.binding.StringExpression; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.FloatProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.LongProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleFloatProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleLongProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; +import javafx.util.converter.IntegerStringConverter; + +public class BindingsTest { + + private final IntegerProperty i = new SimpleIntegerProperty(6); + private final LongProperty l = new SimpleLongProperty(4L); + private final FloatProperty f = new SimpleFloatProperty(1.5f); + private final DoubleProperty d = new SimpleDoubleProperty(2.5); + + @Test + public void customBindingIsLazyAndCaches() { + final int[] computed = new int[1]; + final IntegerProperty source = new SimpleIntegerProperty(2); + IntegerBinding binding = new IntegerBinding() { + { + bind(source); + } + + @Override + protected int computeValue() { + computed[0]++; + return source.get() * 10; + } + }; + assertFalse(binding.isValid()); + assertEquals(20, binding.get()); + assertEquals(20, binding.get()); + assertEquals("the value is cached while valid", 1, computed[0]); + assertTrue(binding.isValid()); + Probe.Invalidations probe = new Probe.Invalidations(); + binding.addListener(probe); + source.set(3); + source.get(); + source.set(4); + assertEquals("one invalidation until read", 1, probe.count); + assertFalse(binding.isValid()); + assertEquals(1, computed[0]); + assertEquals(40, binding.get()); + assertEquals(2, computed[0]); + binding.invalidate(); + assertFalse(binding.isValid()); + assertEquals(2, probe.count); + } + + @Test + public void changeListenerOnABindingReportsOldAndNew() { + NumberBinding sum = i.add(1); + Probe.Values changes = new Probe.Values(); + sum.addListener(changes); + i.set(7); + i.set(7); + i.set(8); + assertEquals("[7->8, 8->9]", changes.log.toString()); + } + + @Test + public void arithmeticResultHasTheWiderType() { + assertTrue(Bindings.add(i, i) instanceof IntegerBinding); + assertTrue(Bindings.add(i, l) instanceof LongBinding); + assertTrue(Bindings.add(l, f) instanceof FloatBinding); + assertTrue(Bindings.add(f, d) instanceof DoubleBinding); + assertTrue(Bindings.add(i, 1) instanceof IntegerBinding); + assertTrue(Bindings.add(i, 1L) instanceof LongBinding); + assertTrue(Bindings.add(i, 1f) instanceof FloatBinding); + assertTrue(Bindings.multiply(2L, i) instanceof LongBinding); + assertTrue(Bindings.subtract(d, 1) instanceof DoubleBinding); + assertTrue(i.multiply(l) instanceof LongBinding); + assertTrue(l.divide(2) instanceof LongBinding); + DoubleBinding widened = Bindings.add(i, 1.0); + assertEquals(7.0, widened.get(), 0d); + DoubleBinding fluent = d.add(i); + assertEquals(8.5, fluent.get(), 0d); + } + + @Test + public void arithmeticValues() { + assertEquals(10, Bindings.add(i, l).intValue()); + assertEquals(2, Bindings.subtract(i, l).intValue()); + assertEquals(24, Bindings.multiply(i, l).intValue()); + assertEquals(1, Bindings.divide(i, l).intValue()); + assertEquals("integer division truncates", 1L, Bindings.divide(i, l).longValue()); + assertEquals(2.4, Bindings.divide(i, d).doubleValue(), 1e-9); + assertEquals(4.0, Bindings.add(f, d).doubleValue(), 1e-9); + assertEquals(3.5, Bindings.subtract(i, d).doubleValue(), 1e-9); + assertEquals(9.0f, Bindings.multiply(i, f).floatValue(), 0f); + assertEquals(16, Bindings.add(10, i).intValue()); + assertEquals(4, Bindings.subtract(10, i).intValue()); + assertEquals(-4, Bindings.subtract(i, 10).intValue()); + assertEquals(60, Bindings.multiply(i, 10).intValue()); + assertEquals(3, Bindings.divide(i, 2).intValue()); + assertEquals(2, Bindings.divide(12, i).intValue()); + assertEquals(1.25, Bindings.divide(d, 2).doubleValue(), 1e-9); + assertEquals(-6, Bindings.negate(i).intValue()); + assertEquals(-2.5, d.negate().get(), 0d); + assertTrue(i.negate() instanceof IntegerBinding); + } + + @Test + public void arithmeticFollowsItsOperands() { + NumberBinding expression = i.multiply(2).add(l).subtract(d); + assertEquals(13.5, expression.doubleValue(), 1e-9); + i.set(1); + assertEquals(3.5, expression.doubleValue(), 1e-9); + l.set(10); + d.set(0.5); + assertEquals(11.5, expression.doubleValue(), 1e-9); + Probe.Invalidations probe = new Probe.Invalidations(); + expression.addListener(probe); + i.set(2); + assertEquals(1, probe.count); + } + + @Test + public void integerDivisionByZeroThrows() { + IntegerProperty zero = new SimpleIntegerProperty(0); + NumberBinding quotient = Bindings.divide(i, zero); + try { + quotient.intValue(); + fail("expected ArithmeticException"); + } catch (ArithmeticException expected) { + assertTrue(true); + } + assertTrue(Double.isInfinite(Bindings.divide(d, zero).doubleValue())); + } + + @Test + public void minAndMax() { + assertEquals(4, Bindings.min(i, l).intValue()); + assertEquals(6, Bindings.max(i, l).intValue()); + assertTrue(Bindings.max(i, l) instanceof LongBinding); + assertEquals(2.5, Bindings.min(i, d).doubleValue(), 0d); + assertEquals(6, Bindings.min(i, 100).intValue()); + assertEquals(100, Bindings.max(i, 100).intValue()); + assertEquals(1, Bindings.min(1, i).intValue()); + assertEquals(6.5, Bindings.max(6.5, i).doubleValue(), 0d); + NumberBinding max = Bindings.max(i, l); + l.set(50); + assertEquals(50, max.intValue()); + } + + @Test + public void numericEquality() { + assertFalse(Bindings.equal(i, l).get()); + l.set(6); + assertTrue(Bindings.equal(i, l).get()); + assertFalse(Bindings.notEqual(i, l).get()); + assertTrue(Bindings.equal(i, 6).get()); + assertTrue(Bindings.equal(6L, i).get()); + assertTrue(Bindings.notEqual(i, 7).get()); + assertTrue(Bindings.equal(d, 2.6, 0.2).get()); + assertFalse(Bindings.equal(d, 2.6, 0.05).get()); + assertTrue(Bindings.notEqual(d, 2.6, 0.05).get()); + assertTrue(Bindings.equal(2.4, d, 0.2).get()); + assertTrue(Bindings.equal(i, d, 4.0).get()); + assertTrue(i.isEqualTo(6).get()); + assertTrue(i.isNotEqualTo(5).get()); + assertTrue(d.isEqualTo(2.5, 0.0).get()); + assertTrue(i.isEqualTo(l).get()); + BooleanBinding equal = i.isEqualTo(l); + i.set(1); + assertFalse(equal.get()); + } + + @Test + public void numericOrdering() { + assertTrue(Bindings.greaterThan(i, l).get()); + assertFalse(Bindings.lessThan(i, l).get()); + assertTrue(Bindings.greaterThanOrEqual(i, 6).get()); + assertTrue(Bindings.lessThanOrEqual(i, 6).get()); + assertFalse(Bindings.greaterThan(i, 6).get()); + assertFalse(Bindings.lessThan(i, 6).get()); + assertTrue(Bindings.lessThan(1, i).get()); + assertTrue(Bindings.greaterThan(6.5, i).get()); + assertTrue(Bindings.lessThanOrEqual(2.5, d).get()); + assertTrue(Bindings.greaterThanOrEqual(2.5f, d).get()); + assertTrue(i.greaterThan(l).get()); + assertTrue(i.greaterThanOrEqualTo(6).get()); + assertTrue(l.lessThan(i).get()); + assertTrue(l.lessThanOrEqualTo(4L).get()); + assertTrue(d.lessThan(2.6).get()); + BooleanBinding greater = i.greaterThan(l); + l.set(100); + assertFalse(greater.get()); + } + + @Test + public void booleanOperators() { + BooleanProperty a = new SimpleBooleanProperty(true); + BooleanProperty b = new SimpleBooleanProperty(false); + BooleanBinding and = Bindings.and(a, b); + BooleanBinding or = Bindings.or(a, b); + BooleanBinding not = Bindings.not(a); + assertFalse(and.get()); + assertTrue(or.get()); + assertFalse(not.get()); + b.set(true); + assertTrue(and.get()); + a.set(false); + assertFalse(and.get()); + assertTrue(or.get()); + assertTrue(not.get()); + b.set(false); + assertFalse(or.get()); + assertTrue(a.not().get()); + assertFalse(a.and(b).get()); + assertTrue(a.or(b.not()).get()); + assertTrue(Bindings.equal(a, b).get()); + assertFalse(Bindings.notEqual(a, b).get()); + assertTrue(a.isEqualTo(b).get()); + a.set(true); + assertTrue(a.isNotEqualTo(b).get()); + assertEquals("true", a.asString().get()); + assertEquals(Boolean.TRUE, a.asObject().get()); + } + + @Test + public void booleanBindingInvalidatesWhenAnOperandChanges() { + BooleanProperty a = new SimpleBooleanProperty(true); + BooleanProperty b = new SimpleBooleanProperty(true); + BooleanBinding and = a.and(b); + assertTrue(and.get()); + Probe.Values changes = new Probe.Values(); + and.addListener(changes); + b.set(false); + b.set(true); + assertEquals("[true->false, false->true]", changes.log.toString()); + } + + @Test + public void whenThenOtherwise() { + BooleanProperty condition = new SimpleBooleanProperty(true); + NumberBinding number = Bindings.when(condition).then(i).otherwise(l); + assertEquals(6, number.intValue()); + assertTrue(number instanceof LongBinding); + condition.set(false); + assertEquals(4, number.intValue()); + l.set(40); + assertEquals(40, number.intValue()); + condition.set(true); + i.set(60); + assertEquals(60, number.intValue()); + + NumberBinding constants = Bindings.when(condition).then(1).otherwise(2); + assertTrue(constants instanceof IntegerBinding); + assertEquals(1, constants.intValue()); + condition.set(false); + assertEquals(2, constants.intValue()); + DoubleBinding mixed = Bindings.when(condition).then(i).otherwise(0.5); + assertEquals(0.5, mixed.get(), 0d); + + StringProperty yes = new SimpleStringProperty("yes"); + StringBinding text = Bindings.when(condition).then(yes).otherwise("no"); + assertEquals("no", text.get()); + condition.set(true); + assertEquals("yes", text.get()); + yes.set("YES"); + assertEquals("YES", text.get()); + assertEquals("a", Bindings.when(condition).then("a").otherwise("b").get()); + + BooleanBinding flag = Bindings.when(condition).then(false).otherwise(true); + assertFalse(flag.get()); + BooleanProperty other = new SimpleBooleanProperty(true); + assertTrue(Bindings.when(condition).then(other).otherwise(false).get()); + + ObjectProperty> list = new SimpleObjectProperty>(new ArrayList()); + ObjectBinding> object = Bindings.when(condition).then(list).otherwise((List) null); + assertSame(list.get(), object.get()); + condition.set(false); + assertNull(object.get()); + } + + @Test + public void whenInvalidatesOnConditionChange() { + BooleanProperty condition = new SimpleBooleanProperty(true); + NumberBinding binding = Bindings.when(condition).then(1).otherwise(2); + binding.intValue(); + Probe.Invalidations probe = new Probe.Invalidations(); + binding.addListener(probe); + condition.set(false); + assertEquals(1, probe.count); + } + + @Test + public void stringConcatAndConvert() { + StringProperty name = new SimpleStringProperty("World"); + StringExpression greeting = Bindings.concat("Hello ", name, "! ", i); + assertEquals("Hello World! 6", greeting.get()); + name.set("FX"); + i.set(7); + assertEquals("Hello FX! 7", greeting.get()); + assertEquals("FX7", name.concat(i).get()); + assertEquals("FX-", name.concat("-").get()); + assertEquals("7", Bindings.convert(i).get()); + assertEquals("7", i.asString().get()); + StringExpression converted = Bindings.convert(d); + d.set(1.25); + assertEquals("1.25", converted.get()); + name.set(null); + assertEquals("null-", name.concat("-").get()); + assertEquals("", name.getValueSafe()); + } + + @Test + public void stringFormat() { + StringProperty name = new SimpleStringProperty("x"); + StringExpression formatted = Bindings.format("%s=%d", name, i); + assertEquals("x=6", formatted.get()); + i.set(8); + assertEquals("x=8", formatted.get()); + assertEquals("[8]", i.asString("[%d]").get()); + } + + @Test + public void stringComparisons() { + StringProperty a = new SimpleStringProperty("apple"); + StringProperty b = new SimpleStringProperty("Banana"); + assertFalse(Bindings.equal(a, b).get()); + assertTrue(Bindings.notEqual(a, b).get()); + assertTrue(Bindings.equal(a, "apple").get()); + assertTrue(Bindings.equal("apple", a).get()); + assertTrue(Bindings.equalIgnoreCase(a, "APPLE").get()); + assertFalse(Bindings.notEqualIgnoreCase(a, "APPLE").get()); + assertTrue(Bindings.notEqualIgnoreCase(a, b).get()); + assertTrue(Bindings.greaterThan(a, b).get()); + assertTrue(Bindings.lessThan(b, a).get()); + assertTrue(Bindings.greaterThanOrEqual(a, "apple").get()); + assertTrue(Bindings.lessThanOrEqual("apple", a).get()); + assertTrue(a.isEqualTo("apple").get()); + assertTrue(a.isNotEqualTo(b).get()); + assertTrue(a.isEqualToIgnoreCase("Apple").get()); + assertTrue(a.isNotEqualToIgnoreCase(b).get()); + assertTrue(a.greaterThan("aardvark").get()); + assertTrue(a.lessThan("b").get()); + assertTrue(a.greaterThanOrEqualTo(a).get()); + assertTrue(a.lessThanOrEqualTo("apple").get()); + StringProperty empty = new SimpleStringProperty(); + assertTrue("null counts as empty", Bindings.equal(empty, "").get()); + BooleanBinding equal = a.isEqualTo(b); + b.set("apple"); + assertTrue(equal.get()); + } + + @Test + public void stringLengthAndEmptiness() { + StringProperty s = new SimpleStringProperty(); + assertTrue(s.isNull().get()); + assertFalse(s.isNotNull().get()); + assertTrue(s.isEmpty().get()); + assertFalse(s.isNotEmpty().get()); + assertEquals(0, s.length().get()); + IntegerBinding length = s.length(); + BooleanBinding isEmpty = s.isEmpty(); + s.set("abc"); + assertEquals(3, length.get()); + assertFalse(isEmpty.get()); + assertTrue(s.isNotNull().get()); + assertTrue(s.isNotEmpty().get()); + s.set(""); + assertTrue(isEmpty.get()); + assertFalse(s.isNull().get()); + } + + @Test + public void objectComparisonsAndNullChecks() { + ObjectProperty a = new SimpleObjectProperty("x"); + ObjectProperty b = new SimpleObjectProperty("y"); + assertFalse(Bindings.equal(a, b).get()); + assertTrue(Bindings.notEqual(a, b).get()); + assertTrue(Bindings.equal(a, "x").get()); + assertTrue(Bindings.equal("y", b).get()); + assertTrue(Bindings.notEqual(a, "z").get()); + assertTrue(Bindings.notEqual("z", a).get()); + assertFalse(Bindings.isNull(a).get()); + assertTrue(Bindings.isNotNull(a).get()); + BooleanBinding isNull = a.isNull(); + BooleanBinding equal = a.isEqualTo(b); + a.set(null); + assertTrue(isNull.get()); + assertFalse(a.isNotNull().get()); + assertFalse(equal.get()); + b.set(null); + assertTrue("two nulls are equal", equal.get()); + assertTrue(a.isEqualTo((Object) null).get()); + assertTrue(a.isNotEqualTo("x").get()); + a.set("v"); + assertEquals("v", a.asString().get()); + } + + @Test + public void createBindingsFromCallables() { + final IntegerProperty source = new SimpleIntegerProperty(3); + IntegerBinding integer = Bindings.createIntegerBinding(new Callable() { + @Override + public Integer call() { + return Integer.valueOf(source.get() * 2); + } + }, source); + assertEquals(6, integer.get()); + source.set(5); + assertEquals(10, integer.get()); + assertEquals(1, integer.getDependencies().size()); + assertSame(source, integer.getDependencies().get(0)); + DoubleBinding dbl = Bindings.createDoubleBinding(() -> source.get() / 2.0, source); + assertEquals(2.5, dbl.get(), 0d); + LongBinding lng = Bindings.createLongBinding(() -> (long) source.get() << 33, source); + assertEquals(5L << 33, lng.get()); + FloatBinding flt = Bindings.createFloatBinding(() -> source.get() + 0.5f, source); + assertEquals(5.5f, flt.get(), 0f); + BooleanBinding bool = Bindings.createBooleanBinding(() -> source.get() > 4, source); + assertTrue(bool.get()); + StringBinding str = Bindings.createStringBinding(() -> "n" + source.get(), source); + assertEquals("n5", str.get()); + ObjectBinding> obj = Bindings.createObjectBinding(() -> { + List result = new ArrayList(); + result.add(Integer.valueOf(source.get())); + return result; + }, source); + assertEquals("[5]", obj.get().toString()); + source.set(1); + assertEquals(0.5, dbl.get(), 0d); + assertFalse(bool.get()); + assertEquals("n1", str.get()); + assertEquals("[1]", obj.get().toString()); + assertEquals(0, Bindings.createIntegerBinding(() -> 1).getDependencies().size()); + } + + @Test + public void disposeStopsObserving() { + final IntegerProperty source = new SimpleIntegerProperty(3); + IntegerBinding binding = Bindings.createIntegerBinding(() -> source.get(), source); + assertEquals(3, binding.get()); + Probe.Invalidations probe = new Probe.Invalidations(); + binding.addListener(probe); + binding.dispose(); + source.set(4); + assertEquals(0, probe.count); + assertEquals("the cached value stays", 3, binding.get()); + } + + @Test + public void listBindings() { + ObservableList list = FXCollections.observableArrayList("a", "b"); + IntegerBinding size = Bindings.size(list); + BooleanBinding empty = Bindings.isEmpty(list); + BooleanBinding notEmpty = Bindings.isNotEmpty(list); + ObjectBinding second = Bindings.valueAt(list, 1); + IntegerProperty index = new SimpleIntegerProperty(0); + ObjectBinding indexed = Bindings.valueAt(list, index); + assertEquals(2, size.get()); + assertFalse(empty.get()); + assertTrue(notEmpty.get()); + assertEquals("b", second.get()); + assertEquals("a", indexed.get()); + index.set(1); + assertEquals("b", indexed.get()); + index.set(5); + assertNull("outside the list is null", indexed.get()); + list.remove(1); + assertEquals(1, size.get()); + assertNull(second.get()); + list.clear(); + assertTrue(empty.get()); + assertFalse(notEmpty.get()); + assertEquals(0, size.get()); + } + + @Test + public void setAndMapBindings() { + ObservableSet set = FXCollections.observableSet(new HashSet()); + IntegerBinding setSize = Bindings.size(set); + BooleanBinding setEmpty = Bindings.isEmpty(set); + assertTrue(setEmpty.get()); + set.add("a"); + assertEquals(1, setSize.get()); + assertFalse(setEmpty.get()); + assertTrue(Bindings.isNotEmpty(set).get()); + + ObservableMap map = FXCollections.observableHashMap(); + IntegerBinding mapSize = Bindings.size(map); + BooleanBinding mapEmpty = Bindings.isEmpty(map); + ObjectBinding value = Bindings.valueAt(map, "k"); + StringProperty key = new SimpleStringProperty("k"); + ObjectBinding keyed = Bindings.valueAt(map, key); + assertTrue(mapEmpty.get()); + assertNull(value.get()); + map.put("k", Integer.valueOf(1)); + map.put("j", Integer.valueOf(2)); + assertEquals(2, mapSize.get()); + assertFalse(mapEmpty.get()); + assertTrue(Bindings.isNotEmpty(map).get()); + assertEquals(Integer.valueOf(1), value.get()); + assertEquals(Integer.valueOf(1), keyed.get()); + key.set("j"); + assertEquals(Integer.valueOf(2), keyed.get()); + map.remove("k"); + assertNull(value.get()); + } + + @Test + public void bidirectionalBindingThroughAConverter() { + StringProperty text = new SimpleStringProperty("0"); + ObjectProperty number = new SimpleObjectProperty(Integer.valueOf(12)); + Bindings.bindBidirectional(text, number, new IntegerStringConverter()); + assertEquals("12", text.get()); + number.set(Integer.valueOf(5)); + assertEquals("5", text.get()); + text.set("42"); + assertEquals(Integer.valueOf(42), number.get()); + Bindings.unbindBidirectional(text, number); + text.set("1"); + assertEquals(Integer.valueOf(42), number.get()); + + StringProperty fluent = new SimpleStringProperty(); + fluent.bindBidirectional(number, new IntegerStringConverter()); + assertEquals("42", fluent.get()); + fluent.set("7"); + assertEquals(Integer.valueOf(7), number.get()); + fluent.unbindBidirectional(number); + fluent.set("8"); + assertEquals(Integer.valueOf(7), number.get()); + } + + @Test + public void staticBidirectionalBinding() { + StringProperty a = new SimpleStringProperty("a"); + StringProperty b = new SimpleStringProperty("b"); + Bindings.bindBidirectional(a, b); + assertEquals("b", a.get()); + b.set("c"); + assertEquals("c", a.get()); + Bindings.unbindBidirectional(a, b); + b.set("d"); + assertEquals("c", a.get()); + try { + Bindings.bindBidirectional(a, a); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + @Test + public void bindContentOfAList() { + ObservableList source = FXCollections.observableArrayList("a", "b"); + List target = new ArrayList(); + target.add("stale"); + Bindings.bindContent(target, source); + assertEquals("[a, b]", target.toString()); + source.add("c"); + source.remove("a"); + source.set(0, "B"); + assertEquals("[B, c]", target.toString()); + FXCollections.sort(source); + assertEquals(source.toString(), target.toString()); + source.setAll("x", "y", "z"); + assertEquals("[x, y, z]", target.toString()); + source.remove(0, 2); + assertEquals("[z]", target.toString()); + Bindings.unbindContent(target, source); + source.add("late"); + assertEquals("[z]", target.toString()); + } + + @Test + public void bindContentOfASetAndAMap() { + ObservableSet sourceSet = FXCollections.observableSet(new HashSet()); + sourceSet.add("a"); + Set targetSet = new HashSet(); + targetSet.add("stale"); + Bindings.bindContent(targetSet, sourceSet); + assertEquals(sourceSet, targetSet); + sourceSet.add("b"); + sourceSet.remove("a"); + assertEquals(sourceSet, targetSet); + assertEquals(1, targetSet.size()); + Bindings.unbindContent(targetSet, sourceSet); + sourceSet.add("c"); + assertEquals(1, targetSet.size()); + + ObservableMap sourceMap = FXCollections.observableHashMap(); + sourceMap.put("a", Integer.valueOf(1)); + Map targetMap = new HashMap(); + targetMap.put("stale", Integer.valueOf(0)); + Bindings.bindContent(targetMap, sourceMap); + assertEquals(sourceMap, targetMap); + sourceMap.put("a", Integer.valueOf(2)); + sourceMap.put("b", Integer.valueOf(3)); + sourceMap.remove("a"); + assertEquals(sourceMap, targetMap); + assertEquals("{b=3}", targetMap.toString()); + Bindings.unbindContent(targetMap, sourceMap); + sourceMap.clear(); + assertEquals("{b=3}", targetMap.toString()); + } + + @Test + public void bindContentBidirectional() { + ObservableList a = FXCollections.observableArrayList("a"); + ObservableList b = FXCollections.observableArrayList("b", "c"); + Bindings.bindContentBidirectional(a, b); + assertEquals("[b, c]", a.toString()); + a.add("d"); + assertEquals("[b, c, d]", b.toString()); + b.remove("b"); + assertEquals("[c, d]", a.toString()); + a.set(0, "C"); + assertEquals("[C, d]", b.toString()); + Bindings.unbindContentBidirectional(a, b); + a.add("e"); + assertEquals("[C, d]", b.toString()); + b.clear(); + assertEquals("[C, d, e]", a.toString()); + } + + @Test + public void expressionFactoriesReturnTheValueItself() { + assertSame(i, javafx.beans.binding.IntegerExpression.integerExpression(i)); + assertSame(d, javafx.beans.binding.NumberExpressionBase.numberExpression(d)); + StringProperty s = new SimpleStringProperty("s"); + assertSame(s, StringExpression.stringExpression(s)); + assertEquals("6", StringExpression.stringExpression(i).get()); + assertEquals(Integer.valueOf(6), i.asObject().get()); + } +} diff --git a/maven/javafx-compat/src/test/java/com/codename1/fxcompat/EventTest.java b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/EventTest.java new file mode 100644 index 00000000000..2f2c0610064 --- /dev/null +++ b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/EventTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import com.codename1.fxcompat.runtime.EventDispatchChainImpl; + +import javafx.event.ActionEvent; +import javafx.event.Event; +import javafx.event.EventDispatchChain; +import javafx.event.EventDispatcher; +import javafx.event.EventHandler; +import javafx.event.EventTarget; +import javafx.event.EventType; +import javafx.event.WeakEventHandler; + +public class EventTest { + + private final List log = new ArrayList(); + + private EventDispatcher named(final String name, final boolean consume) { + return new EventDispatcher() { + @Override + public Event dispatchEvent(Event event, EventDispatchChain tail) { + log.add(name + ">"); + if (consume) { + event.consume(); + return null; + } + Event result = tail.dispatchEvent(event); + log.add("<" + name); + return result; + } + }; + } + + @Test + public void eventTypeHierarchy() { + assertNull(EventType.ROOT.getSuperType()); + assertEquals("EVENT", EventType.ROOT.getName()); + assertSame(EventType.ROOT, Event.ANY); + assertSame(Event.ANY, ActionEvent.ACTION.getSuperType()); + assertEquals("ACTION", ActionEvent.ACTION.getName()); + assertEquals("ACTION", ActionEvent.ACTION.toString()); + assertSame(ActionEvent.ACTION, ActionEvent.ANY); + EventType custom = new EventType("CUSTOM"); + assertSame(EventType.ROOT, custom.getSuperType()); + EventType child = new EventType(custom, "CHILD"); + assertSame(custom, child.getSuperType()); + assertEquals("CHILD", child.getName()); + EventType unnamed = new EventType(custom); + assertNull(unnamed.getName()); + assertTrue(unnamed.toString().length() > 0); + try { + new EventType((EventType) null, "X"); + fail("expected NullPointerException"); + } catch (NullPointerException expected) { + assertTrue(true); + } + } + + @Test + public void eventBasics() { + Event bare = new Event(Event.ANY); + assertSame(Event.NULL_SOURCE_TARGET, bare.getSource()); + assertSame(Event.NULL_SOURCE_TARGET, bare.getTarget()); + assertSame(Event.ANY, bare.getEventType()); + assertFalse(bare.isConsumed()); + bare.consume(); + assertTrue(bare.isConsumed()); + EventTarget target = tail -> tail; + Object source = new Object(); + Event event = new Event(source, target, ActionEvent.ACTION); + assertSame(source, event.getSource()); + assertSame(target, event.getTarget()); + Event copy = event.copyFor("other", null); + assertNotSame(event, copy); + assertEquals("other", copy.getSource()); + assertSame(Event.NULL_SOURCE_TARGET, copy.getTarget()); + assertSame(ActionEvent.ACTION, copy.getEventType()); + assertSame("the original is untouched", source, event.getSource()); + } + + @Test + public void actionEvent() { + ActionEvent bare = new ActionEvent(); + assertSame(ActionEvent.ACTION, bare.getEventType()); + assertSame(Event.NULL_SOURCE_TARGET, bare.getTarget()); + EventTarget target = tail -> tail; + ActionEvent event = new ActionEvent("button", target); + assertEquals("button", event.getSource()); + ActionEvent copy = event.copyFor("menu", target); + assertNotSame(event, copy); + assertEquals("menu", copy.getSource()); + assertSame(target, copy.getTarget()); + assertSame(ActionEvent.ACTION, copy.getEventType()); + } + + @Test + public void subclassWithoutCopyForIsReaddressed() { + class Custom extends Event { + private static final long serialVersionUID = 1L; + final String payload = "p"; + + Custom() { + super(Event.ANY); + } + } + Custom custom = new Custom(); + Event copy = custom.copyFor("s", null); + assertTrue(copy instanceof Custom); + assertEquals("p", ((Custom) copy).payload); + assertEquals("s", copy.getSource()); + } + + @Test + public void dispatchChainRunsInOrderAndUnwinds() { + EventDispatchChain chain = new EventDispatchChainImpl(); + chain = chain.append(named("b", false)); + chain = chain.append(named("c", false)); + chain = chain.prepend(named("a", false)); + Event event = new Event(Event.ANY); + assertSame(event, chain.dispatchEvent(event)); + assertEquals("[a>, b>, c>, , stop>, seen = new ArrayList(); + final EventHandler handler = new EventHandler() { + @Override + public void handle(ActionEvent event) { + seen.add(event); + } + }; + EventTarget target = new EventTarget() { + @Override + public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) { + return tail.prepend(new EventDispatcher() { + @Override + public Event dispatchEvent(Event event, EventDispatchChain rest) { + if (event instanceof ActionEvent) { + handler.handle((ActionEvent) event); + } + return rest.dispatchEvent(event); + } + }); + } + }; + ActionEvent original = new ActionEvent(); + Event.fireEvent(target, original); + assertEquals(1, seen.size()); + assertSame("the event is addressed to the target", target, seen.get(0).getTarget()); + assertSame(target, seen.get(0).getSource()); + assertSame(Event.NULL_SOURCE_TARGET, original.getTarget()); + try { + Event.fireEvent(null, original); + fail("expected NullPointerException"); + } catch (NullPointerException expected) { + assertTrue(true); + } + try { + Event.fireEvent(target, null); + fail("expected NullPointerException"); + } catch (NullPointerException expected) { + assertTrue(true); + } + } + + @Test + public void weakEventHandlerForwards() { + final int[] calls = new int[1]; + EventHandler strong = event -> calls[0]++; + WeakEventHandler weak = new WeakEventHandler(strong); + weak.handle(new ActionEvent()); + assertEquals(1, calls[0]); + assertFalse(weak.wasGarbageCollected()); + } +} diff --git a/maven/javafx-compat/src/test/java/com/codename1/fxcompat/GeometryTest.java b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/GeometryTest.java new file mode 100644 index 00000000000..35b9a3b73f0 --- /dev/null +++ b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/GeometryTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import javafx.geometry.BoundingBox; +import javafx.geometry.Bounds; +import javafx.geometry.Dimension2D; +import javafx.geometry.HPos; +import javafx.geometry.HorizontalDirection; +import javafx.geometry.Insets; +import javafx.geometry.NodeOrientation; +import javafx.geometry.Orientation; +import javafx.geometry.Point2D; +import javafx.geometry.Pos; +import javafx.geometry.Rectangle2D; +import javafx.geometry.Side; +import javafx.geometry.VPos; +import javafx.geometry.VerticalDirection; + +public class GeometryTest { + + @Test + public void insets() { + Insets insets = new Insets(1, 2, 3, 4); + assertEquals(1.0, insets.getTop(), 0d); + assertEquals(2.0, insets.getRight(), 0d); + assertEquals(3.0, insets.getBottom(), 0d); + assertEquals(4.0, insets.getLeft(), 0d); + Insets uniform = new Insets(5); + assertEquals(5.0, uniform.getTop(), 0d); + assertEquals(5.0, uniform.getLeft(), 0d); + assertEquals(insets, new Insets(1, 2, 3, 4)); + assertEquals(insets.hashCode(), new Insets(1, 2, 3, 4).hashCode()); + assertNotEquals(insets, uniform); + assertNotEquals(insets, "insets"); + assertEquals(Insets.EMPTY, new Insets(0)); + assertEquals("Insets [top=1.0, right=2.0, bottom=3.0, left=4.0]", insets.toString()); + } + + @Test + public void point() { + Point2D p = new Point2D(3, 4); + assertEquals(3.0, p.getX(), 0d); + assertEquals(4.0, p.getY(), 0d); + assertEquals(5.0, p.magnitude(), 1e-12); + assertEquals(5.0, p.distance(0, 0), 1e-12); + assertEquals(5.0, p.distance(Point2D.ZERO), 1e-12); + assertEquals(new Point2D(4, 6), p.add(1, 2)); + assertEquals(new Point2D(4, 6), p.add(new Point2D(1, 2))); + assertEquals(new Point2D(2, 2), p.subtract(1, 2)); + assertEquals(new Point2D(2, 2), p.subtract(new Point2D(1, 2))); + assertEquals(new Point2D(6, 8), p.multiply(2)); + Point2D unit = p.normalize(); + assertEquals(0.6, unit.getX(), 1e-12); + assertEquals(0.8, unit.getY(), 1e-12); + assertEquals(Point2D.ZERO, Point2D.ZERO.normalize()); + assertEquals(new Point2D(2, 3), p.midpoint(1, 2)); + assertEquals(new Point2D(1.5, 2), p.midpoint(Point2D.ZERO)); + assertEquals(11.0, p.dotProduct(1, 2), 0d); + assertEquals(25.0, p.dotProduct(p), 0d); + assertEquals(90.0, new Point2D(1, 0).angle(0, 1), 1e-9); + assertEquals(0.0, new Point2D(1, 0).angle(new Point2D(5, 0)), 1e-9); + assertEquals(180.0, new Point2D(1, 0).angle(-2, 0), 1e-9); + assertEquals(45.0, new Point2D(1, 0).angle(1, 1), 1e-9); + assertEquals(90.0, Point2D.ZERO.angle(new Point2D(1, 0), new Point2D(0, 3)), 1e-9); + assertEquals(p.hashCode(), new Point2D(3, 4).hashCode()); + assertNotEquals(p, new Point2D(4, 3)); + assertNotEquals(p, "point"); + assertEquals("Point2D [x = 3.0, y = 4.0]", p.toString()); + assertEquals(new Point2D(0.0, 0.0), new Point2D(-0.0, 0.0)); + assertEquals(new Point2D(0.0, 0.0).hashCode(), new Point2D(-0.0, 0.0).hashCode()); + assertNotEquals(new Point2D(Double.NaN, 0.0), new Point2D(Double.NaN, 0.0)); + } + + @Test + public void dimension() { + Dimension2D d = new Dimension2D(2, 3); + assertEquals(2.0, d.getWidth(), 0d); + assertEquals(3.0, d.getHeight(), 0d); + assertEquals(d, new Dimension2D(2, 3)); + assertEquals(d.hashCode(), new Dimension2D(2, 3).hashCode()); + assertNotEquals(d, new Dimension2D(3, 2)); + assertEquals("Dimension2D [width = 2.0, height = 3.0]", d.toString()); + } + + @Test + public void rectangle() { + Rectangle2D r = new Rectangle2D(10, 20, 30, 40); + assertEquals(10.0, r.getMinX(), 0d); + assertEquals(20.0, r.getMinY(), 0d); + assertEquals(30.0, r.getWidth(), 0d); + assertEquals(40.0, r.getHeight(), 0d); + assertEquals(40.0, r.getMaxX(), 0d); + assertEquals(60.0, r.getMaxY(), 0d); + assertTrue(r.contains(10, 20)); + assertTrue(r.contains(40, 60)); + assertFalse(r.contains(41, 60)); + assertTrue(r.contains(new Point2D(25, 30))); + assertFalse(r.contains((Point2D) null)); + assertTrue(r.contains(new Rectangle2D(15, 25, 5, 5))); + assertFalse(r.contains(new Rectangle2D(15, 25, 50, 5))); + assertFalse(r.contains((Rectangle2D) null)); + assertTrue(r.contains(10, 20, 30, 40)); + assertFalse(r.contains(10, 20, 31, 40)); + assertTrue(r.intersects(new Rectangle2D(35, 55, 100, 100))); + assertFalse(r.intersects(new Rectangle2D(41, 61, 5, 5))); + assertFalse(r.intersects((Rectangle2D) null)); + assertTrue(r.intersects(0, 0, 11, 21)); + assertFalse(r.intersects(0, 0, 5, 5)); + assertEquals(r, new Rectangle2D(10, 20, 30, 40)); + assertEquals(r.hashCode(), new Rectangle2D(10, 20, 30, 40).hashCode()); + assertNotEquals(r, Rectangle2D.EMPTY); + assertTrue(r.toString().startsWith("Rectangle2D [minX = 10.0")); + try { + new Rectangle2D(0, 0, -1, 1); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + @Test + public void boundingBox() { + Bounds b = new BoundingBox(1, 2, 10, 20); + assertEquals(1.0, b.getMinX(), 0d); + assertEquals(2.0, b.getMinY(), 0d); + assertEquals(0.0, b.getMinZ(), 0d); + assertEquals(10.0, b.getWidth(), 0d); + assertEquals(20.0, b.getHeight(), 0d); + assertEquals(0.0, b.getDepth(), 0d); + assertEquals(11.0, b.getMaxX(), 0d); + assertEquals(22.0, b.getMaxY(), 0d); + assertEquals(0.0, b.getMaxZ(), 0d); + assertEquals(6.0, b.getCenterX(), 0d); + assertEquals(12.0, b.getCenterY(), 0d); + assertEquals(0.0, b.getCenterZ(), 0d); + assertFalse(b.isEmpty()); + assertTrue(new BoundingBox(0, 0, -1, 5).isEmpty()); + assertTrue(b.contains(1, 2)); + assertTrue(b.contains(11, 22)); + assertFalse(b.contains(12, 22)); + assertTrue(b.contains(new Point2D(5, 5))); + assertFalse(b.contains((Point2D) null)); + assertTrue(b.contains(5, 5, 0)); + assertFalse(b.contains(5, 5, 1)); + assertTrue(b.contains(new BoundingBox(2, 3, 1, 1))); + assertFalse(b.contains(new BoundingBox(2, 3, 100, 1))); + assertFalse(b.contains((Bounds) null)); + assertTrue(b.contains(2, 3, 4, 5)); + assertTrue(b.contains(2, 3, 0, 4, 5, 0)); + assertTrue(b.intersects(new BoundingBox(10, 20, 50, 50))); + assertFalse(b.intersects(new BoundingBox(12, 23, 5, 5))); + assertFalse(b.intersects((Bounds) null)); + assertTrue(b.intersects(0, 0, 1, 2)); + assertFalse(b.intersects(0, 0, 0.5, 0.5)); + assertTrue(b.intersects(0, 0, 0, 5, 5, 0)); + assertFalse(new BoundingBox(0, 0, -1, 5).contains(0, 0)); + Bounds box = new BoundingBox(0, 0, 0, 2, 4, 6); + assertEquals(6.0, box.getMaxZ(), 0d); + assertEquals(3.0, box.getCenterZ(), 0d); + assertTrue(box.contains(1, 1, 5)); + assertEquals(b, new BoundingBox(1, 2, 10, 20)); + assertEquals(b.hashCode(), new BoundingBox(1, 2, 10, 20).hashCode()); + assertNotEquals(b, box); + assertTrue(b.toString().startsWith("BoundingBox [minX:1.0")); + } + + @Test + public void positions() { + assertSame(VPos.TOP, Pos.TOP_LEFT.getVpos()); + assertSame(HPos.LEFT, Pos.TOP_LEFT.getHpos()); + assertSame(VPos.CENTER, Pos.CENTER.getVpos()); + assertSame(HPos.CENTER, Pos.CENTER.getHpos()); + assertSame(VPos.BASELINE, Pos.BASELINE_RIGHT.getVpos()); + assertSame(HPos.RIGHT, Pos.BASELINE_RIGHT.getHpos()); + assertSame(VPos.BOTTOM, Pos.BOTTOM_CENTER.getVpos()); + assertEquals(12, Pos.values().length); + assertSame(Pos.CENTER_LEFT, Pos.valueOf("CENTER_LEFT")); + assertEquals(3, HPos.values().length); + assertEquals(4, VPos.values().length); + } + + @Test + public void directions() { + assertTrue(Side.LEFT.isVertical()); + assertTrue(Side.RIGHT.isVertical()); + assertTrue(Side.TOP.isHorizontal()); + assertTrue(Side.BOTTOM.isHorizontal()); + assertFalse(Side.TOP.isVertical()); + assertFalse(Side.LEFT.isHorizontal()); + assertEquals(2, Orientation.values().length); + assertEquals(2, HorizontalDirection.values().length); + assertEquals(2, VerticalDirection.values().length); + assertEquals(3, NodeOrientation.values().length); + assertSame(NodeOrientation.INHERIT, NodeOrientation.valueOf("INHERIT")); + assertSame(Orientation.VERTICAL, Orientation.valueOf("VERTICAL")); + } +} diff --git a/maven/javafx-compat/src/test/java/com/codename1/fxcompat/ObservableListTest.java b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/ObservableListTest.java new file mode 100644 index 00000000000..c38d69ada6b --- /dev/null +++ b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/ObservableListTest.java @@ -0,0 +1,480 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Random; + +import org.junit.Test; + +import javafx.beans.Observable; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ModifiableObservableListBase; +import javafx.collections.ObservableList; +import javafx.collections.WeakListChangeListener; +import javafx.util.Callback; + +public class ObservableListTest { + + private ObservableList list; + private Probe.ListChanges probe; + + private void start(String... items) { + list = FXCollections.observableArrayList(items); + probe = new Probe.ListChanges(); + list.addListener(probe); + } + + @Test + public void addReportsTheAddedRange() { + start(); + assertTrue(list.add("a")); + assertEquals("[add[0,1] +[a]]", probe.take()); + list.add(0, "b"); + assertEquals("[add[0,1] +[b]]", probe.take()); + list.addAll("c", "d"); + assertEquals("[add[2,4] +[c, d]]", probe.take()); + list.addAll(1, Arrays.asList("x", "y")); + assertEquals("[add[1,3] +[x, y]]", probe.take()); + assertEquals("[b, x, y, a, c, d]", list.toString()); + assertEquals(4, probe.events); + assertFalse(list.addAll(new ArrayList())); + assertEquals("an empty addition is not reported", 4, probe.events); + } + + @Test + public void removeReportsTheRemovedElements() { + start("a", "b", "c", "d"); + assertEquals("b", list.remove(1)); + assertEquals("[rem[1] -[b]]", probe.take()); + assertTrue(list.remove("d")); + assertEquals("[rem[2] -[d]]", probe.take()); + assertFalse(list.remove("missing")); + assertEquals("[]", probe.take()); + list.clear(); + assertEquals("[rem[0] -[a, c]]", probe.take()); + list.clear(); + assertEquals(3, probe.events); + } + + @Test + public void removeRange() { + start("a", "b", "c", "d", "e"); + list.remove(1, 3); + assertEquals("[rem[1] -[b, c]]", probe.take()); + assertEquals("[a, d, e]", list.toString()); + list.remove(1, 1); + assertEquals("[]", probe.take()); + } + + @Test + public void setReportsAReplacement() { + start("a", "b", "c"); + assertEquals("b", list.set(1, "B")); + assertEquals("[rep[1,2] -[b] +[B]]", probe.take()); + } + + @Test + public void setAllReplacesEverything() { + start("a", "b"); + list.setAll("x", "y", "z"); + assertEquals("[rep[0,3] -[a, b] +[x, y, z]]", probe.take()); + list.setAll(Arrays.asList("q")); + assertEquals("[rep[0,1] -[x, y, z] +[q]]", probe.take()); + list.setAll(new ArrayList()); + assertEquals("[rem[0] -[q]]", probe.take()); + list.setAll("n"); + assertEquals("[add[0,1] +[n]]", probe.take()); + } + + @Test + public void removeAllReportsEveryRunInOneEvent() { + start("a", "b", "c", "d", "e"); + assertTrue(list.removeAll("a", "c", "d")); + assertEquals(1, probe.events); + assertEquals("[rem[0] -[a], rem[1] -[c, d]]", probe.take()); + assertEquals("[b, e]", list.toString()); + assertFalse(list.removeAll(Arrays.asList("zz"))); + assertEquals(1, probe.events); + } + + @Test + public void retainAllReportsEveryRunInOneEvent() { + start("a", "b", "c", "d", "e"); + assertTrue(list.retainAll("b", "e")); + assertEquals(1, probe.events); + assertEquals("[rem[0] -[a], rem[1] -[c, d]]", probe.take()); + assertEquals("[b, e]", list.toString()); + assertFalse(list.retainAll(Arrays.asList("b", "e"))); + assertEquals(1, probe.events); + } + + @Test + public void removeIfReportsEachRemoval() { + start("a", "bb", "c", "dd"); + assertTrue(list.removeIf(s -> s.length() == 2)); + assertEquals("removeIf goes through the iterator, as in JavaFX", 2, probe.events); + assertEquals("[rem[1] -[bb], rem[2] -[dd]]", probe.take()); + } + + @Test + public void sortReportsAPermutation() { + start("c", "a", "b"); + FXCollections.sort(list); + assertEquals("[a, b, c]", list.toString()); + assertEquals("entry i is where the element at i went", "[perm[0,3]{2,0,1}]", probe.take()); + list.sort(Comparator.reverseOrder()); + assertEquals("[c, b, a]", list.toString()); + assertEquals("[perm[0,3]{2,1,0}]", probe.take()); + FXCollections.sort(list, Comparator.reverseOrder()); + assertEquals("an already sorted list reports nothing", "[]", probe.take()); + } + + @Test + public void reverseShuffleRotateFillReplace() { + start("a", "b", "c"); + FXCollections.reverse(list); + assertEquals("[c, b, a]", list.toString()); + assertEquals(1, probe.events); + FXCollections.rotate(list, 1); + assertEquals("[a, c, b]", list.toString()); + FXCollections.shuffle(list, new Random(42)); + List sorted = new ArrayList(list); + Collections.sort(sorted); + assertEquals("[a, b, c]", sorted.toString()); + assertTrue(FXCollections.replaceAll(list, "a", "A")); + assertTrue(list.contains("A")); + assertFalse(FXCollections.replaceAll(list, "nothing", "N")); + FXCollections.fill(list, "z"); + assertEquals("[z, z, z]", list.toString()); + FXCollections.copy(list, Arrays.asList("1", "2")); + assertEquals("[1, 2, z]", list.toString()); + assertTrue(probe.events >= 5); + } + + @Test + public void subListEditsAreReportedOnTheList() { + start("a", "b", "c", "d"); + List middle = list.subList(1, 3); + assertEquals("[b, c]", middle.toString()); + middle.clear(); + assertEquals("[a, d]", list.toString()); + assertEquals("[rem[1] -[b, c]]", probe.take()); + List tail = list.subList(1, 2); + tail.add("e"); + assertEquals("[a, d, e]", list.toString()); + assertEquals("[add[2,3] +[e]]", probe.take()); + tail.set(0, "D"); + assertEquals("[rep[1,2] -[d] +[D]]", probe.take()); + tail.remove("e"); + assertEquals("[rem[2] -[e]]", probe.take()); + } + + @Test + public void iteratorEditsAreReported() { + start("a", "b", "c"); + Iterator it = list.iterator(); + it.next(); + it.remove(); + assertEquals("[rem[0] -[a]]", probe.take()); + ListIterator li = list.listIterator(); + li.next(); + li.set("B"); + assertEquals("[rep[0,1] -[b] +[B]]", probe.take()); + li.add("x"); + assertEquals("[add[1,2] +[x]]", probe.take()); + assertEquals("[B, x, c]", list.toString()); + } + + @Test + public void changeAccessorsFollowTheProtocol() { + list = FXCollections.observableArrayList("a", "b", "c"); + final int[] checked = new int[1]; + list.addListener(new ListChangeListener() { + @Override + public void onChanged(Change c) { + assertSame(list, c.getList()); + try { + c.getFrom(); + fail("reading before next() must fail"); + } catch (IllegalStateException expected) { + checked[0]++; + } + assertTrue(c.next()); + assertTrue(c.wasReplaced()); + assertTrue(c.wasAdded()); + assertTrue(c.wasRemoved()); + assertFalse(c.wasPermutated()); + assertFalse(c.wasUpdated()); + assertEquals(1, c.getFrom()); + assertEquals(2, c.getTo()); + assertEquals(1, c.getAddedSize()); + assertEquals(1, c.getRemovedSize()); + assertEquals("[B]", c.getAddedSubList().toString()); + assertEquals("[b]", c.getRemoved().toString()); + assertFalse(c.next()); + c.reset(); + assertTrue(c.next()); + assertEquals(1, c.getFrom()); + checked[0]++; + } + }); + list.set(1, "B"); + assertEquals(2, checked[0]); + } + + @Test + public void everyListenerSeesTheWholeChange() { + start("a"); + Probe.ListChanges second = new Probe.ListChanges(); + list.addListener(second); + list.addAll("b", "c"); + assertEquals("[add[1,3] +[b, c]]", probe.take()); + assertEquals("[add[1,3] +[b, c]]", second.take()); + list.removeListener(second); + list.add("d"); + assertEquals(1, second.events); + } + + @Test + public void invalidationListenerIsToldOfEveryChange() { + start("a"); + Probe.Invalidations invalidations = new Probe.Invalidations(); + list.addListener(invalidations); + list.add("b"); + list.add("c"); + assertEquals("a list has no value to read, so it is never lazy", 2, invalidations.count); + assertSame(list, invalidations.last); + list.removeListener(invalidations); + list.add("d"); + assertEquals(2, invalidations.count); + } + + @Test + public void extractorReportsUpdates() { + ObservableList items = FXCollections + .observableArrayList(new Callback() { + @Override + public Observable[] call(IntegerProperty item) { + return new Observable[] {item}; + } + }); + IntegerProperty first = new SimpleIntegerProperty(1); + IntegerProperty second = new SimpleIntegerProperty(2); + items.add(first); + items.add(second); + Probe.ListChanges updates = new Probe.ListChanges(); + items.addListener(updates); + second.set(20); + assertEquals("[upd[1,2]]", updates.take()); + first.set(10); + assertEquals("[upd[0,1]]", updates.take()); + items.remove(first); + updates.take(); + first.get(); + first.set(11); + assertEquals("a removed element is no longer watched", "[]", updates.take()); + second.get(); + second.set(21); + assertEquals("[upd[0,1]]", updates.take()); + } + + @Test + public void observableListWrapsTheBackingList() { + List backing = new ArrayList(); + backing.add("a"); + ObservableList wrapped = FXCollections.observableList(backing); + wrapped.add("b"); + assertEquals("[a, b]", backing.toString()); + assertEquals(2, wrapped.size()); + ObservableList copy = FXCollections.observableArrayList(backing); + copy.add("c"); + assertEquals("a copy does not write through", 2, backing.size()); + } + + @Test + public void unmodifiableViewForwardsChangesAndRejectsEdits() { + start("a"); + ObservableList view = FXCollections.unmodifiableObservableList(list); + Probe.ListChanges viewProbe = new Probe.ListChanges(); + view.addListener(viewProbe); + list.add("b"); + assertEquals("[add[1,2] +[b]]", viewProbe.take()); + assertEquals("[a, b]", view.toString()); + try { + view.add("c"); + fail("expected UnsupportedOperationException"); + } catch (UnsupportedOperationException expected) { + assertEquals(2, view.size()); + } + try { + view.remove(0); + fail("expected UnsupportedOperationException"); + } catch (UnsupportedOperationException expected) { + assertEquals(2, list.size()); + } + } + + @Test + public void emptyAndSingletonLists() { + ObservableList empty = FXCollections.emptyObservableList(); + assertEquals(0, empty.size()); + try { + empty.add("a"); + fail("expected UnsupportedOperationException"); + } catch (UnsupportedOperationException expected) { + assertTrue(empty.isEmpty()); + } + ObservableList one = FXCollections.singletonObservableList("x"); + assertEquals(1, one.size()); + assertEquals("x", one.get(0)); + try { + one.set(0, "y"); + fail("expected UnsupportedOperationException"); + } catch (UnsupportedOperationException expected) { + assertEquals("x", one.get(0)); + } + ObservableList joined = FXCollections.concat(one, FXCollections.observableArrayList("y", "z")); + assertEquals("[x, y, z]", joined.toString()); + } + + @Test + public void weakListChangeListenerForwards() { + start(); + Probe.ListChanges target = new Probe.ListChanges(); + WeakListChangeListener weak = new WeakListChangeListener(target); + list.addListener(weak); + list.add("a"); + assertEquals("[add[0,1] +[a]]", target.take()); + assertFalse(weak.wasGarbageCollected()); + } + + @Test + public void modifiableBaseDerivesEverythingFromFourMethods() { + final List store = new ArrayList(); + ObservableList custom = new ModifiableObservableListBase() { + @Override + public String get(int index) { + return store.get(index); + } + + @Override + public int size() { + return store.size(); + } + + @Override + protected void doAdd(int index, String element) { + store.add(index, element); + } + + @Override + protected String doSet(int index, String element) { + return store.set(index, element); + } + + @Override + protected String doRemove(int index) { + return store.remove(index); + } + }; + Probe.ListChanges changes = new Probe.ListChanges(); + custom.addListener(changes); + custom.addAll("a", "b", "c"); + assertEquals("[add[0,3] +[a, b, c]]", changes.take()); + custom.set(0, "A"); + assertEquals("[rep[0,1] -[a] +[A]]", changes.take()); + custom.remove("b"); + assertEquals("[rem[1] -[b]]", changes.take()); + custom.setAll("x"); + assertEquals("[rep[0,1] -[A, c] +[x]]", changes.take()); + custom.retainAll("none"); + assertEquals("[rem[0] -[x]]", changes.take()); + assertEquals("[]", store.toString()); + } + + @Test + public void listPropertyTracksItsList() { + javafx.beans.property.ListProperty property = new javafx.beans.property.SimpleListProperty( + this, "items", FXCollections.observableArrayList("a")); + Probe.ListChanges content = new Probe.ListChanges(); + Probe.Invalidations invalidations = new Probe.Invalidations(); + property.addListener(content); + property.addListener(invalidations); + assertEquals(1, property.getSize()); + assertEquals("items", property.getName()); + javafx.beans.property.ReadOnlyIntegerProperty size = property.sizeProperty(); + javafx.beans.property.ReadOnlyBooleanProperty empty = property.emptyProperty(); + Probe.Values sizes = new Probe.Values(); + size.addListener(sizes); + property.add("b"); + assertEquals("[add[1,2] +[b]]", content.take()); + assertEquals(1, invalidations.count); + assertEquals("[1->2]", sizes.log.toString()); + assertFalse(empty.get()); + ObservableList other = FXCollections.observableArrayList("x", "y", "z"); + property.set(other); + assertEquals("[rep[0,3] -[a, b] +[x, y, z]]", content.take()); + assertSame(other, property.get()); + assertEquals(3, size.get()); + other.remove("y"); + assertEquals("[rem[1] -[y]]", content.take()); + assertEquals("[x, z]", property.toString().substring(property.toString().indexOf("value: ") + 7, + property.toString().length() - 1)); + assertTrue(property.isEqualTo(FXCollections.observableArrayList("x", "z")).get()); + assertEquals("z", property.valueAt(1).get()); + property.set(null); + assertEquals("[rem[0] -[x, z]]", content.take()); + assertTrue(empty.get()); + assertEquals(0, property.size()); + assertTrue(property.isNull().get()); + javafx.beans.property.ListProperty bound = new javafx.beans.property.SimpleListProperty(); + bound.bind(property); + property.set(other); + assertSame(other, bound.get()); + try { + bound.set(null); + fail("expected a RuntimeException"); + } catch (RuntimeException expected) { + assertTrue(expected.getMessage().endsWith("A bound value cannot be set.")); + } + bound.unbind(); + bound.set(null); + assertEquals(0, bound.size()); + } +} diff --git a/maven/javafx-compat/src/test/java/com/codename1/fxcompat/ObservableMapSetTest.java b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/ObservableMapSetTest.java new file mode 100644 index 00000000000..b3db9620803 --- /dev/null +++ b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/ObservableMapSetTest.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import javafx.collections.FXCollections; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; + +public class ObservableMapSetTest { + + private final List log = new ArrayList(); + + private final MapChangeListener mapProbe = new MapChangeListener() { + @Override + public void onChanged(Change c) { + log.add(c.getKey() + (c.wasRemoved() ? " -" + c.getValueRemoved() : "") + + (c.wasAdded() ? " +" + c.getValueAdded() : "")); + } + }; + + private final SetChangeListener setProbe = new SetChangeListener() { + @Override + public void onChanged(Change c) { + log.add(c.wasAdded() ? "+" + c.getElementAdded() : "-" + c.getElementRemoved()); + } + }; + + private String take() { + String result = log.toString(); + log.clear(); + return result; + } + + @Test + public void mapReportsPutReplaceAndRemove() { + ObservableMap map = FXCollections.observableMap(new LinkedHashMap()); + map.addListener(mapProbe); + assertNull(map.put("a", Integer.valueOf(1))); + assertEquals("[a +1]", take()); + assertEquals(Integer.valueOf(1), map.put("a", Integer.valueOf(2))); + assertEquals("[a -1 +2]", take()); + map.put("a", Integer.valueOf(2)); + assertEquals("putting an equal value reports nothing", "[]", take()); + assertEquals(Integer.valueOf(2), map.remove("a")); + assertEquals("[a -2]", take()); + assertNull(map.remove("a")); + assertEquals("[]", take()); + Map more = new LinkedHashMap(); + more.put("x", Integer.valueOf(1)); + more.put("y", Integer.valueOf(2)); + map.putAll(more); + assertEquals("[x +1, y +2]", take()); + map.clear(); + assertEquals("[x -1, y -2]", take()); + assertTrue(map.isEmpty()); + } + + @Test + public void mapViewsReportRemovals() { + ObservableMap map = FXCollections.observableMap(new LinkedHashMap()); + map.put("a", Integer.valueOf(1)); + map.put("b", Integer.valueOf(2)); + map.put("c", Integer.valueOf(3)); + map.addListener(mapProbe); + assertTrue(map.keySet().remove("a")); + assertEquals("[a -1]", take()); + assertTrue(map.values().remove(Integer.valueOf(2))); + assertEquals("[b -2]", take()); + Iterator> entries = map.entrySet().iterator(); + Map.Entry entry = entries.next(); + entry.setValue(Integer.valueOf(30)); + assertEquals("[c -3 +30]", take()); + entries.remove(); + assertEquals("[c -30]", take()); + assertEquals(0, map.size()); + } + + @Test + public void mapInvalidationAndListenerRemoval() { + ObservableMap map = FXCollections.observableHashMap(); + Probe.Invalidations invalidations = new Probe.Invalidations(); + map.addListener(invalidations); + map.addListener(mapProbe); + map.put("a", Integer.valueOf(1)); + assertEquals(1, invalidations.count); + assertSame(map, invalidations.last); + map.removeListener(mapProbe); + map.removeListener(invalidations); + map.put("b", Integer.valueOf(2)); + assertEquals(1, invalidations.count); + assertEquals("[a +1]", take()); + assertEquals(Integer.valueOf(2), map.get("b")); + assertTrue(map.containsKey("a")); + assertTrue(map.containsValue(Integer.valueOf(1))); + } + + @Test + public void unmodifiableAndEmptyMaps() { + ObservableMap map = FXCollections.observableMap(new HashMap()); + ObservableMap view = FXCollections.unmodifiableObservableMap(map); + view.addListener(mapProbe); + map.put("a", Integer.valueOf(1)); + assertEquals("[a +1]", take()); + assertEquals(Integer.valueOf(1), view.get("a")); + try { + view.put("b", Integer.valueOf(2)); + fail("expected UnsupportedOperationException"); + } catch (UnsupportedOperationException expected) { + assertEquals(1, view.size()); + } + ObservableMap empty = FXCollections.emptyObservableMap(); + assertTrue(empty.isEmpty()); + try { + empty.put("a", Integer.valueOf(1)); + fail("expected UnsupportedOperationException"); + } catch (UnsupportedOperationException expected) { + assertTrue(empty.isEmpty()); + } + } + + @Test + public void setReportsAddAndRemove() { + ObservableSet set = FXCollections.observableSet(new LinkedHashSet()); + set.addListener(setProbe); + assertTrue(set.add("a")); + assertEquals("[+a]", take()); + assertFalse(set.add("a")); + assertEquals("[]", take()); + List more = new ArrayList(); + more.add("a"); + more.add("b"); + more.add("c"); + assertTrue(set.addAll(more)); + assertEquals("[+b, +c]", take()); + assertTrue(set.remove("b")); + assertEquals("[-b]", take()); + assertFalse(set.remove("b")); + List keep = new ArrayList(); + keep.add("c"); + assertTrue(set.retainAll(keep)); + assertEquals("[-a]", take()); + set.add("d"); + take(); + Iterator it = set.iterator(); + it.next(); + it.remove(); + assertEquals("[-c]", take()); + set.clear(); + assertEquals("[-d]", take()); + assertTrue(set.isEmpty()); + } + + @Test + public void setFactoriesAndViews() { + ObservableSet set = FXCollections.observableSet("a", "b"); + assertEquals(2, set.size()); + assertTrue(set.contains("a")); + ObservableSet view = FXCollections.unmodifiableObservableSet(set); + view.addListener(setProbe); + Probe.Invalidations invalidations = new Probe.Invalidations(); + set.addListener(invalidations); + set.add("c"); + assertEquals("[+c]", take()); + assertEquals(1, invalidations.count); + try { + view.add("d"); + fail("expected UnsupportedOperationException"); + } catch (UnsupportedOperationException expected) { + assertEquals(3, view.size()); + } + ObservableSet empty = FXCollections.emptyObservableSet(); + assertTrue(empty.isEmpty()); + set.removeListener(invalidations); + set.add("e"); + assertEquals(1, invalidations.count); + } +} diff --git a/maven/javafx-compat/src/test/java/com/codename1/fxcompat/Probe.java b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/Probe.java new file mode 100644 index 00000000000..d56ff698a9d --- /dev/null +++ b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/Probe.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.ListChangeListener; + +/// Listeners that record what they are told, shared by the tests. +public final class Probe { + + private Probe() { + } + + /// Counts invalidations. + public static final class Invalidations implements InvalidationListener { + public int count; + public Observable last; + + @Override + public void invalidated(Observable observable) { + count++; + last = observable; + } + } + + /// Records every change as `old->new`. + public static final class Values implements ChangeListener { + public final List log = new ArrayList(); + + @Override + public void changed(ObservableValue observable, T oldValue, T newValue) { + log.add(oldValue + "->" + newValue); + } + } + + /// Records every list change, one entry per step. + public static final class ListChanges implements ListChangeListener { + public final List log = new ArrayList(); + public int events; + + @Override + public void onChanged(Change change) { + events++; + while (change.next()) { + StringBuilder step = new StringBuilder(); + if (change.wasPermutated()) { + step.append("perm[").append(change.getFrom()).append(',').append(change.getTo()).append("]{"); + for (int i = change.getFrom(); i < change.getTo(); i++) { + if (i > change.getFrom()) { + step.append(','); + } + step.append(change.getPermutation(i)); + } + step.append('}'); + } else if (change.wasUpdated()) { + step.append("upd[").append(change.getFrom()).append(',').append(change.getTo()).append(']'); + } else if (change.wasReplaced()) { + step.append("rep[").append(change.getFrom()).append(',').append(change.getTo()).append("] -") + .append(change.getRemoved()).append(" +").append(change.getAddedSubList()); + } else if (change.wasAdded()) { + step.append("add[").append(change.getFrom()).append(',').append(change.getTo()).append("] +") + .append(change.getAddedSubList()); + } else if (change.wasRemoved()) { + step.append("rem[").append(change.getFrom()).append("] -").append(change.getRemoved()); + } else { + step.append("none"); + } + log.add(step.toString()); + } + } + + public String take() { + String result = log.toString(); + log.clear(); + return result; + } + } + + /// Runs the collector until a weak reference clears; returns whether it + /// did. + public static boolean collected(WeakReference ref) { + for (int i = 0; i < 200 && ref.get() != null; i++) { + System.gc(); + byte[][] pressure = new byte[32][]; + for (int j = 0; j < pressure.length; j++) { + pressure[j] = new byte[64 * 1024]; + } + try { + Thread.sleep(5); + } catch (InterruptedException interrupted) { + return false; + } + } + return ref.get() == null; + } +} diff --git a/maven/javafx-compat/src/test/java/com/codename1/fxcompat/PropertyTest.java b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/PropertyTest.java new file mode 100644 index 00000000000..d06a67d15f8 --- /dev/null +++ b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/PropertyTest.java @@ -0,0 +1,614 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.FloatProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.LongProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.ReadOnlyIntegerWrapper; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleFloatProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleLongProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.beans.value.WeakChangeListener; + +public class PropertyTest { + + @Test + public void defaultsAndInitialValues() { + assertEquals(0, new SimpleIntegerProperty().get()); + assertEquals(0L, new SimpleLongProperty().get()); + assertEquals(0f, new SimpleFloatProperty().get(), 0f); + assertEquals(0d, new SimpleDoubleProperty().get(), 0d); + assertFalse(new SimpleBooleanProperty().get()); + assertNull(new SimpleStringProperty().get()); + assertNull(new SimpleObjectProperty().get()); + assertEquals(7, new SimpleIntegerProperty(7).get()); + assertEquals(7L, new SimpleLongProperty(7L).get()); + assertEquals(1.5f, new SimpleFloatProperty(1.5f).get(), 0f); + assertEquals(2.5, new SimpleDoubleProperty(2.5).get(), 0d); + assertTrue(new SimpleBooleanProperty(true).get()); + assertEquals("x", new SimpleStringProperty("x").get()); + assertEquals("o", new SimpleObjectProperty("o").get()); + } + + @Test + public void beanAndName() { + Object bean = new Object(); + IntegerProperty p = new SimpleIntegerProperty(bean, "width", 3); + assertSame(bean, p.getBean()); + assertEquals("width", p.getName()); + assertEquals(3, p.get()); + IntegerProperty anonymous = new SimpleIntegerProperty(); + assertNull(anonymous.getBean()); + assertEquals("", anonymous.getName()); + assertEquals("", new SimpleStringProperty(bean, null).getName()); + assertEquals("s", new SimpleStringProperty(bean, "s", "v").getName()); + assertEquals("d", new SimpleDoubleProperty(bean, "d").getName()); + } + + @Test + public void typedValueAccessors() { + IntegerProperty i = new SimpleIntegerProperty(5); + assertEquals(Integer.valueOf(5), i.getValue()); + assertEquals(5L, i.longValue()); + assertEquals(5f, i.floatValue(), 0f); + assertEquals(5d, i.doubleValue(), 0d); + assertEquals(5, i.intValue()); + i.setValue(9); + assertEquals(9, i.get()); + DoubleProperty d = new SimpleDoubleProperty(2.75); + assertEquals(2, d.intValue()); + assertEquals(2L, d.longValue()); + assertEquals(Double.valueOf(2.75), d.getValue()); + d.setValue(4); + assertEquals(4.0, d.get(), 0d); + LongProperty l = new SimpleLongProperty(1L << 40); + assertEquals(Long.valueOf(1L << 40), l.getValue()); + FloatProperty f = new SimpleFloatProperty(0.5f); + assertEquals(Float.valueOf(0.5f), f.getValue()); + BooleanProperty b = new SimpleBooleanProperty(); + b.setValue(Boolean.TRUE); + assertTrue(b.get()); + assertEquals(Boolean.TRUE, b.getValue()); + StringProperty s = new SimpleStringProperty(); + s.setValue("hello"); + assertEquals("hello", s.getValue()); + } + + @Test + public void invalidationIsLazy() { + IntegerProperty p = new SimpleIntegerProperty(1); + Probe.Invalidations probe = new Probe.Invalidations(); + p.addListener(probe); + p.set(2); + assertEquals(1, probe.count); + assertSame(p, probe.last); + p.set(3); + p.set(4); + assertEquals("no second invalidation until the value is read", 1, probe.count); + assertEquals(4, p.get()); + p.set(5); + assertEquals(2, probe.count); + p.get(); + p.set(5); + assertEquals("setting the same value does not invalidate", 2, probe.count); + } + + @Test + public void changeListenerMakesThePropertyEager() { + IntegerProperty p = new SimpleIntegerProperty(1); + Probe.Invalidations invalidations = new Probe.Invalidations(); + Probe.Values changes = new Probe.Values(); + p.addListener(invalidations); + p.addListener(changes); + p.set(2); + p.set(3); + p.set(3); + assertEquals(2, invalidations.count); + assertEquals("[1->2, 2->3]", changes.log.toString()); + } + + @Test + public void listenersRunInRegistrationOrder() { + final List order = new ArrayList(); + StringProperty p = new SimpleStringProperty("a"); + p.addListener(new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + order.add("inv1"); + } + }); + p.addListener(new ChangeListener() { + @Override + public void changed(ObservableValue o, String oldValue, String newValue) { + order.add("chg1"); + } + }); + p.addListener(new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + order.add("inv2"); + } + }); + p.addListener(new ChangeListener() { + @Override + public void changed(ObservableValue o, String oldValue, String newValue) { + order.add("chg2"); + } + }); + p.set("b"); + assertEquals("[inv1, inv2, chg1, chg2]", order.toString()); + } + + @Test + public void removedListenersAreNotCalled() { + DoubleProperty p = new SimpleDoubleProperty(); + Probe.Invalidations invalidations = new Probe.Invalidations(); + Probe.Values changes = new Probe.Values(); + p.addListener(invalidations); + p.addListener(changes); + p.set(1); + p.removeListener(invalidations); + p.removeListener(changes); + p.set(2); + assertEquals(1, invalidations.count); + assertEquals(1, changes.log.size()); + p.removeListener(invalidations); + p.removeListener(changes); + } + + @Test + public void listenerMayRemoveItselfWhileNotified() { + final IntegerProperty p = new SimpleIntegerProperty(); + final int[] calls = new int[2]; + p.addListener(new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + calls[0]++; + p.removeListener(this); + } + }); + p.addListener(new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + calls[1]++; + } + }); + p.set(1); + p.get(); + p.set(2); + assertEquals(1, calls[0]); + assertEquals(2, calls[1]); + } + + @Test + public void changeListenerSeesNestedChangeInOrder() { + final IntegerProperty p = new SimpleIntegerProperty(0); + final Probe.Values changes = new Probe.Values(); + p.addListener(new ChangeListener() { + @Override + public void changed(ObservableValue o, Number oldValue, Number newValue) { + if (newValue.intValue() == 1) { + p.set(2); + } + } + }); + p.addListener(changes); + p.set(1); + assertEquals(2, p.get()); + assertEquals("the nested change is delivered first, as in JavaFX", "[1->2, 0->1]", + changes.log.toString()); + } + + @Test + public void bindFollowsTheSource() { + IntegerProperty source = new SimpleIntegerProperty(1); + IntegerProperty target = new SimpleIntegerProperty(9); + Probe.Invalidations probe = new Probe.Invalidations(); + target.addListener(probe); + assertFalse(target.isBound()); + target.bind(source); + assertTrue(target.isBound()); + assertEquals(1, probe.count); + assertEquals(1, target.get()); + source.set(2); + assertEquals(2, probe.count); + source.set(3); + assertEquals("lazy while unread", 2, probe.count); + assertEquals(3, target.get()); + target.unbind(); + assertFalse(target.isBound()); + source.set(4); + assertEquals("keeps the last bound value", 3, target.get()); + target.set(10); + assertEquals(10, target.get()); + assertEquals(4, source.get()); + } + + @Test + public void bindingTwiceToTheSameSourceIsANoOp() { + StringProperty source = new SimpleStringProperty("a"); + StringProperty target = new SimpleStringProperty(); + target.bind(source); + target.get(); + Probe.Invalidations probe = new Probe.Invalidations(); + target.addListener(probe); + target.bind(source); + assertEquals(0, probe.count); + StringProperty other = new SimpleStringProperty("b"); + target.bind(other); + assertEquals("b", target.get()); + source.set("c"); + assertEquals("the first source was released", "b", target.get()); + } + + @Test + public void aBoundValueCannotBeSet() { + Object bean = "owner"; + IntegerProperty source = new SimpleIntegerProperty(1); + IntegerProperty i = new SimpleIntegerProperty(bean, "count"); + i.bind(source); + try { + i.set(5); + fail("expected a RuntimeException"); + } catch (RuntimeException expected) { + assertTrue(expected.getMessage(), expected.getMessage().endsWith("A bound value cannot be set.")); + assertTrue(expected.getMessage(), expected.getMessage().indexOf("count") >= 0); + } + try { + i.setValue(6); + fail("expected a RuntimeException"); + } catch (RuntimeException expected) { + assertEquals(1, i.get()); + } + StringProperty s = new SimpleStringProperty(); + s.bind(new SimpleStringProperty("x")); + try { + s.set("y"); + fail("expected a RuntimeException"); + } catch (RuntimeException expected) { + assertEquals("A bound value cannot be set.", expected.getMessage()); + } + BooleanProperty b = new SimpleBooleanProperty(); + b.bind(new SimpleBooleanProperty(true)); + DoubleProperty d = new SimpleDoubleProperty(); + d.bind(source); + LongProperty l = new SimpleLongProperty(); + l.bind(source); + FloatProperty f = new SimpleFloatProperty(); + f.bind(source); + ObjectProperty o = new SimpleObjectProperty(); + o.bind(new SimpleObjectProperty("v")); + int thrown = 0; + try { + b.set(false); + } catch (RuntimeException expected) { + thrown++; + } + try { + d.set(2); + } catch (RuntimeException expected) { + thrown++; + } + try { + l.set(2); + } catch (RuntimeException expected) { + thrown++; + } + try { + f.set(2); + } catch (RuntimeException expected) { + thrown++; + } + try { + o.set("w"); + } catch (RuntimeException expected) { + thrown++; + } + assertEquals(5, thrown); + assertTrue(b.get()); + assertEquals(1.0, d.get(), 0d); + assertEquals(1L, l.get()); + assertEquals(1f, f.get(), 0f); + assertEquals("v", o.get()); + } + + @Test(expected = NullPointerException.class) + public void bindToNullThrows() { + new SimpleIntegerProperty().bind(null); + } + + @Test + public void numericPropertyBindsToAnyNumber() { + DoubleProperty d = new SimpleDoubleProperty(); + IntegerProperty i = new SimpleIntegerProperty(4); + d.bind(i); + assertEquals(4.0, d.get(), 0d); + i.set(6); + assertEquals(6.0, d.get(), 0d); + IntegerProperty truncated = new SimpleIntegerProperty(); + DoubleProperty source = new SimpleDoubleProperty(2.9); + truncated.bind(source); + assertEquals(2, truncated.get()); + } + + @Test + public void aBoundPropertyIsNotKeptAliveByItsSource() { + IntegerProperty source = new SimpleIntegerProperty(1); + IntegerProperty target = new SimpleIntegerProperty(); + target.bind(source); + assertEquals(1, target.get()); + WeakReference ref = new WeakReference(target); + target = null; + assertTrue("the bound property stayed reachable from its source", Probe.collected(ref)); + source.set(2); + source.set(3); + assertEquals(3, source.get()); + } + + @Test + public void bidirectionalBindingCopiesBothWays() { + IntegerProperty a = new SimpleIntegerProperty(1); + IntegerProperty b = new SimpleIntegerProperty(2); + a.bindBidirectional(b); + assertEquals("the first takes the value of the second", 2, a.get()); + a.set(5); + assertEquals(5, b.get()); + b.set(7); + assertEquals(7, a.get()); + a.unbindBidirectional(b); + a.set(8); + assertEquals(7, b.get()); + b.set(9); + assertEquals(8, a.get()); + } + + @Test + public void bidirectionalUnbindWorksFromEitherSide() { + StringProperty a = new SimpleStringProperty("a"); + StringProperty b = new SimpleStringProperty("b"); + a.bindBidirectional(b); + b.unbindBidirectional(a); + a.set("x"); + assertEquals("b", b.get()); + ObjectProperty c = new SimpleObjectProperty("c"); + ObjectProperty d = new SimpleObjectProperty("d"); + c.bindBidirectional(d); + c.bindBidirectional(d); + d.set("e"); + assertEquals("e", c.get()); + c.unbindBidirectional(d); + d.set("f"); + assertEquals("binding twice needs unbinding twice", "f", c.get()); + c.unbindBidirectional(d); + d.set("g"); + assertEquals("f", c.get()); + } + + @Test + public void bidirectionalBindingDoesNotKeepEitherSideAlive() { + DoubleProperty a = new SimpleDoubleProperty(1); + DoubleProperty b = new SimpleDoubleProperty(2); + a.bindBidirectional(b); + WeakReference ref = new WeakReference(b); + b = null; + assertTrue("the partner stayed reachable through the binding", Probe.collected(ref)); + a.set(3); + a.set(4); + assertEquals(4.0, a.get(), 0d); + } + + @Test + public void bidirectionalBindingToABoundPropertyRestoresTheValue() { + IntegerProperty a = new SimpleIntegerProperty(1); + IntegerProperty b = new SimpleIntegerProperty(1); + IntegerProperty master = new SimpleIntegerProperty(1); + a.bindBidirectional(b); + b.bind(master); + try { + a.set(5); + fail("expected a RuntimeException"); + } catch (RuntimeException expected) { + assertEquals("the change that could not be copied is undone", 1, a.get()); + } + } + + @Test + public void chainOfBidirectionalBindings() { + BooleanProperty a = new SimpleBooleanProperty(); + BooleanProperty b = new SimpleBooleanProperty(); + BooleanProperty c = new SimpleBooleanProperty(); + a.bindBidirectional(b); + b.bindBidirectional(c); + a.set(true); + assertTrue(b.get()); + assertTrue(c.get()); + c.set(false); + assertFalse(a.get()); + } + + @Test + public void weakInvalidationListenerForwardsAndReleases() { + IntegerProperty p = new SimpleIntegerProperty(); + Probe.Invalidations probe = new Probe.Invalidations(); + WeakInvalidationListener weak = new WeakInvalidationListener(probe); + p.addListener(weak); + p.set(1); + assertEquals(1, probe.count); + assertFalse(weak.wasGarbageCollected()); + WeakReference ref = new WeakReference(probe); + probe = null; + assertTrue(Probe.collected(ref)); + assertTrue(weak.wasGarbageCollected()); + p.get(); + p.set(2); + p.get(); + p.set(3); + assertEquals(3, p.get()); + } + + @Test + public void weakChangeListenerForwards() { + StringProperty p = new SimpleStringProperty("a"); + Probe.Values probe = new Probe.Values(); + WeakChangeListener weak = new WeakChangeListener(probe); + p.addListener(weak); + p.set("b"); + assertEquals("[a->b]", probe.log.toString()); + assertFalse(weak.wasGarbageCollected()); + } + + @Test(expected = NullPointerException.class) + public void weakListenerRejectsNull() { + new WeakInvalidationListener(null); + } + + @Test + public void readOnlyWrapperExposesAReadOnlyView() { + ReadOnlyIntegerWrapper wrapper = new ReadOnlyIntegerWrapper(this, "size", 1); + ReadOnlyIntegerProperty view = wrapper.getReadOnlyProperty(); + assertSame(view, wrapper.getReadOnlyProperty()); + assertFalse(view instanceof IntegerProperty); + assertEquals(1, view.get()); + assertSame(this, view.getBean()); + assertEquals("size", view.getName()); + Probe.Invalidations invalidations = new Probe.Invalidations(); + Probe.Values changes = new Probe.Values(); + view.addListener(invalidations); + wrapper.set(2); + assertEquals(1, invalidations.count); + wrapper.set(3); + assertEquals(1, invalidations.count); + assertEquals(3, view.get()); + view.addListener(changes); + wrapper.set(4); + assertEquals("[3->4]", changes.log.toString()); + } + + @Test + public void everyWrapperTypeHasAView() { + ReadOnlyStringWrapper s = new ReadOnlyStringWrapper("a"); + ReadOnlyStringProperty view = s.getReadOnlyProperty(); + s.set("b"); + assertEquals("b", view.get()); + ReadOnlyBooleanWrapper b = new ReadOnlyBooleanWrapper(true); + assertTrue(b.getReadOnlyProperty().get()); + ReadOnlyDoubleWrapper d = new ReadOnlyDoubleWrapper(1.5); + assertEquals(1.5, d.getReadOnlyProperty().get(), 0d); + ReadOnlyObjectWrapper o = new ReadOnlyObjectWrapper("x"); + assertEquals("x", o.getReadOnlyProperty().get()); + IntegerProperty source = new SimpleIntegerProperty(8); + ReadOnlyIntegerWrapper bound = new ReadOnlyIntegerWrapper(); + bound.bind(source); + assertEquals(8, bound.getReadOnlyProperty().get()); + Probe.Invalidations probe = new Probe.Invalidations(); + bound.getReadOnlyProperty().addListener(probe); + source.set(9); + assertEquals(1, probe.count); + assertEquals(9, bound.getReadOnlyProperty().get()); + } + + @Test + public void asObjectIsALiveTwoWayView() { + IntegerProperty i = new SimpleIntegerProperty(1); + ObjectProperty boxed = i.asObject(); + assertEquals(Integer.valueOf(1), boxed.get()); + i.set(2); + assertEquals(Integer.valueOf(2), boxed.get()); + boxed.set(Integer.valueOf(3)); + assertEquals(3, i.get()); + ObjectProperty source = new SimpleObjectProperty(Double.valueOf(1.5)); + DoubleProperty d = DoubleProperty.doubleProperty(source); + assertEquals(1.5, d.get(), 0d); + d.set(2.5); + assertEquals(Double.valueOf(2.5), source.get()); + source.set(Double.valueOf(4)); + assertEquals(4.0, d.get(), 0d); + } + + @Test + public void toStringDescribesTheProperty() { + assertEquals("IntegerProperty [value: 3]", new SimpleIntegerProperty(3).toString()); + String named = new SimpleStringProperty("bean", "title", "t").toString(); + assertTrue(named, named.startsWith("StringProperty [")); + assertTrue(named, named.indexOf("bean: bean") >= 0); + assertTrue(named, named.indexOf("name: title") >= 0); + assertTrue(named, named.endsWith("value: t]")); + IntegerProperty bound = new SimpleIntegerProperty(); + IntegerProperty source = new SimpleIntegerProperty(4); + bound.bind(source); + assertTrue(bound.toString(), bound.toString().indexOf("bound") >= 0); + } + + @Test + public void invalidatedHookRunsBeforeListeners() { + final List order = new ArrayList(); + IntegerProperty p = new SimpleIntegerProperty() { + @Override + protected void invalidated() { + order.add("hook"); + } + }; + p.addListener(new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + order.add("listener"); + } + }); + p.set(1); + p.set(2); + assertEquals("[hook, listener]", order.toString()); + } +} diff --git a/maven/javafx-compat/src/test/java/com/codename1/fxcompat/TransformationListTest.java b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/TransformationListTest.java new file mode 100644 index 00000000000..14e2e1743e5 --- /dev/null +++ b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/TransformationListTest.java @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; + +import org.junit.Test; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; + +public class TransformationListTest { + + private static List ints(int... values) { + List result = new ArrayList(); + for (int value : values) { + result.add(Integer.valueOf(value)); + } + return result; + } + + @Test + public void filteredListShowsMatchingElements() { + ObservableList source = FXCollections.observableArrayList(ints(1, 2, 3, 4, 5, 6)); + FilteredList even = new FilteredList(source, n -> n.intValue() % 2 == 0); + assertEquals("[2, 4, 6]", even.toString()); + assertEquals(3, even.size()); + assertEquals(1, even.getSourceIndex(0)); + assertEquals(5, even.getSourceIndex(2)); + assertEquals(1, even.getViewIndex(3)); + assertTrue(even.getViewIndex(0) < 0); + assertSame(source, even.getSource()); + assertTrue(even.isInTransformationChain(source)); + assertEquals(3, even.getSourceIndexFor(source, 1)); + } + + @Test + public void filteredListPropagatesSourceChanges() { + ObservableList source = FXCollections.observableArrayList(ints(1, 2, 3, 4)); + FilteredList even = source.filtered(n -> n.intValue() % 2 == 0); + Probe.ListChanges probe = new Probe.ListChanges(); + even.addListener(probe); + source.add(Integer.valueOf(6)); + assertEquals("[add[2,3] +[6]]", probe.take()); + source.add(Integer.valueOf(7)); + assertEquals("an element that does not match changes nothing", "[]", probe.take()); + source.add(0, Integer.valueOf(0)); + assertEquals("[add[0,1] +[0]]", probe.take()); + assertEquals("[0, 2, 4, 6]", even.toString()); + source.remove(Integer.valueOf(2)); + assertEquals("[rem[1] -[2]]", probe.take()); + source.remove(Integer.valueOf(1)); + assertEquals("[]", probe.take()); + source.set(0, Integer.valueOf(10)); + assertEquals("[rep[0,1] -[0] +[10]]", probe.take()); + source.set(0, Integer.valueOf(11)); + assertEquals("[rem[0] -[10]]", probe.take()); + source.set(0, Integer.valueOf(12)); + assertEquals("[add[0,1] +[12]]", probe.take()); + assertEquals("[12, 4, 6]", even.toString()); + source.clear(); + assertEquals("[rem[0] -[12, 4, 6]]", probe.take()); + assertEquals(0, even.size()); + } + + @Test + public void filteredListFollowsASourceSort() { + ObservableList source = FXCollections.observableArrayList(ints(6, 1, 4, 3, 2)); + FilteredList even = new FilteredList(source, n -> n.intValue() % 2 == 0); + assertEquals("[6, 4, 2]", even.toString()); + FXCollections.sort(source); + assertEquals("[2, 4, 6]", even.toString()); + assertEquals(1, even.getSourceIndex(0)); + source.removeAll(Integer.valueOf(2), Integer.valueOf(6), Integer.valueOf(1)); + assertEquals("[4]", even.toString()); + } + + @Test + public void filteredListPredicateChange() { + ObservableList source = FXCollections.observableArrayList(ints(1, 2, 3, 4)); + FilteredList view = new FilteredList(source); + assertNull(view.getPredicate()); + assertEquals("no predicate shows everything", "[1, 2, 3, 4]", view.toString()); + Probe.ListChanges probe = new Probe.ListChanges(); + view.addListener(probe); + view.setPredicate(n -> n.intValue() > 2); + assertEquals("[3, 4]", view.toString()); + assertEquals(1, probe.events); + view.predicateProperty().set(n -> n.intValue() < 2); + assertEquals("[1]", view.toString()); + assertEquals(2, probe.events); + view.setPredicate(null); + assertEquals(4, view.size()); + } + + @Test + public void filteredListIsReadOnly() { + FilteredList view = new FilteredList(FXCollections.observableArrayList(ints(1))); + try { + view.add(Integer.valueOf(2)); + fail("expected UnsupportedOperationException"); + } catch (UnsupportedOperationException expected) { + assertEquals(1, view.size()); + } + } + + @Test + public void sortedListOrdersItsSource() { + ObservableList source = FXCollections.observableArrayList(ints(3, 1, 2)); + SortedList sorted = new SortedList(source, Comparator.naturalOrder()); + assertEquals("[1, 2, 3]", sorted.toString()); + assertEquals("[3, 1, 2]", source.toString()); + assertEquals(1, sorted.getSourceIndex(0)); + assertEquals(0, sorted.getSourceIndex(2)); + assertEquals(2, sorted.getViewIndex(0)); + assertEquals(0, sorted.getViewIndex(1)); + } + + @Test + public void sortedListPropagatesSourceChanges() { + ObservableList source = FXCollections.observableArrayList(ints(30, 10)); + SortedList sorted = source.sorted(); + Probe.ListChanges probe = new Probe.ListChanges(); + sorted.addListener(probe); + source.add(Integer.valueOf(20)); + assertEquals("[add[1,2] +[20]]", probe.take()); + assertEquals("[10, 20, 30]", sorted.toString()); + source.add(Integer.valueOf(5)); + assertEquals("[add[0,1] +[5]]", probe.take()); + source.remove(Integer.valueOf(20)); + assertEquals("[rem[2] -[20]]", probe.take()); + assertEquals("[5, 10, 30]", sorted.toString()); + source.set(0, Integer.valueOf(7)); + assertEquals("[5, 7, 10]", sorted.toString()); + assertEquals(1, probe.events - 3); + probe.take(); + source.addAll(ints(1, 100)); + assertEquals("[1, 5, 7, 10, 100]", sorted.toString()); + assertEquals("[add[0,1] +[1], add[4,5] +[100]]", probe.take()); + source.clear(); + assertEquals("[rem[0] -[1, 5, 7, 10, 100]]", probe.take()); + } + + @Test + public void sortedListIsStableForEqualElements() { + ObservableList source = FXCollections.observableArrayList("bb", "a", "cc", "d"); + SortedList byLength = source.sorted(new Comparator() { + @Override + public int compare(String first, String second) { + return first.length() - second.length(); + } + }); + assertEquals("[a, d, bb, cc]", byLength.toString()); + source.add("e"); + assertEquals("[a, d, e, bb, cc]", byLength.toString()); + source.add(0, "ff"); + assertEquals("[a, d, e, ff, bb, cc]", byLength.toString()); + } + + @Test + public void sortedListComparatorChangeIsAPermutation() { + ObservableList source = FXCollections.observableArrayList(ints(2, 3, 1)); + SortedList sorted = new SortedList(source, Comparator.naturalOrder()); + Probe.ListChanges probe = new Probe.ListChanges(); + sorted.addListener(probe); + sorted.setComparator(Comparator.reverseOrder()); + assertEquals("[3, 2, 1]", sorted.toString()); + assertEquals("[perm[0,3]{2,1,0}]", probe.take()); + sorted.setComparator(null); + assertEquals("no comparator shows the source order", "[2, 3, 1]", sorted.toString()); + assertNull(sorted.getComparator()); + sorted.comparatorProperty().set(Comparator.naturalOrder()); + assertEquals("[1, 2, 3]", sorted.toString()); + } + + @Test + public void sortedOverFilteredFollowsRandomEdits() { + ObservableList source = FXCollections.observableArrayList(); + FilteredList filtered = source.filtered(n -> n.intValue() % 3 != 0); + SortedList sorted = filtered.sorted(); + final List mirror = new ArrayList(); + sorted.addListener(new javafx.collections.ListChangeListener() { + @Override + public void onChanged(Change c) { + while (c.next()) { + if (c.wasPermutated()) { + List before = new ArrayList(mirror.subList(c.getFrom(), c.getTo())); + for (int i = c.getFrom(); i < c.getTo(); i++) { + mirror.set(c.getPermutation(i), before.get(i - c.getFrom())); + } + } else { + for (int i = 0; i < c.getRemovedSize(); i++) { + mirror.remove(c.getFrom()); + } + mirror.addAll(c.getFrom(), c.getAddedSubList()); + } + } + } + }); + Random random = new Random(7); + for (int step = 0; step < 400; step++) { + int action = random.nextInt(6); + if (action < 2 || source.isEmpty()) { + source.add(random.nextInt(source.size() + 1), Integer.valueOf(random.nextInt(50))); + } else if (action == 2) { + source.remove(random.nextInt(source.size())); + } else if (action == 3) { + source.set(random.nextInt(source.size()), Integer.valueOf(random.nextInt(50))); + } else if (action == 4) { + source.removeAll(Integer.valueOf(random.nextInt(50)), Integer.valueOf(random.nextInt(50))); + } else { + FXCollections.shuffle(source, random); + } + List expected = new ArrayList(); + for (Integer value : source) { + if (value.intValue() % 3 != 0) { + expected.add(value); + } + } + assertEquals("filtered view after step " + step, expected, new ArrayList(filtered)); + Collections.sort(expected); + assertEquals("sorted view after step " + step, expected, new ArrayList(sorted)); + assertEquals("replaying the reported changes after step " + step, expected, mirror); + } + assertFalse(sorted.isInTransformationChain(FXCollections.observableArrayList())); + assertTrue(sorted.isInTransformationChain(source)); + } + + @Test + public void replayingListChangesReproducesTheList() { + final ObservableList source = FXCollections.observableArrayList(); + final List mirror = new ArrayList(); + source.addListener(new javafx.collections.ListChangeListener() { + @Override + public void onChanged(Change c) { + while (c.next()) { + if (c.wasPermutated()) { + List before = new ArrayList(mirror.subList(c.getFrom(), c.getTo())); + for (int i = c.getFrom(); i < c.getTo(); i++) { + mirror.set(c.getPermutation(i), before.get(i - c.getFrom())); + } + } else if (!c.wasUpdated()) { + for (int i = 0; i < c.getRemovedSize(); i++) { + assertEquals(c.getRemoved().get(i), mirror.remove(c.getFrom())); + } + mirror.addAll(c.getFrom(), c.getAddedSubList()); + } + } + } + }); + Random random = new Random(11); + for (int step = 0; step < 500; step++) { + int action = random.nextInt(9); + if (action < 2 || source.size() < 2) { + source.addAll(random.nextInt(source.size() + 1), ints(random.nextInt(20), random.nextInt(20))); + } else if (action == 2) { + source.remove(random.nextInt(source.size())); + } else if (action == 3) { + source.set(random.nextInt(source.size()), Integer.valueOf(random.nextInt(20))); + } else if (action == 4) { + source.removeAll(ints(random.nextInt(20), random.nextInt(20), random.nextInt(20))); + } else if (action == 5) { + source.retainAll(ints(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, random.nextInt(20))); + } else if (action == 6) { + FXCollections.sort(source); + } else if (action == 7) { + int from = random.nextInt(source.size()); + source.subList(from, from + random.nextInt(source.size() - from)).clear(); + } else { + final int limit = random.nextInt(20); + source.removeIf(n -> n.intValue() > limit && n.intValue() % 2 == 0); + } + assertEquals("after step " + step, new ArrayList(source), mirror); + } + } +} diff --git a/maven/javafx-compat/src/test/java/com/codename1/fxcompat/UtilTest.java b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/UtilTest.java new file mode 100644 index 00000000000..9d0d572615c --- /dev/null +++ b/maven/javafx-compat/src/test/java/com/codename1/fxcompat/UtilTest.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.fxcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import javafx.util.Callback; +import javafx.util.Duration; +import javafx.util.Pair; +import javafx.util.StringConverter; +import javafx.util.converter.BooleanStringConverter; +import javafx.util.converter.DefaultStringConverter; +import javafx.util.converter.DoubleStringConverter; +import javafx.util.converter.IntegerStringConverter; +import javafx.util.converter.NumberStringConverter; + +public class UtilTest { + + @Test + public void durationFactoriesAndConversions() { + assertEquals(1500.0, Duration.seconds(1.5).toMillis(), 0d); + assertEquals(120000.0, Duration.minutes(2).toMillis(), 0d); + assertEquals(7200000.0, Duration.hours(2).toMillis(), 0d); + Duration d = Duration.millis(90000); + assertEquals(90.0, d.toSeconds(), 0d); + assertEquals(1.5, d.toMinutes(), 0d); + assertEquals(0.025, d.toHours(), 1e-12); + assertSame(Duration.ZERO, Duration.millis(0)); + assertSame(Duration.ONE, Duration.millis(1)); + assertSame(Duration.INDEFINITE, Duration.millis(Double.POSITIVE_INFINITY)); + assertSame(Duration.UNKNOWN, Duration.seconds(Double.NaN)); + assertEquals(250.0, new Duration(250).toMillis(), 0d); + } + + @Test + public void durationArithmetic() { + Duration a = Duration.seconds(3); + Duration b = Duration.millis(500); + assertEquals(3500.0, a.add(b).toMillis(), 0d); + assertEquals(2500.0, a.subtract(b).toMillis(), 0d); + assertEquals(6000.0, a.multiply(2).toMillis(), 0d); + assertEquals(1500.0, a.divide(2).toMillis(), 0d); + assertEquals(-3000.0, a.negate().toMillis(), 0d); + assertSame(Duration.ZERO, a.subtract(a)); + assertTrue(a.add(Duration.INDEFINITE).isIndefinite()); + assertTrue(a.add(Duration.UNKNOWN).isUnknown()); + assertTrue(Duration.INDEFINITE.subtract(Duration.INDEFINITE).isUnknown()); + assertFalse(a.isIndefinite()); + assertFalse(a.isUnknown()); + assertTrue(a.divide(0).isIndefinite()); + } + + @Test + public void durationComparison() { + Duration a = Duration.seconds(1); + Duration b = Duration.seconds(2); + assertTrue(a.lessThan(b)); + assertTrue(a.lessThanOrEqualTo(b)); + assertTrue(a.lessThanOrEqualTo(Duration.millis(1000))); + assertTrue(b.greaterThan(a)); + assertTrue(b.greaterThanOrEqualTo(a)); + assertFalse(a.greaterThan(b)); + assertFalse(a.greaterThan(Duration.UNKNOWN)); + assertFalse(a.lessThan(Duration.UNKNOWN)); + assertTrue(a.lessThan(Duration.INDEFINITE)); + assertTrue(a.compareTo(b) < 0); + assertTrue(b.compareTo(a) > 0); + assertEquals(0, a.compareTo(Duration.millis(1000))); + assertEquals(a, Duration.millis(1000)); + assertEquals(a.hashCode(), Duration.millis(1000).hashCode()); + assertNotEquals(a, b); + assertNotEquals(a, "1000.0 ms"); + assertEquals(Duration.UNKNOWN, new Duration(Double.NaN)); + } + + @Test + public void durationParsing() { + assertEquals(500.0, Duration.valueOf("500ms").toMillis(), 0d); + assertEquals(1500.0, Duration.valueOf("1.5s").toMillis(), 0d); + assertEquals(120000.0, Duration.valueOf("2m").toMillis(), 0d); + assertEquals(3600000.0, Duration.valueOf("1h").toMillis(), 0d); + assertEquals(-250.0, Duration.valueOf("-250ms").toMillis(), 0d); + assertEquals(500.0, Duration.valueOf(".5s").toMillis(), 0d); + assertEquals("units fold as ASCII", 2000.0, Duration.valueOf("2S").toMillis(), 0d); + assertEquals(3.0, Duration.valueOf("3MS").toMillis(), 0d); + assertSame(Duration.ZERO, Duration.valueOf("0s")); + String[] malformed = {"", "10", "ms", "10 ms", "10x", "1.2.3s", "s10", "10sec", "--1s"}; + for (String text : malformed) { + try { + Duration.valueOf(text); + fail("expected IllegalArgumentException for \"" + text + "\""); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().length() > 0); + } + } + try { + Duration.valueOf(null); + fail("expected NullPointerException"); + } catch (NullPointerException expected) { + assertTrue(true); + } + } + + @Test + public void durationText() { + assertEquals("1500.0 ms", Duration.seconds(1.5).toString()); + assertEquals("0.0 ms", Duration.ZERO.toString()); + assertEquals("-2.5 ms", Duration.millis(-2.5).toString()); + assertEquals("INDEFINITE", Duration.INDEFINITE.toString()); + assertEquals("UNKNOWN", Duration.UNKNOWN.toString()); + } + + @Test + public void pair() { + Pair pair = new Pair("k", Integer.valueOf(1)); + assertEquals("k", pair.getKey()); + assertEquals(Integer.valueOf(1), pair.getValue()); + assertEquals("k=1", pair.toString()); + assertEquals(pair, new Pair("k", Integer.valueOf(1))); + assertEquals(pair.hashCode(), new Pair("k", Integer.valueOf(1)).hashCode()); + assertNotEquals(pair, new Pair("k", Integer.valueOf(2))); + assertNotEquals(pair, new Pair("j", Integer.valueOf(1))); + assertNotEquals(pair, "k=1"); + Pair nulls = new Pair(null, null); + assertEquals(nulls, new Pair(null, null)); + assertEquals("null=null", nulls.toString()); + assertNotEquals(nulls, pair); + assertNotEquals(pair, nulls); + } + + @Test + public void callback() { + Callback length = new Callback() { + @Override + public Integer call(String param) { + return Integer.valueOf(param.length()); + } + }; + assertEquals(Integer.valueOf(3), length.call("abc")); + Callback twice = n -> Integer.valueOf(n.intValue() * 2); + assertEquals(Integer.valueOf(8), twice.call(Integer.valueOf(4))); + } + + @Test + public void simpleConverters() { + StringConverter text = new DefaultStringConverter(); + assertEquals("a", text.toString("a")); + assertEquals("", text.toString(null)); + assertEquals("a", text.fromString("a")); + assertNull(text.fromString(null)); + + StringConverter bool = new BooleanStringConverter(); + assertEquals("true", bool.toString(Boolean.TRUE)); + assertEquals("false", bool.toString(Boolean.FALSE)); + assertEquals("", bool.toString(null)); + assertEquals(Boolean.TRUE, bool.fromString(" TRUE ")); + assertEquals(Boolean.TRUE, bool.fromString("true")); + assertEquals(Boolean.FALSE, bool.fromString("yes")); + assertNull(bool.fromString(" ")); + assertNull(bool.fromString(null)); + + StringConverter integer = new IntegerStringConverter(); + assertEquals("42", integer.toString(Integer.valueOf(42))); + assertEquals("-7", integer.toString(Integer.valueOf(-7))); + assertEquals("", integer.toString(null)); + assertEquals(Integer.valueOf(42), integer.fromString(" 42 ")); + assertEquals(Integer.valueOf(-7), integer.fromString("-7")); + assertNull(integer.fromString("")); + assertNull(integer.fromString(null)); + try { + integer.fromString("4.2"); + fail("expected NumberFormatException"); + } catch (NumberFormatException expected) { + assertTrue(true); + } + + StringConverter dbl = new DoubleStringConverter(); + assertEquals("1.5", dbl.toString(Double.valueOf(1.5))); + assertEquals("", dbl.toString(null)); + assertEquals(Double.valueOf(1.5), dbl.fromString(" 1.5 ")); + assertEquals(Double.valueOf(3), dbl.fromString("3")); + assertNull(dbl.fromString(" ")); + try { + dbl.fromString("abc"); + fail("expected NumberFormatException"); + } catch (NumberFormatException expected) { + assertTrue(true); + } + } + + @Test + public void numberConverterFormats() { + NumberStringConverter c = new NumberStringConverter(); + assertEquals("", c.toString(null)); + assertEquals("0", c.toString(Integer.valueOf(0))); + assertEquals("42", c.toString(Integer.valueOf(42))); + assertEquals("1,234", c.toString(Integer.valueOf(1234))); + assertEquals("-1,234,567", c.toString(Long.valueOf(-1234567L))); + assertEquals("999", c.toString(Long.valueOf(999))); + assertEquals("1,000", c.toString(Long.valueOf(1000))); + assertEquals("-9,223,372,036,854,775,808", c.toString(Long.valueOf(Long.MIN_VALUE))); + assertEquals("1.5", c.toString(Double.valueOf(1.5))); + assertEquals("1,234.568", c.toString(Double.valueOf(1234.5678))); + assertEquals("0.125", c.toString(Double.valueOf(0.125))); + assertEquals("half rounds to the even digit", "0.062", c.toString(Double.valueOf(0.0625))); + assertEquals("0.188", c.toString(Double.valueOf(0.1875))); + assertEquals("2", c.toString(Double.valueOf(1.9999))); + assertEquals("3", c.toString(Double.valueOf(3.0))); + assertEquals("-0.5", c.toString(Double.valueOf(-0.5))); + assertEquals("0", c.toString(Double.valueOf(-0.0001))); + assertEquals("2.5", c.toString(Float.valueOf(2.5f))); + assertEquals("NaN", c.toString(Double.valueOf(Double.NaN))); + assertEquals("10,000,000,000,000,000", c.toString(Double.valueOf(1e16))); + } + + @Test + public void numberConverterParses() { + NumberStringConverter c = new NumberStringConverter(); + assertNull(c.fromString(null)); + assertNull(c.fromString(" ")); + assertEquals(Long.valueOf(42), c.fromString("42")); + assertEquals(Long.valueOf(1234567), c.fromString(" 1,234,567 ")); + assertEquals(Long.valueOf(-5), c.fromString("-5")); + assertEquals(Double.valueOf(1.5), c.fromString("1.5")); + assertEquals(Double.valueOf(1234.25), c.fromString("1,234.25")); + assertEquals("a whole number is a Long", Long.valueOf(3), c.fromString("3.0")); + try { + c.fromString("abc"); + fail("expected RuntimeException"); + } catch (RuntimeException expected) { + assertTrue(expected.getMessage().indexOf("abc") >= 0); + } + assertEquals(Long.valueOf(1234), c.fromString(c.toString(Integer.valueOf(1234)))); + } +} From 21f5d2b65ac11fd31cba066edf0377ed71f9d19a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:40:23 +0300 Subject: [PATCH 005/257] JDK shims: Optional, ResourceBundle, number and message formats, URL, concurrent collections Adds to compat-jdk the JDK classes desktop Java expects and the device library lacks, and a JDK_SHIMS row for each: - java.util: EventObject, Optional, StringJoiner, ResourceBundle, PropertyResourceBundle, ListResourceBundle, MissingResourceException. ResourceBundle.cn1Register lets the build name the packaged resource of a bundle; it is consulted before the path lookup. - java.util.concurrent: Callable, Future, ExecutionException, CancellationException, TimeoutException, TimeUnit, ConcurrentMap, ConcurrentHashMap (and its KeySetView), CopyOnWriteArrayList. None synchronizes. - java.text: NumberFormat, DecimalFormat, DecimalFormatSymbols, ChoiceFormat, MessageFormat, FieldPosition, ParsePosition. - java.net: URL, MalformedURLException, with the cn1res scheme for packaged resources. The tests are differential: each shim is run beside the JDK class it is named after, over pattern x value tables and seeded random input, and the few intended differences are asserted and explained where they are made. Co-Authored-By: Claude Opus 5.5 --- .../java/com/codename1/maven/Relocation.java | 35 + maven/compat-jdk/pom.xml | 5 + maven/compat-jdk/spotbugs-exclude.xml | 39 + .../com/codename1/compat/jdk/Callable.java | 30 + .../compat/jdk/CancellationException.java | 39 + .../codename1/compat/jdk/ChoiceFormat.java | 338 ++++ .../compat/jdk/ConcurrentHashMap.java | 676 ++++++++ .../codename1/compat/jdk/ConcurrentMap.java | 44 + .../compat/jdk/CopyOnWriteArrayList.java | 530 +++++++ .../codename1/compat/jdk/DecimalFormat.java | 1394 +++++++++++++++++ .../compat/jdk/DecimalFormatSymbols.java | 317 ++++ .../com/codename1/compat/jdk/EventObject.java | 49 + .../compat/jdk/ExecutionException.java | 47 + .../codename1/compat/jdk/FieldPosition.java | 78 + .../java/com/codename1/compat/jdk/Future.java | 38 + .../compat/jdk/ListResourceBundle.java | 79 + .../compat/jdk/MalformedURLException.java | 40 + .../codename1/compat/jdk/MessageFormat.java | 455 ++++++ .../compat/jdk/MissingResourceException.java | 47 + .../codename1/compat/jdk/NumberFormat.java | 278 ++++ .../com/codename1/compat/jdk/Optional.java | 149 ++ .../codename1/compat/jdk/ParsePosition.java | 70 + .../compat/jdk/PropertyResourceBundle.java | 248 +++ .../codename1/compat/jdk/ResourceBundle.java | 369 +++++ .../codename1/compat/jdk/StringJoiner.java | 105 ++ .../com/codename1/compat/jdk/TimeUnit.java | 110 ++ .../compat/jdk/TimeoutException.java | 38 + .../java/com/codename1/compat/jdk/URL.java | 759 +++++++++ .../codename1/compat/jdk/package-info.java | 43 + .../jdk/DecimalFormatDifferentialTest.java | 513 ++++++ .../jdk/MessageFormatDifferentialTest.java | 360 +++++ .../jdk/ResourceBundleDifferentialTest.java | 496 ++++++ .../compat/jdk/URLDifferentialTest.java | 353 +++++ .../compat/jdk/UtilShimsDifferentialTest.java | 631 ++++++++ .../compat/jdk/bundles/Messages.properties | 4 + .../compat/jdk/bundles/Messages_fr.properties | 3 + .../jdk/bundles/Messages_fr_CA.properties | 1 + .../compat/jdk/bundles/NoBase_de.properties | 1 + .../compat/jdk/registered/odd-name-es.txt | 1 + .../compat/jdk/registered/odd-name.txt | 1 + .../codename1/compat/jdk/registered/plain.txt | 2 + 41 files changed, 8815 insertions(+) create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Callable.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/CancellationException.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ChoiceFormat.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ConcurrentHashMap.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ConcurrentMap.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/CopyOnWriteArrayList.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/DecimalFormat.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/DecimalFormatSymbols.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/EventObject.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ExecutionException.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FieldPosition.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Future.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ListResourceBundle.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MalformedURLException.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MessageFormat.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MissingResourceException.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/NumberFormat.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Optional.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ParsePosition.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/PropertyResourceBundle.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ResourceBundle.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/StringJoiner.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/TimeUnit.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/TimeoutException.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/URL.java create mode 100644 maven/compat-jdk/src/main/java/com/codename1/compat/jdk/package-info.java create mode 100644 maven/compat-jdk/src/test/java/com/codename1/compat/jdk/DecimalFormatDifferentialTest.java create mode 100644 maven/compat-jdk/src/test/java/com/codename1/compat/jdk/MessageFormatDifferentialTest.java create mode 100644 maven/compat-jdk/src/test/java/com/codename1/compat/jdk/ResourceBundleDifferentialTest.java create mode 100644 maven/compat-jdk/src/test/java/com/codename1/compat/jdk/URLDifferentialTest.java create mode 100644 maven/compat-jdk/src/test/java/com/codename1/compat/jdk/UtilShimsDifferentialTest.java create mode 100644 maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages.properties create mode 100644 maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages_fr.properties create mode 100644 maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages_fr_CA.properties create mode 100644 maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/NoBase_de.properties create mode 100644 maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/odd-name-es.txt create mode 100644 maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/odd-name.txt create mode 100644 maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/plain.txt diff --git a/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java b/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java index 31b306ae4a6..a1f5f30e9c8 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java @@ -58,6 +58,41 @@ public final class Relocation { // close() is the interface's only method, so code holding a Closeable // runs unchanged against it. {"java/io/Closeable", "java/lang/AutoCloseable"}, + // java.util classes the device library leaves out. + {"java/util/EventObject", JDK_PACKAGE + "EventObject"}, + {"java/util/Optional", JDK_PACKAGE + "Optional"}, + {"java/util/StringJoiner", JDK_PACKAGE + "StringJoiner"}, + {"java/util/ResourceBundle", JDK_PACKAGE + "ResourceBundle"}, + {"java/util/PropertyResourceBundle", JDK_PACKAGE + "PropertyResourceBundle"}, + {"java/util/ListResourceBundle", JDK_PACKAGE + "ListResourceBundle"}, + {"java/util/MissingResourceException", JDK_PACKAGE + "MissingResourceException"}, + // java.util.concurrent. The device has the atomics and + // ThreadLocalRandom; these it does not. None of them synchronizes: + // application code is confined to one thread. + {"java/util/concurrent/Callable", JDK_PACKAGE + "Callable"}, + {"java/util/concurrent/Future", JDK_PACKAGE + "Future"}, + {"java/util/concurrent/ExecutionException", JDK_PACKAGE + "ExecutionException"}, + {"java/util/concurrent/CancellationException", JDK_PACKAGE + "CancellationException"}, + {"java/util/concurrent/TimeoutException", JDK_PACKAGE + "TimeoutException"}, + {"java/util/concurrent/TimeUnit", JDK_PACKAGE + "TimeUnit"}, + {"java/util/concurrent/ConcurrentMap", JDK_PACKAGE + "ConcurrentMap"}, + {"java/util/concurrent/ConcurrentHashMap", JDK_PACKAGE + "ConcurrentHashMap"}, + // keySet() is declared to return this nested class, so compiled + // callers name it in the method descriptor. + {"java/util/concurrent/ConcurrentHashMap$KeySetView", JDK_PACKAGE + "ConcurrentHashMap$KeySetView"}, + {"java/util/concurrent/CopyOnWriteArrayList", JDK_PACKAGE + "CopyOnWriteArrayList"}, + // java.text. The device has Format, DateFormat, SimpleDateFormat, + // DateFormatSymbols and ParseException, and those stay its own. + {"java/text/NumberFormat", JDK_PACKAGE + "NumberFormat"}, + {"java/text/DecimalFormat", JDK_PACKAGE + "DecimalFormat"}, + {"java/text/DecimalFormatSymbols", JDK_PACKAGE + "DecimalFormatSymbols"}, + {"java/text/ChoiceFormat", JDK_PACKAGE + "ChoiceFormat"}, + {"java/text/MessageFormat", JDK_PACKAGE + "MessageFormat"}, + {"java/text/FieldPosition", JDK_PACKAGE + "FieldPosition"}, + {"java/text/ParsePosition", JDK_PACKAGE + "ParsePosition"}, + // java.net. The device has URI and URISyntaxException. + {"java/net/URL", JDK_PACKAGE + "URL"}, + {"java/net/MalformedURLException", JDK_PACKAGE + "MalformedURLException"}, }; private final String name; diff --git a/maven/compat-jdk/pom.xml b/maven/compat-jdk/pom.xml index f8da98308fb..8a80bdf745d 100644 --- a/maven/compat-jdk/pom.xml +++ b/maven/compat-jdk/pom.xml @@ -69,5 +69,10 @@ codenameone-core provided + + junit + junit + test + diff --git a/maven/compat-jdk/spotbugs-exclude.xml b/maven/compat-jdk/spotbugs-exclude.xml index 647e0f718ef..23c1e805f29 100644 --- a/maven/compat-jdk/spotbugs-exclude.xml +++ b/maven/compat-jdk/spotbugs-exclude.xml @@ -24,4 +24,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Callable.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Callable.java new file mode 100644 index 00000000000..2082d938df4 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Callable.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.util.concurrent.Callable` for the Codename One runtime: a task that +/// returns a result and may throw. +public interface Callable { + + V call() throws Exception; +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/CancellationException.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/CancellationException.java new file mode 100644 index 00000000000..8c189e67199 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/CancellationException.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.util.concurrent.CancellationException` for the Codename One +/// runtime: the result of a task cannot be retrieved because the task was +/// cancelled. +public class CancellationException extends IllegalStateException { + + private static final long serialVersionUID = 1L; + + public CancellationException() { + super(); + } + + public CancellationException(String message) { + super(message); + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ChoiceFormat.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ChoiceFormat.java new file mode 100644 index 00000000000..7d5ad7aee2b --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ChoiceFormat.java @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.text.ChoiceFormat` for the Codename One runtime: picks a string by +/// the range a number falls in. It is what the `choice` element of a +/// [MessageFormat] pattern uses to write plurals. +/// +/// A pattern is a list of `limit#text` pairs separated by `|`, the limits +/// ascending: `0#no files|1#one file|1<{0} files`. A number selects the last +/// pair whose limit it reaches; `#` includes the limit itself and `<` starts +/// just above it. A number below the first limit selects the first pair. A +/// single quote makes the characters up to the next one literal, and two in a +/// row stand for one quote. +public class ChoiceFormat extends NumberFormat { + + private static final long serialVersionUID = 1L; + + private static final char LESS_OR_EQUAL = (char) 0x2264; + private static final String INFINITY = String.valueOf((char) 0x221E); + + private static final long SIGN_BIT = 0x8000000000000000L; + private static final long POSITIVE_INFINITY_BITS = 0x7FF0000000000000L; + + private double[] choiceLimits = new double[0]; + private String[] choiceFormats = new String[0]; + + public ChoiceFormat(String newPattern) { + parsePattern(newPattern); + } + + public ChoiceFormat(double[] limits, String[] formats) { + store(limits, formats); + } + + public void applyPattern(String newPattern) { + parsePattern(newPattern); + } + + private void parsePattern(String newPattern) { + StringBuilder limitText = new StringBuilder(); + StringBuilder formatText = new StringBuilder(); + double[] limits = new double[8]; + String[] formats = new String[8]; + int count = 0; + boolean inFormat = false; + boolean inQuote = false; + double startValue = 0; + double oldStartValue = Double.NaN; + for (int i = 0; i < newPattern.length(); i++) { + char ch = newPattern.charAt(i); + StringBuilder current = inFormat ? formatText : limitText; + if (ch == '\'') { + if (i + 1 < newPattern.length() && newPattern.charAt(i + 1) == ch) { + current.append(ch); + i++; + } else { + inQuote = !inQuote; + } + } else if (inQuote) { + current.append(ch); + } else if (ch == '<' || ch == '#' || ch == LESS_OR_EQUAL) { + if (limitText.length() == 0) { + throw new IllegalArgumentException("A choice has no limit: " + newPattern); + } + startValue = parseLimit(limitText.toString().trim(), newPattern); + if (ch == '<' && !Double.isInfinite(startValue)) { + startValue = nextDouble(startValue); + } + if (startValue <= oldStartValue) { + throw new IllegalArgumentException("Choice limits are not ascending: " + newPattern); + } + limitText.setLength(0); + inFormat = true; + } else if (ch == '|') { + if (count == limits.length) { + double[] moreLimits = new double[count * 2]; + String[] moreFormats = new String[count * 2]; + System.arraycopy(limits, 0, moreLimits, 0, count); + System.arraycopy(formats, 0, moreFormats, 0, count); + limits = moreLimits; + formats = moreFormats; + } + limits[count] = startValue; + formats[count] = formatText.toString(); + count++; + oldStartValue = startValue; + formatText.setLength(0); + inFormat = false; + } else { + current.append(ch); + } + } + if (inFormat) { + if (count == limits.length) { + double[] moreLimits = new double[count + 1]; + String[] moreFormats = new String[count + 1]; + System.arraycopy(limits, 0, moreLimits, 0, count); + System.arraycopy(formats, 0, moreFormats, 0, count); + limits = moreLimits; + formats = moreFormats; + } + limits[count] = startValue; + formats[count] = formatText.toString(); + count++; + } + choiceLimits = new double[count]; + System.arraycopy(limits, 0, choiceLimits, 0, count); + choiceFormats = new String[count]; + System.arraycopy(formats, 0, choiceFormats, 0, count); + } + + private static double parseLimit(String text, String pattern) { + if (text.equals(INFINITY)) { + return Double.POSITIVE_INFINITY; + } + if (text.equals("-" + INFINITY)) { + return Double.NEGATIVE_INFINITY; + } + try { + return Double.parseDouble(text); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("A choice limit is not a number: " + pattern); + } + } + + private static double distanceToInteger(double value) { + return Math.abs(value - Math.floor(value + 0.5)); + } + + public String toPattern() { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < choiceLimits.length; i++) { + if (i != 0) { + result.append('|'); + } + // Either "limit#" or "justBelow<" says the same thing; the one + // that is closer to a whole number is the one that was written. + double limit = choiceLimits[i]; + double less = previousDouble(limit); + if (distanceToInteger(limit) < distanceToInteger(less)) { + appendLimit(result, limit); + result.append('#'); + } else { + appendLimit(result, less); + result.append('<'); + } + String text = choiceFormats[i]; + boolean needQuote = text.indexOf('<') >= 0 || text.indexOf('#') >= 0 + || text.indexOf(LESS_OR_EQUAL) >= 0 || text.indexOf('|') >= 0; + if (needQuote) { + result.append('\''); + } + for (int k = 0; k < text.length(); k++) { + char c = text.charAt(k); + result.append(c); + if (c == '\'') { + result.append(c); + } + } + if (needQuote) { + result.append('\''); + } + } + return result.toString(); + } + + private static void appendLimit(StringBuilder out, double limit) { + if (limit == Double.POSITIVE_INFINITY) { + out.append(INFINITY); + } else if (limit == Double.NEGATIVE_INFINITY) { + out.append('-').append(INFINITY); + } else { + out.append(Double.toString(limit)); + } + } + + public void setChoices(double[] limits, String[] formats) { + store(limits, formats); + } + + private void store(double[] limits, String[] formats) { + if (limits.length != formats.length) { + throw new IllegalArgumentException("Array and limit arrays must be of the same length."); + } + choiceLimits = limits.clone(); + choiceFormats = formats.clone(); + } + + public double[] getLimits() { + return choiceLimits.clone(); + } + + public Object[] getFormats() { + return choiceFormats.clone(); + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition status) { + return format((double) number, toAppendTo, status); + } + + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition status) { + if (choiceFormats.length == 0) { + return toAppendTo; + } + int i = 0; + // Written so that not-a-number, which is below nothing, selects the + // first choice. + while (i < choiceLimits.length && number >= choiceLimits[i]) { + i++; + } + i--; + if (i < 0) { + i = 0; + } + return toAppendTo.append(choiceFormats[i]); + } + + /// Finds the choice whose text starts at the position, the longest if + /// several do, and answers its limit as a `Double`. When none does, the + /// error index is set and the answer is not-a-number. + @Override + public Number parse(String text, ParsePosition status) { + int start = status.getIndex(); + int furthest = start; + double bestNumber = Double.NaN; + for (int i = 0; i < choiceFormats.length; i++) { + String candidate = choiceFormats[i]; + if (text.regionMatches(start, candidate, 0, candidate.length())) { + int end = start + candidate.length(); + if (end > furthest) { + furthest = end; + bestNumber = choiceLimits[i]; + if (furthest == text.length()) { + break; + } + } + } + } + status.setIndex(furthest); + if (furthest == start) { + status.setErrorIndex(furthest); + } + return Double.valueOf(bestNumber); + } + + public static final double nextDouble(double d) { + return nextDouble(d, true); + } + + public static final double previousDouble(double d) { + return nextDouble(d, false); + } + + /// The `double` next to `d`: the next greater when `positive`, else the + /// next smaller. + public static double nextDouble(double d, boolean positive) { + if (Double.isNaN(d)) { + return d; + } + if (d == 0.0) { + double smallest = Double.longBitsToDouble(1L); + return positive ? smallest : -smallest; + } + long bits = Double.doubleToLongBits(d); + long magnitude = bits & ~SIGN_BIT; + if ((bits > 0) == positive) { + if (magnitude != POSITIVE_INFINITY_BITS) { + magnitude += 1; + } + } else { + magnitude -= 1; + } + return Double.longBitsToDouble(magnitude | (bits & SIGN_BIT)); + } + + /// A copy of this format, built from copies of its arrays: + /// `Object.clone()` answers `null` on the device for anything but an + /// array. + @Override + public Object clone() { + ChoiceFormat copy = new ChoiceFormat(choiceLimits, choiceFormats); + copyNumberFormatTo(copy); + return copy; + } + + @Override + public int hashCode() { + int result = choiceLimits.length; + if (choiceFormats.length > 0) { + result ^= choiceFormats[choiceFormats.length - 1].hashCode(); + } + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ChoiceFormat) || getClass() != obj.getClass()) { + return false; + } + ChoiceFormat other = (ChoiceFormat) obj; + if (choiceLimits.length != other.choiceLimits.length) { + return false; + } + for (int i = 0; i < choiceLimits.length; i++) { + if (Double.doubleToLongBits(choiceLimits[i]) != Double.doubleToLongBits(other.choiceLimits[i]) + || !choiceFormats[i].equals(other.choiceFormats[i])) { + return false; + } + } + return true; + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ConcurrentHashMap.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ConcurrentHashMap.java new file mode 100644 index 00000000000..5986f979229 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ConcurrentHashMap.java @@ -0,0 +1,676 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; + +/// `java.util.concurrent.ConcurrentHashMap` for the Codename One runtime, +/// over a `HashMap`. +/// +/// **It does not synchronize.** Codename One user interface code is single +/// threaded: everything that touches components runs on the event dispatch +/// thread, and an application ported from the desktop keeps that shape. A map +/// used from a background thread must stay confined to that one thread, and +/// results cross to the event dispatch thread through `callSerially`, not +/// through a shared map. +/// +/// What it does keep from the JDK class is everything visible on one thread: +/// +/// - `null` keys and values are rejected with `NullPointerException`. +/// - Iterators never throw `ConcurrentModificationException`. Each iterates +/// over the entries present when it was created, so a loop may add to and +/// remove from the map it is walking; `Iterator.remove` and +/// `Map.Entry.setValue` write through to the map. +/// - The compound operations (`putIfAbsent`, `computeIfAbsent`, `compute`, +/// `merge`, ...) are atomic in the only sense that applies here: nothing +/// else runs between their read and their write. +/// +/// The bulk parallel operations (`forEach(long, ...)`, `search`, `reduce`) +/// and `forEachKey`/`forEachValue` are not provided. +public class ConcurrentHashMap extends AbstractMap implements ConcurrentMap, java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private final HashMap map; + + public ConcurrentHashMap() { + map = new HashMap(); + } + + public ConcurrentHashMap(int initialCapacity) { + if (initialCapacity < 0) { + throw new IllegalArgumentException(); + } + map = new HashMap(initialCapacity); + } + + public ConcurrentHashMap(int initialCapacity, float loadFactor) { + this(initialCapacity, loadFactor, 1); + } + + public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { + if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) { + throw new IllegalArgumentException(); + } + map = new HashMap(initialCapacity); + } + + public ConcurrentHashMap(Map m) { + map = new HashMap(); + putAll(m); + } + + private static void requireNonNull(Object o) { + if (o == null) { + throw new NullPointerException(); + } + } + + @Override + public int size() { + return map.size(); + } + + public long mappingCount() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public V get(Object key) { + requireNonNull(key); + return map.get(key); + } + + @Override + public boolean containsKey(Object key) { + requireNonNull(key); + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + requireNonNull(value); + return map.containsValue(value); + } + + public boolean contains(Object value) { + return containsValue(value); + } + + @Override + public V put(K key, V value) { + requireNonNull(key); + requireNonNull(value); + return map.put(key, value); + } + + @Override + public void putAll(Map m) { + for (Map.Entry e : m.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + @Override + public V remove(Object key) { + requireNonNull(key); + return map.remove(key); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public V getOrDefault(Object key, V defaultValue) { + V v = get(key); + return v == null ? defaultValue : v; + } + + @Override + public V putIfAbsent(K key, V value) { + requireNonNull(key); + requireNonNull(value); + V old = map.get(key); + if (old == null) { + map.put(key, value); + } + return old; + } + + @Override + public boolean remove(Object key, Object value) { + requireNonNull(key); + if (value == null) { + return false; + } + V old = map.get(key); + if (old != null && old.equals(value)) { + map.remove(key); + return true; + } + return false; + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + requireNonNull(key); + requireNonNull(oldValue); + requireNonNull(newValue); + V old = map.get(key); + if (old != null && old.equals(oldValue)) { + map.put(key, newValue); + return true; + } + return false; + } + + @Override + public V replace(K key, V value) { + requireNonNull(key); + requireNonNull(value); + V old = map.get(key); + if (old != null) { + map.put(key, value); + } + return old; + } + + @Override + public V computeIfAbsent(K key, Function mappingFunction) { + requireNonNull(key); + requireNonNull(mappingFunction); + V old = map.get(key); + if (old != null) { + return old; + } + V created = mappingFunction.apply(key); + if (created != null) { + map.put(key, created); + } + return created; + } + + public V computeIfPresent(K key, BiFunction remappingFunction) { + requireNonNull(key); + requireNonNull(remappingFunction); + V old = map.get(key); + if (old == null) { + return null; + } + V updated = remappingFunction.apply(key, old); + if (updated == null) { + map.remove(key); + } else { + map.put(key, updated); + } + return updated; + } + + public V compute(K key, BiFunction remappingFunction) { + requireNonNull(key); + requireNonNull(remappingFunction); + V old = map.get(key); + V updated = remappingFunction.apply(key, old); + if (updated == null) { + if (old != null) { + map.remove(key); + } + } else { + map.put(key, updated); + } + return updated; + } + + public V merge(K key, V value, BiFunction remappingFunction) { + requireNonNull(key); + requireNonNull(value); + requireNonNull(remappingFunction); + V old = map.get(key); + if (old == null) { + map.put(key, value); + return value; + } + V updated = remappingFunction.apply(old, value); + if (updated == null) { + map.remove(key); + } else { + map.put(key, updated); + } + return updated; + } + + @Override + public void forEach(BiConsumer action) { + requireNonNull(action); + for (Map.Entry e : snapshot()) { + action.accept(e.getKey(), e.getValue()); + } + } + + @Override + public void replaceAll(BiFunction function) { + requireNonNull(function); + for (Map.Entry e : snapshot()) { + V updated = function.apply(e.getKey(), e.getValue()); + requireNonNull(updated); + if (map.containsKey(e.getKey())) { + map.put(e.getKey(), updated); + } + } + } + + /// The entries present now, as entries that write through to this map. + /// Every iterator walks one of these, which is what lets a caller modify + /// the map while iterating it. + private List> snapshot() { + List> out = new ArrayList>(map.size()); + for (Map.Entry e : map.entrySet()) { + out.add(new WriteThroughEntry(this, e.getKey(), e.getValue())); + } + return out; + } + + /// The keys, as a view of this map. Compiled code names the JDK's return + /// type, `ConcurrentHashMap.KeySetView`, so that is what this returns. + @Override + public KeySetView keySet() { + return new KeySetView(this, null); + } + + public KeySetView keySet(V mappedValue) { + requireNonNull(mappedValue); + return new KeySetView(this, mappedValue); + } + + public static KeySetView newKeySet() { + return new KeySetView(new ConcurrentHashMap(), Boolean.TRUE); + } + + public static KeySetView newKeySet(int initialCapacity) { + return new KeySetView(new ConcurrentHashMap(initialCapacity), Boolean.TRUE); + } + + @Override + public Collection values() { + return new ValuesView(this); + } + + @Override + public Set> entrySet() { + return new EntrySetView(this); + } + + public Enumeration keys() { + return new IteratorEnumeration(keySet().iterator()); + } + + public Enumeration elements() { + return new IteratorEnumeration(values().iterator()); + } + + private static final class WriteThroughEntry implements Map.Entry { + private final ConcurrentHashMap owner; + private final K key; + private V value; + + WriteThroughEntry(ConcurrentHashMap owner, K key, V value) { + this.owner = owner; + this.key = key; + this.value = value; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V newValue) { + requireNonNull(newValue); + V old = value; + value = newValue; + owner.map.put(key, newValue); + return old; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry other = (Map.Entry) o; + Object otherKey = other.getKey(); + Object otherValue = other.getValue(); + return key.equals(otherKey) && value.equals(otherValue); + } + + @Override + public int hashCode() { + return key.hashCode() ^ value.hashCode(); + } + + @Override + public String toString() { + return key + "=" + value; + } + } + + /// Walks a snapshot of the entries; `remove` removes the key from the + /// live map. + private static final class SnapshotIterator { + private final ConcurrentHashMap owner; + private final List> entries; + private int next; + private Map.Entry last; + + SnapshotIterator(ConcurrentHashMap owner) { + this.owner = owner; + this.entries = owner.snapshot(); + } + + boolean hasNext() { + return next < entries.size(); + } + + Map.Entry nextEntry() { + if (next >= entries.size()) { + throw new NoSuchElementException(); + } + last = entries.get(next++); + return last; + } + + void remove() { + if (last == null) { + throw new IllegalStateException(); + } + owner.map.remove(last.getKey()); + last = null; + } + } + + private static final class KeyIterator implements Iterator { + private final SnapshotIterator it; + + KeyIterator(ConcurrentHashMap owner) { + it = new SnapshotIterator(owner); + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public K next() { + return it.nextEntry().getKey(); + } + + @Override + public void remove() { + it.remove(); + } + } + + private static final class ValueIterator implements Iterator { + private final SnapshotIterator it; + + ValueIterator(ConcurrentHashMap owner) { + it = new SnapshotIterator(owner); + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public V next() { + return it.nextEntry().getValue(); + } + + @Override + public void remove() { + it.remove(); + } + } + + private static final class EntryIterator implements Iterator> { + private final SnapshotIterator it; + + EntryIterator(ConcurrentHashMap owner) { + it = new SnapshotIterator(owner); + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Map.Entry next() { + return it.nextEntry(); + } + + @Override + public void remove() { + it.remove(); + } + } + + private static final class IteratorEnumeration implements Enumeration { + private final Iterator it; + + IteratorEnumeration(Iterator it) { + this.it = it; + } + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public T nextElement() { + return it.next(); + } + } + + private static final class ValuesView extends AbstractCollection { + private final ConcurrentHashMap owner; + + ValuesView(ConcurrentHashMap owner) { + this.owner = owner; + } + + @Override + public Iterator iterator() { + return new ValueIterator(owner); + } + + @Override + public int size() { + return owner.size(); + } + + @Override + public boolean contains(Object o) { + return o != null && owner.containsValue(o); + } + + @Override + public void clear() { + owner.clear(); + } + } + + private static final class EntrySetView extends AbstractSet> { + private final ConcurrentHashMap owner; + + EntrySetView(ConcurrentHashMap owner) { + this.owner = owner; + } + + @Override + public Iterator> iterator() { + return new EntryIterator(owner); + } + + @Override + public int size() { + return owner.size(); + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + Object key = e.getKey(); + Object value = e.getValue(); + if (key == null || value == null) { + return false; + } + Object mine = owner.get(key); + return mine != null && mine.equals(value); + } + + @Override + public boolean remove(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + Object key = e.getKey(); + Object value = e.getValue(); + return key != null && value != null && owner.remove(key, value); + } + + @Override + public void clear() { + owner.clear(); + } + } + + /// `java.util.concurrent.ConcurrentHashMap.KeySetView`: the keys of a + /// map as a set. A view created with a mapped value (`keySet(V)`, + /// `newKeySet()`) also accepts `add`, which maps the key to that value. + public static class KeySetView extends AbstractSet implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private final ConcurrentHashMap owner; + private final V value; + + KeySetView(ConcurrentHashMap owner, V value) { + this.owner = owner; + this.value = value; + } + + /// The map this is a view of. It is the live map, as in the JDK: + /// the view exists to share it. + public ConcurrentHashMap getMap() { + return owner; + } + + public V getMappedValue() { + return value; + } + + @Override + public Iterator iterator() { + return new KeyIterator(owner); + } + + @Override + public int size() { + return owner.size(); + } + + @Override + public boolean isEmpty() { + return owner.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return owner.containsKey(o); + } + + @Override + public boolean remove(Object o) { + return owner.remove(o) != null; + } + + @Override + public void clear() { + owner.clear(); + } + + @Override + public boolean add(K e) { + if (value == null) { + throw new UnsupportedOperationException(); + } + return owner.putIfAbsent(e, value) == null; + } + + @Override + public boolean addAll(Collection c) { + boolean added = false; + for (K e : c) { + if (add(e)) { + added = true; + } + } + return added; + } + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ConcurrentMap.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ConcurrentMap.java new file mode 100644 index 00000000000..a365f26ee18 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ConcurrentMap.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.util.Map; + +/// `java.util.concurrent.ConcurrentMap` for the Codename One runtime. The +/// four methods it declares are already default methods of the device's +/// `java.util.Map`; the interface exists so that code declaring a variable or +/// a parameter of this type keeps compiling and verifying. +public interface ConcurrentMap extends Map { + + @Override + V putIfAbsent(K key, V value); + + @Override + boolean remove(Object key, Object value); + + @Override + boolean replace(K key, V oldValue, V newValue); + + @Override + V replace(K key, V value); +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/CopyOnWriteArrayList.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/CopyOnWriteArrayList.java new file mode 100644 index 00000000000..2005d128a8e --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/CopyOnWriteArrayList.java @@ -0,0 +1,530 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.RandomAccess; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/// `java.util.concurrent.CopyOnWriteArrayList` for the Codename One runtime. +/// +/// Every mutation replaces the backing array with a modified copy, and an +/// iterator keeps the array that was current when it was created. That is the +/// property listener lists rely on, and it matters on a single thread as much +/// as across threads: a listener that removes itself, or adds another, while +/// the list is being walked to deliver an event neither disturbs the walk nor +/// throws `ConcurrentModificationException`. +/// +/// It does not synchronize. Codename One user interface code runs on the +/// event dispatch thread, and a list used elsewhere must stay confined to one +/// thread. +/// +/// Iterators are read only, as in the JDK: `remove`, `set` and `add` on one +/// throw `UnsupportedOperationException`. `subList` differs from the JDK: it +/// returns an unmodifiable copy of the range instead of a live view. +public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private static final Object[] EMPTY = new Object[0]; + + private Object[] array; + + public CopyOnWriteArrayList() { + array = EMPTY; + } + + public CopyOnWriteArrayList(Collection c) { + array = c.toArray(); + } + + public CopyOnWriteArrayList(E[] toCopyIn) { + Object[] copy = new Object[toCopyIn.length]; + System.arraycopy(toCopyIn, 0, copy, 0, copy.length); + array = copy; + } + + private CopyOnWriteArrayList(Object[] shared, boolean marker) { + array = shared; + } + + private static boolean same(Object a, Object b) { + return a == null ? b == null : a.equals(b); + } + + private static int indexOf(Object o, Object[] elements, int from, int to) { + for (int i = from; i < to; i++) { + if (same(o, elements[i])) { + return i; + } + } + return -1; + } + + private static int lastIndexOf(Object o, Object[] elements, int from) { + for (int i = from; i >= 0; i--) { + if (same(o, elements[i])) { + return i; + } + } + return -1; + } + + @SuppressWarnings("unchecked") + private static T element(Object[] elements, int index) { + return (T) elements[index]; + } + + @Override + public int size() { + return array.length; + } + + @Override + public boolean isEmpty() { + return array.length == 0; + } + + @Override + public boolean contains(Object o) { + Object[] elements = array; + return indexOf(o, elements, 0, elements.length) >= 0; + } + + @Override + public int indexOf(Object o) { + Object[] elements = array; + return indexOf(o, elements, 0, elements.length); + } + + public int indexOf(E e, int index) { + Object[] elements = array; + return indexOf(e, elements, index, elements.length); + } + + @Override + public int lastIndexOf(Object o) { + Object[] elements = array; + return lastIndexOf(o, elements, elements.length - 1); + } + + public int lastIndexOf(E e, int index) { + return lastIndexOf(e, array, index); + } + + /// A list sharing this one's current array. Neither list ever writes to + /// an array it has published, so the two are independent from here on, + /// which is all a copy has to be. + @Override + public Object clone() { + return new CopyOnWriteArrayList(array, true); + } + + @Override + public Object[] toArray() { + Object[] elements = array; + Object[] copy = new Object[elements.length]; + System.arraycopy(elements, 0, copy, 0, elements.length); + return copy; + } + + @Override + public T[] toArray(T[] a) { + Object[] elements = array; + if (a.length < elements.length) { + // Only a collection can create an array of the caller's type + // without reflection. + return new ArrayList(Arrays.asList(elements)).toArray(a); + } + System.arraycopy(elements, 0, a, 0, elements.length); + if (a.length > elements.length) { + a[elements.length] = null; + } + return a; + } + + @Override + public E get(int index) { + return element(array, index); + } + + @Override + public E set(int index, E element) { + Object[] elements = array; + E old = element(elements, index); + if (old != element) { + Object[] copy = new Object[elements.length]; + System.arraycopy(elements, 0, copy, 0, elements.length); + copy[index] = element; + array = copy; + } + return old; + } + + @Override + public boolean add(E e) { + Object[] elements = array; + Object[] copy = new Object[elements.length + 1]; + System.arraycopy(elements, 0, copy, 0, elements.length); + copy[elements.length] = e; + array = copy; + return true; + } + + @Override + public void add(int index, E element) { + Object[] elements = array; + int len = elements.length; + if (index > len || index < 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len); + } + Object[] copy = new Object[len + 1]; + System.arraycopy(elements, 0, copy, 0, index); + System.arraycopy(elements, index, copy, index + 1, len - index); + copy[index] = element; + array = copy; + } + + @Override + public E remove(int index) { + Object[] elements = array; + int len = elements.length; + E old = element(elements, index); + Object[] copy = new Object[len - 1]; + System.arraycopy(elements, 0, copy, 0, index); + System.arraycopy(elements, index + 1, copy, index, len - index - 1); + array = copy; + return old; + } + + @Override + public boolean remove(Object o) { + Object[] elements = array; + int index = indexOf(o, elements, 0, elements.length); + if (index < 0) { + return false; + } + remove(index); + return true; + } + + public boolean addIfAbsent(E e) { + if (contains(e)) { + return false; + } + return add(e); + } + + @Override + public boolean containsAll(Collection c) { + Object[] elements = array; + for (Object e : c) { + if (indexOf(e, elements, 0, elements.length) < 0) { + return false; + } + } + return true; + } + + /// Keeps the elements for which membership in `c` equals `keep`. + private boolean filter(Collection c, boolean keep) { + if (c == null) { + throw new NullPointerException(); + } + Object[] elements = array; + Object[] kept = new Object[elements.length]; + int n = 0; + for (int i = 0; i < elements.length; i++) { + if (c.contains(elements[i]) == keep) { + kept[n++] = elements[i]; + } + } + if (n == elements.length) { + return false; + } + Object[] copy = new Object[n]; + System.arraycopy(kept, 0, copy, 0, n); + array = copy; + return true; + } + + @Override + public boolean removeAll(Collection c) { + return filter(c, false); + } + + @Override + public boolean retainAll(Collection c) { + return filter(c, true); + } + + public int addAllAbsent(Collection c) { + int added = 0; + for (E e : c) { + if (addIfAbsent(e)) { + added++; + } + } + return added; + } + + @Override + public void clear() { + array = EMPTY; + } + + @Override + public boolean addAll(Collection c) { + return addAll(array.length, c); + } + + @Override + public boolean addAll(int index, Collection c) { + Object[] incoming = c.toArray(); + Object[] elements = array; + int len = elements.length; + if (index > len || index < 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len); + } + if (incoming.length == 0) { + return false; + } + Object[] copy = new Object[len + incoming.length]; + System.arraycopy(elements, 0, copy, 0, index); + System.arraycopy(incoming, 0, copy, index, incoming.length); + System.arraycopy(elements, index, copy, index + incoming.length, len - index); + array = copy; + return true; + } + + @Override + public void forEach(Consumer action) { + if (action == null) { + throw new NullPointerException(); + } + Object[] elements = array; + for (int i = 0; i < elements.length; i++) { + action.accept(CopyOnWriteArrayList.element(elements, i)); + } + } + + @Override + public boolean removeIf(Predicate filter) { + if (filter == null) { + throw new NullPointerException(); + } + Object[] elements = array; + Object[] kept = new Object[elements.length]; + int n = 0; + for (int i = 0; i < elements.length; i++) { + if (!filter.test(CopyOnWriteArrayList.element(elements, i))) { + kept[n++] = elements[i]; + } + } + if (n == elements.length) { + return false; + } + Object[] copy = new Object[n]; + System.arraycopy(kept, 0, copy, 0, n); + array = copy; + return true; + } + + @Override + public void replaceAll(UnaryOperator operator) { + if (operator == null) { + throw new NullPointerException(); + } + Object[] elements = array; + Object[] copy = new Object[elements.length]; + for (int i = 0; i < elements.length; i++) { + copy[i] = operator.apply(CopyOnWriteArrayList.element(elements, i)); + } + array = copy; + } + + @Override + public void sort(Comparator c) { + Object[] copy = toArray(); + if (c == null) { + Arrays.sort(copy); + } else { + sortWith(copy, c); + } + array = copy; + } + + @SuppressWarnings("unchecked") + private static void sortWith(Object[] elements, Comparator c) { + // Every element was added as an E, which is what T stands for here. + Arrays.sort((T[]) elements, c); + } + + @Override + public String toString() { + Object[] elements = array; + StringBuilder sb = new StringBuilder(); + sb.append('['); + for (int i = 0; i < elements.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(elements[i] == this ? "(this Collection)" : String.valueOf(elements[i])); + } + return sb.append(']').toString(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof List)) { + return false; + } + Object[] elements = array; + Iterator it = ((List) o).iterator(); + for (int i = 0; i < elements.length; i++) { + if (!it.hasNext() || !same(elements[i], it.next())) { + return false; + } + } + return !it.hasNext(); + } + + @Override + public int hashCode() { + int hash = 1; + Object[] elements = array; + for (int i = 0; i < elements.length; i++) { + hash = 31 * hash + (elements[i] == null ? 0 : elements[i].hashCode()); + } + return hash; + } + + @Override + public Iterator iterator() { + return new SnapshotIterator(array, 0); + } + + @Override + public ListIterator listIterator() { + return new SnapshotIterator(array, 0); + } + + @Override + public ListIterator listIterator(int index) { + Object[] elements = array; + if (index < 0 || index > elements.length) { + throw new IndexOutOfBoundsException("Index: " + index); + } + return new SnapshotIterator(elements, index); + } + + /// An unmodifiable copy of the range. The JDK returns a live view; code + /// that writes through a sublist of a copy-on-write list gets an + /// `UnsupportedOperationException` here instead of a lost update. + @Override + public List subList(int fromIndex, int toIndex) { + Object[] elements = array; + if (fromIndex < 0 || toIndex > elements.length || fromIndex > toIndex) { + throw new IndexOutOfBoundsException(); + } + List out = new ArrayList(toIndex - fromIndex); + for (int i = fromIndex; i < toIndex; i++) { + out.add(CopyOnWriteArrayList.element(elements, i)); + } + return Collections.unmodifiableList(out); + } + + private static final class SnapshotIterator implements ListIterator { + private final Object[] snapshot; + private int cursor; + + SnapshotIterator(Object[] snapshot, int cursor) { + this.snapshot = snapshot; + this.cursor = cursor; + } + + @Override + public boolean hasNext() { + return cursor < snapshot.length; + } + + @Override + public boolean hasPrevious() { + return cursor > 0; + } + + @Override + public E next() { + if (cursor >= snapshot.length) { + throw new NoSuchElementException(); + } + return element(snapshot, cursor++); + } + + @Override + public E previous() { + if (cursor <= 0) { + throw new NoSuchElementException(); + } + return element(snapshot, --cursor); + } + + @Override + public int nextIndex() { + return cursor; + } + + @Override + public int previousIndex() { + return cursor - 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void set(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(E e) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/DecimalFormat.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/DecimalFormat.java new file mode 100644 index 00000000000..0cbd0cabd5b --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/DecimalFormat.java @@ -0,0 +1,1394 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import com.codename1.util.BigInteger; + +/// `java.text.DecimalFormat` for the Codename One runtime: formats and +/// parses decimal numbers according to a pattern. +/// +/// #### Pattern syntax +/// +/// A pattern is a positive subpattern, optionally followed by `;` and a +/// negative subpattern of which only the prefix and suffix are used. Each +/// subpattern is a prefix, a number and a suffix. In the number: +/// +/// - `0` is a digit that is always written, `#` a digit written only when +/// it is significant. +/// - `.` is the decimal separator and `,` the grouping separator; the +/// distance from the last `,` to the end of the integer part is the group +/// size. +/// - `E` followed by one or more `0` selects scientific notation, with at +/// least that many exponent digits. With more integer digit positions than +/// `0`s among them (`##0.###E0`), the exponent is kept a multiple of the +/// number of integer positions. +/// +/// In a prefix or suffix, `%` multiplies the number by 100 and writes a +/// percent sign, the per mille sign multiplies by 1000, the currency sign +/// writes the currency symbol (doubled, the international currency code), and +/// `-` writes the minus sign. A single quote makes the characters up to the +/// next one literal, and two in a row stand for one quote. +/// +/// The characters actually written -- separators, minus, percent, currency +/// -- come from the format's [DecimalFormatSymbols]. +/// +/// #### Rounding +/// +/// Rounding is half-even, the JDK default, and like the JDK it is decided by +/// the exact binary value of a `double`: `0.125` is a true tie and formats as +/// `0.12` under `0.00`, while `0.15`, which as a `double` lies just below the +/// decimal `0.15`, formats as `0.1` under `0.0`. +/// +/// The JDK departs from half-even in two places, and so does this class, +/// because a last digit that differs from the desktop's is worse than either +/// answer: a whole number rounds a tie up in scientific notation (`12345` +/// under `0.###E0` is `1.235E4`), and `5E-4`, `5E-5` and so on round down +/// when the 5 is the first digit dropped (`0.0005` under `0.000` is `0.000`). +/// +/// #### Differences from the JDK +/// +/// - No `setRoundingMode`, `setCurrency`, `setParseBigDecimal` or +/// `formatToCharacterIterator`: the device has none of the types they name. +/// - No localized patterns (`applyLocalizedPattern`, `toLocalizedPattern`). +/// - It is not a `java.text.Format`; see [NumberFormat]. +public class DecimalFormat extends NumberFormat { + + private static final long serialVersionUID = 1L; + + /// The currency sign of a pattern. Not written as a character literal: + /// the sources are ASCII. + static final char CURRENCY_SIGN = (char) 0xA4; + + private static final char PER_MILLE_SIGN = (char) 0x2030; + private static final char QUOTE = '\''; + + /// A `double` has no more integer digits than this, nor fraction digits + /// than the next; the JDK caps the settings there for a `double` as well. + private static final int DOUBLE_INTEGER_DIGITS = 309; + private static final int DOUBLE_FRACTION_DIGITS = 340; + + private DecimalFormatSymbols symbols; + + // The affixes as the pattern gave them, in an internal form where a quote + // precedes each character to be replaced by a symbol and '' is a quote. + private String posPrefixPattern = ""; + private String posSuffixPattern = ""; + private String negPrefixPattern = "'-"; + private String negSuffixPattern = ""; + + // The same affixes with the symbols filled in. + private String positivePrefix = ""; + private String positiveSuffix = ""; + private String negativePrefix = "-"; + private String negativeSuffix = ""; + + private int multiplier = 1; + private int groupingSize = 3; + private boolean decimalSeparatorAlwaysShown; + private boolean useExponentialNotation; + private int minExponentDigits; + private boolean currencyFormat; + + public DecimalFormat() { + this("#,##0.###", DecimalFormatSymbols.getInstance()); + } + + public DecimalFormat(String pattern) { + this(pattern, DecimalFormatSymbols.getInstance()); + } + + public DecimalFormat(String pattern, DecimalFormatSymbols symbols) { + if (symbols == null) { + throw new NullPointerException(); + } + Object copy = symbols.clone(); + this.symbols = copy instanceof DecimalFormatSymbols ? (DecimalFormatSymbols) copy : symbols; + parsePattern(pattern); + } + + // ------------------------------------------------------------------ + // Digits + // ------------------------------------------------------------------ + + /// A non-negative decimal number: `count` significant digits, with the + /// decimal point `decimalAt` digits from the left (so the value is + /// 0.d1d2... times ten to the `decimalAt`). No trailing zeros; zero is no + /// digits at all. + private static final class Digits { + /// The digits are the number: a tie goes to the even neighbour. + static final int TIE_EVEN = 0; + /// The digits are the shortest text of a `double`: a tie is settled + /// by where the `double` really lies, and is even only if it lies + /// exactly there. + static final int TIE_BY_VALUE = 1; + static final int TIE_UP = 2; + static final int TIE_DOWN = 3; + + private char[] d = new char[24]; + private int count; + private int decimalAt; + + private void append(char c) { + if (count == d.length) { + char[] bigger = new char[d.length * 2]; + System.arraycopy(d, 0, bigger, 0, count); + d = bigger; + } + d[count++] = c; + } + + private void trim() { + while (count > 0 && d[count - 1] == '0') { + count--; + } + if (count == 0) { + decimalAt = 0; + } + } + + /// Takes plain decimal digits, an integer. + void setInteger(String digits) { + count = 0; + int i = 0; + while (i < digits.length() && digits.charAt(i) == '0') { + i++; + } + decimalAt = digits.length() - i; + for (; i < digits.length(); i++) { + append(digits.charAt(i)); + } + trim(); + } + + /// Takes the shortest decimal text that identifies a finite, + /// non-negative `double`, as `Double.toString` writes it: digits with + /// a decimal point, and an optional exponent. + void setDouble(double value) { + String s = Double.toString(value); + int exponent = 0; + int e = s.indexOf('E'); + if (e < 0) { + e = s.indexOf('e'); + } + int end = s.length(); + if (e >= 0) { + exponent = parseExponent(s, e + 1); + end = e; + } + count = 0; + int pointAt = -1; + int seen = 0; + boolean leading = true; + int leadingZerosAfterPoint = 0; + for (int i = 0; i < end; i++) { + char c = s.charAt(i); + if (c == '.' || c == ',') { + // Some ports write the device locale's decimal comma. + pointAt = seen; + continue; + } + if (c < '0' || c > '9') { + continue; + } + if (leading && c == '0') { + if (pointAt >= 0) { + leadingZerosAfterPoint++; + } + continue; + } + leading = false; + append(c); + seen++; + } + if (pointAt < 0) { + pointAt = seen; + } + decimalAt = pointAt - leadingZerosAfterPoint + exponent; + trim(); + } + + private static int parseExponent(String s, int from) { + boolean negative = false; + int i = from; + if (i < s.length() && (s.charAt(i) == '-' || s.charAt(i) == '+')) { + negative = s.charAt(i) == '-'; + i++; + } + int value = 0; + for (; i < s.length(); i++) { + char c = s.charAt(i); + if (c >= '0' && c <= '9') { + value = value * 10 + (c - '0'); + } + } + return negative ? -value : value; + } + + /// Multiplies by a positive integer. + void multiply(int by) { + if (count == 0) { + return; + } + char[] out = new char[count + 11]; + int at = out.length; + long carry = 0; + for (int i = count - 1; i >= 0; i--) { + long v = (long) (d[i] - '0') * by + carry; + out[--at] = (char) ('0' + (int) (v % 10)); + carry = v / 10; + } + while (carry > 0) { + out[--at] = (char) ('0' + (int) (carry % 10)); + carry = carry / 10; + } + int produced = out.length - at; + decimalAt += produced - count; + count = 0; + for (int i = at; i < out.length; i++) { + append(out[i]); + } + trim(); + } + + /// Keeps the first `keep` digits, rounding half-even. `tie` says how + /// digits that end in exactly a half are settled; `value` is the + /// `double` they were taken from, for [#TIE_BY_VALUE]. + void round(int keep, int tie, double value) { + if (keep >= count) { + return; + } + if (keep < 0) { + count = 0; + decimalAt = 0; + return; + } + if (roundsUp(keep, tie, value)) { + int i = keep - 1; + while (i >= 0 && d[i] == '9') { + i--; + } + if (i < 0) { + d[0] = '1'; + count = 1; + decimalAt++; + } else { + d[i]++; + count = i + 1; + } + } else { + count = keep; + } + trim(); + } + + private boolean roundsUp(int keep, int tie, double value) { + char next = d[keep]; + if (next != '5') { + return next > '5'; + } + if (keep + 1 < count) { + // Past the 5 there is something, and it is not zero. + return true; + } + // The digits end in exactly a half. + if (tie == TIE_UP) { + return true; + } + if (tie == TIE_DOWN) { + return false; + } + // A long is those digits. A double is only the nearest double to + // them, so the true value is compared against the half it + // appears to be. + int side = tie == TIE_EVEN ? 0 : compareToDigits(value); + if (side != 0) { + return side > 0; + } + return keep > 0 && ((d[keep - 1] - '0') & 1) == 1; + } + + /// The sign of `value` minus the number these digits spell. + private int compareToDigits(double value) { + long bits = Double.doubleToLongBits(value); + int rawExponent = (int) ((bits >> 52) & 0x7ffL); + long mantissa = bits & 0xfffffffffffffL; + int binaryExponent; + if (rawExponent == 0) { + binaryExponent = -1074; + } else { + mantissa |= 1L << 52; + binaryExponent = rawExponent - 1075; + } + // value = mantissa * 2^binaryExponent, digits = n * 10^decimalExponent. + // Negative powers move to the other side, leaving integers. + BigInteger left = BigInteger.valueOf(mantissa); + BigInteger right = new BigInteger(new String(d, 0, count)); + int decimalExponent = decimalAt - count; + if (binaryExponent > 0) { + left = left.shiftLeft(binaryExponent); + } else if (binaryExponent < 0) { + right = right.shiftLeft(-binaryExponent); + } + BigInteger ten = BigInteger.valueOf(10); + if (decimalExponent > 0) { + right = right.multiply(ten.pow(decimalExponent)); + } else if (decimalExponent < 0) { + left = left.multiply(ten.pow(-decimalExponent)); + } + return left.compareTo(right); + } + } + + // ------------------------------------------------------------------ + // Formatting + // ------------------------------------------------------------------ + + @Override + public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) { + fieldPosition.setBeginIndex(0); + fieldPosition.setEndIndex(0); + if (Double.isNaN(number)) { + int begin = result.length(); + result.append(symbols.getNaN()); + if (fieldPosition.getField() == INTEGER_FIELD) { + fieldPosition.setBeginIndex(begin); + fieldPosition.setEndIndex(result.length()); + } + return result; + } + // The sign bit, so that negative zero is negative. + boolean negative = (Double.doubleToLongBits(number) < 0) ^ (multiplier < 0); + double value = number; + if (multiplier != 1) { + value = value * multiplier; + } + if (Double.isInfinite(value)) { + result.append(negative ? negativePrefix : positivePrefix); + int begin = result.length(); + result.append(symbols.getInfinity()); + if (fieldPosition.getField() == INTEGER_FIELD) { + fieldPosition.setBeginIndex(begin); + fieldPosition.setEndIndex(result.length()); + } + result.append(negative ? negativeSuffix : positiveSuffix); + return result; + } + value = Math.abs(value); + int maxInt = Math.min(getMaximumIntegerDigits(), DOUBLE_INTEGER_DIGITS); + int minInt = Math.min(getMinimumIntegerDigits(), DOUBLE_INTEGER_DIGITS); + int maxFrac = Math.min(getMaximumFractionDigits(), DOUBLE_FRACTION_DIGITS); + int minFrac = Math.min(getMinimumFractionDigits(), DOUBLE_FRACTION_DIGITS); + Digits digits = new Digits(); + digits.setDouble(value); + // Two places where the JDK does not round half-even, kept because + // output that differs from the desktop's in the last digit is worse + // than either answer. A whole number below two to the 63rd rounds a + // tie up (12345 under 0.###E0 is 1.235E4), and 5E-4, 5E-5, ... round + // down when the 5 is the first digit dropped (0.0005 under 0.000 is + // 0.000, though 0.005 under 0.00 is 0.01). + int tie = Digits.TIE_BY_VALUE; + if (value < 9.2E18 && (double) (long) value == value) { + tie = Digits.TIE_UP; + } + if (useExponentialNotation) { + digits.round(maxInt + maxFrac, tie, value); + } else { + int keep = digits.decimalAt + maxFrac; + if (keep == 0 && value < 1.0E-3 && !isCommonShape(maxInt, minInt, maxFrac, minFrac)) { + tie = Digits.TIE_DOWN; + } + digits.round(keep, tie, value); + } + return subformat(result, fieldPosition, digits, negative, false, maxInt, minInt, maxFrac, minFrac); + } + + /// The shapes the JDK formats by a separate, exact route -- the default + /// number and currency patterns -- where neither exception applies. + private boolean isCommonShape(int maxInt, int minInt, int maxFrac, int minFrac) { + if (!isGroupingUsed() || groupingSize != 3 || multiplier != 1 || decimalSeparatorAlwaysShown + || minInt != 1 || maxInt < 10) { + return false; + } + return currencyFormat ? minFrac == 2 && maxFrac == 2 : minFrac == 0 && maxFrac == 3; + } + + @Override + public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) { + fieldPosition.setBeginIndex(0); + fieldPosition.setEndIndex(0); + boolean negative = number < 0; + String text = Long.toString(number); + Digits digits = new Digits(); + digits.setInteger(negative ? text.substring(1) : text); + if (multiplier != 1) { + if (multiplier == 0) { + digits.setInteger("0"); + } else if (multiplier < 0) { + negative = !negative; + if (multiplier == Integer.MIN_VALUE) { + digits.multiply(1 << 30); + digits.multiply(2); + } else { + digits.multiply(-multiplier); + } + } else { + digits.multiply(multiplier); + } + } + int maxInt = getMaximumIntegerDigits(); + int minInt = getMinimumIntegerDigits(); + int maxFrac = getMaximumFractionDigits(); + int minFrac = getMinimumFractionDigits(); + if (useExponentialNotation) { + int significant = maxInt + maxFrac; + if (significant > 0) { + digits.round(significant, Digits.TIE_EVEN, 0); + } + } + if (digits.count == 0) { + // An integer zero has no sign. + negative = false; + } + return subformat(result, fieldPosition, digits, negative, true, maxInt, minInt, maxFrac, minFrac); + } + + private char digitChar(char asciiDigit) { + return (char) (asciiDigit - '0' + symbols.getZeroDigit()); + } + + private StringBuffer subformat(StringBuffer result, FieldPosition fieldPosition, Digits digits, + boolean negative, boolean isInteger, + int maxInt, int minInt, int maxFrac, int minFrac) { + char zero = symbols.getZeroDigit(); + char decimal = currencyFormat ? symbols.getMonetaryDecimalSeparator() : symbols.getDecimalSeparator(); + result.append(negative ? negativePrefix : positivePrefix); + int integerBegin = result.length(); + int integerEnd = -1; + int fractionBegin = -1; + if (useExponentialNotation) { + // The exponent is what leaves the wanted number of integer + // digits; with more integer positions than required digits it is + // also kept a multiple of the number of positions. + int exponent = digits.decimalAt; + int repeat = maxInt; + int minimumIntegerDigits = minInt; + if (repeat > 1 && repeat > minInt) { + if (exponent >= 1) { + exponent = ((exponent - 1) / repeat) * repeat; + } else { + exponent = ((exponent - repeat) / repeat) * repeat; + } + minimumIntegerDigits = 1; + } else { + exponent -= minimumIntegerDigits; + } + int minimumDigits = minInt + minFrac; + if (minimumDigits < 0) { + minimumDigits = Integer.MAX_VALUE; + } + int integerDigits = digits.count == 0 ? minimumIntegerDigits : digits.decimalAt - exponent; + if (minimumDigits < integerDigits) { + minimumDigits = integerDigits; + } + int totalDigits = Math.max(digits.count, minimumDigits); + boolean wroteDecimal = false; + for (int i = 0; i < totalDigits; i++) { + if (i == integerDigits) { + integerEnd = result.length(); + result.append(decimal); + wroteDecimal = true; + fractionBegin = result.length(); + } + result.append(i < digits.count ? digitChar(digits.d[i]) : zero); + } + if (decimalSeparatorAlwaysShown && totalDigits == integerDigits) { + integerEnd = result.length(); + result.append(decimal); + wroteDecimal = true; + fractionBegin = result.length(); + } + if (integerEnd < 0) { + integerEnd = result.length(); + } + if (!wroteDecimal) { + fractionBegin = result.length(); + } + int fractionEnd = result.length(); + result.append(symbols.getExponentSeparator()); + if (digits.count == 0) { + exponent = 0; + } + if (exponent < 0) { + exponent = -exponent; + result.append(symbols.getMinusSign()); + } + String exponentText = Integer.toString(exponent); + for (int i = exponentText.length(); i < minExponentDigits; i++) { + result.append(zero); + } + for (int i = 0; i < exponentText.length(); i++) { + result.append(digitChar(exponentText.charAt(i))); + } + setField(fieldPosition, integerBegin, integerEnd, fractionBegin, fractionEnd); + } else { + int count = minInt; + int digitIndex = 0; + if (digits.decimalAt > 0 && count < digits.decimalAt) { + count = digits.decimalAt; + } + // With fewer integer digits allowed than the number has, the + // least significant ones are the ones written: 1997 under a + // maximum of two is "97". + if (count > maxInt) { + count = maxInt; + digitIndex = digits.decimalAt - count; + } + boolean grouping = isGroupingUsed() && groupingSize > 0; + for (int i = count - 1; i >= 0; i--) { + if (i < digits.decimalAt && digitIndex < digits.count) { + result.append(digitChar(digits.d[digitIndex++])); + } else { + result.append(zero); + } + // groupingSize is checked positive just above. + if (grouping && i > 0 && i % groupingSize == 0) { + result.append(symbols.getGroupingSeparator()); + } + } + boolean fractionPresent = minFrac > 0 || (!isInteger && digitIndex < digits.count); + if (!fractionPresent && result.length() == integerBegin) { + result.append(zero); + } + integerEnd = result.length(); + if (decimalSeparatorAlwaysShown || fractionPresent) { + result.append(decimal); + } + fractionBegin = result.length(); + for (int i = 0; i < maxFrac; i++) { + if (i >= minFrac && (isInteger || digitIndex >= digits.count)) { + break; + } + // Zeros between the decimal point and the first digit. + if (-1 - i > digits.decimalAt - 1) { + result.append(zero); + continue; + } + if (!isInteger && digitIndex < digits.count) { + result.append(digitChar(digits.d[digitIndex++])); + } else { + result.append(zero); + } + } + setField(fieldPosition, integerBegin, integerEnd, fractionBegin, result.length()); + } + result.append(negative ? negativeSuffix : positiveSuffix); + return result; + } + + private static void setField(FieldPosition fieldPosition, int integerBegin, int integerEnd, + int fractionBegin, int fractionEnd) { + if (fieldPosition.getField() == INTEGER_FIELD) { + fieldPosition.setBeginIndex(integerBegin); + fieldPosition.setEndIndex(integerEnd); + } else if (fieldPosition.getField() == FRACTION_FIELD) { + fieldPosition.setBeginIndex(fractionBegin); + fieldPosition.setEndIndex(fractionEnd); + } + } + + // ------------------------------------------------------------------ + // Parsing + // ------------------------------------------------------------------ + + private int digitValue(char c) { + int v = c - symbols.getZeroDigit(); + if (v >= 0 && v <= 9) { + return v; + } + v = c - '0'; + return v >= 0 && v <= 9 ? v : -1; + } + + private static boolean matches(String text, int at, String part) { + return text.regionMatches(at, part, 0, part.length()); + } + + @Override + public Number parse(String text, ParsePosition pos) { + int start = pos.getIndex(); + int position = start; + String nan = symbols.getNaN(); + if (nan.length() > 0 && matches(text, position, nan)) { + pos.setIndex(position + nan.length()); + return Double.valueOf(Double.NaN); + } + boolean gotPositive = matches(text, position, positivePrefix); + boolean gotNegative = matches(text, position, negativePrefix); + if (gotPositive && gotNegative) { + if (positivePrefix.length() > negativePrefix.length()) { + gotNegative = false; + } else if (positivePrefix.length() < negativePrefix.length()) { + gotPositive = false; + } + } + if (gotPositive) { + position += positivePrefix.length(); + } else if (gotNegative) { + position += negativePrefix.length(); + } else { + pos.setErrorIndex(position); + return null; + } + + boolean infinite = false; + StringBuilder digits = new StringBuilder(); + int decimalAt = 0; + String infinity = symbols.getInfinity(); + if (infinity.length() > 0 && matches(text, position, infinity)) { + position += infinity.length(); + infinite = true; + } else { + char decimal = currencyFormat ? symbols.getMonetaryDecimalSeparator() : symbols.getDecimalSeparator(); + char grouping = symbols.getGroupingSeparator(); + String exponentText = symbols.getExponentSeparator(); + boolean sawDecimal = false; + boolean sawDigit = false; + int exponent = 0; + int digitCount = 0; + int backup = -1; + for (; position < text.length(); position++) { + char ch = text.charAt(position); + int digit = digitValue(ch); + if (digit >= 0) { + backup = -1; + sawDigit = true; + if (digit == 0 && digits.length() == 0) { + // Leading zeros carry no digits, but after the + // decimal point they shift it. + if (sawDecimal) { + decimalAt--; + } + } else { + digitCount++; + digits.append((char) ('0' + digit)); + } + } else if (ch == decimal) { + if (isParseIntegerOnly() || sawDecimal) { + break; + } + decimalAt = digitCount; + sawDecimal = true; + } else if (ch == grouping && isGroupingUsed()) { + if (sawDecimal) { + break; + } + // A grouping separator must be followed by a digit; if + // it is not, the number ended before it. + backup = position; + } else if (exponentText.length() > 0 && matches(text, position, exponentText)) { + int at = position + exponentText.length(); + boolean negativeExponent = false; + if (at < text.length() && text.charAt(at) == symbols.getMinusSign()) { + negativeExponent = true; + at++; + } + int exponentStart = at; + long parsed = 0; + while (at < text.length() && digitValue(text.charAt(at)) >= 0) { + if (parsed < 100000000L) { + parsed = parsed * 10 + digitValue(text.charAt(at)); + } + at++; + } + if (at > exponentStart) { + exponent = (int) (negativeExponent ? -parsed : parsed); + position = at; + } + break; + } else { + break; + } + } + if (backup != -1) { + position = backup; + } + if (!sawDecimal) { + decimalAt = digitCount; + } + decimalAt += exponent; + if (!sawDigit) { + // As in the JDK, a prefix with no number after it is reported + // at the start of the prefix. + pos.setErrorIndex(start); + return null; + } + } + + if (gotPositive) { + gotPositive = matches(text, position, positiveSuffix); + } + if (gotNegative) { + gotNegative = matches(text, position, negativeSuffix); + } + if (gotPositive && gotNegative) { + if (positiveSuffix.length() > negativeSuffix.length()) { + gotNegative = false; + } else if (positiveSuffix.length() < negativeSuffix.length()) { + gotPositive = false; + } + } + if (gotPositive == gotNegative) { + pos.setErrorIndex(position); + return null; + } + position += gotPositive ? positiveSuffix.length() : negativeSuffix.length(); + pos.setIndex(position); + + if (infinite) { + boolean positiveResult = gotPositive == (multiplier >= 0); + return Double.valueOf(positiveResult ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY); + } + return toNumber(digits, decimalAt, gotPositive); + } + + private static final String LONG_MIN_DIGITS = "9223372036854775808"; + + /// Whether these digits, an integer with `decimalAt` digits before the + /// point, fit a `long` of the given sign. + private static boolean fitsLong(StringBuilder digits, int decimalAt, boolean positive) { + int count = digits.length(); + if (decimalAt < count || decimalAt > LONG_MIN_DIGITS.length()) { + return false; + } + if (decimalAt < LONG_MIN_DIGITS.length()) { + return true; + } + for (int i = 0; i < count; i++) { + char mine = digits.charAt(i); + char limit = LONG_MIN_DIGITS.charAt(i); + if (mine > limit) { + return false; + } + if (mine < limit) { + return true; + } + } + if (count < decimalAt) { + return true; + } + // Exactly the magnitude of Long.MIN_VALUE. + return !positive; + } + + private Number toNumber(StringBuilder digits, int decimalAt, boolean positive) { + while (digits.length() > 0 && digits.charAt(digits.length() - 1) == '0') { + digits.setLength(digits.length() - 1); + } + boolean gotDouble; + long longResult = 0; + double doubleResult = 0.0; + if (digits.length() == 0) { + if (positive || isParseIntegerOnly()) { + gotDouble = false; + } else { + gotDouble = true; + doubleResult = -0.0; + } + } else if (fitsLong(digits, decimalAt, positive)) { + gotDouble = false; + StringBuilder sb = new StringBuilder(); + if (!positive) { + sb.append('-'); + } + sb.append(digits.toString()); + for (int i = digits.length(); i < decimalAt; i++) { + sb.append('0'); + } + longResult = Long.parseLong(sb.toString()); + } else { + gotDouble = true; + StringBuilder sb = new StringBuilder(); + if (!positive) { + sb.append('-'); + } + sb.append(digits.charAt(0)).append('.'); + if (digits.length() > 1) { + sb.append(digits.toString().substring(1)); + } else { + sb.append('0'); + } + sb.append('E').append(decimalAt - 1); + doubleResult = Double.parseDouble(sb.toString()); + } + // A multiplier of zero formats everything as zero; there is nothing + // to divide back by. + if (multiplier != 1 && multiplier != 0) { + if (gotDouble) { + doubleResult = doubleResult / multiplier; + } else if (longResult % multiplier == 0) { + longResult = longResult / multiplier; + } else { + doubleResult = (double) longResult / multiplier; + gotDouble = true; + } + if (gotDouble) { + long whole = (long) doubleResult; + boolean negativeZero = doubleResult == 0.0 && Double.doubleToLongBits(doubleResult) < 0; + if (doubleResult == (double) whole && !negativeZero) { + gotDouble = false; + longResult = whole; + } else if (isParseIntegerOnly()) { + gotDouble = false; + longResult = whole; + } + } + } + if (gotDouble) { + return Double.valueOf(doubleResult); + } + return Long.valueOf(longResult); + } + + // ------------------------------------------------------------------ + // Pattern + // ------------------------------------------------------------------ + + public void applyPattern(String pattern) { + parsePattern(pattern); + } + + private static IllegalArgumentException malformed(String why, String pattern) { + return new IllegalArgumentException(why + " in pattern \"" + pattern + '"'); + } + + private static boolean isNumberChar(char ch) { + return ch == '#' || ch == '0' || ch == ',' || ch == '.'; + } + + private void parsePattern(String pattern) { + if (pattern == null) { + throw new NullPointerException(); + } + int len = pattern.length(); + boolean gotNegative = false; + boolean exponential = false; + boolean currency = false; + int exponentDigits = 0; + int newMultiplier = 1; + String newPosPrefix = ""; + String newPosSuffix = ""; + String newNegPrefix = ""; + String newNegSuffix = ""; + int digitLeftCount = 0; + int zeroDigitCount = 0; + int digitRightCount = 0; + int decimalPos = -1; + int groupingCount = -1; + int numberLength = 0; + + int start = 0; + for (int part = 0; part < 2 && start < len; part++) { + boolean positive = part == 0; + StringBuilder prefix = new StringBuilder(); + StringBuilder suffix = new StringBuilder(); + StringBuilder affix = prefix; + int phase = 0; + boolean inQuote = false; + boolean sawSeparator = false; + int partMultiplier = 1; + int toSkip = numberLength; + int pos = start; + while (pos < len) { + char ch = pattern.charAt(pos); + if (phase == 1) { + if (!positive) { + // Only the affixes of the negative subpattern count; + // its number is skipped, as long as the positive one. + if (toSkip > 0) { + toSkip--; + pos++; + continue; + } + phase = 2; + affix = suffix; + continue; + } + if (ch == '#') { + if (zeroDigitCount > 0) { + digitRightCount++; + } else { + digitLeftCount++; + } + if (groupingCount >= 0 && decimalPos < 0) { + groupingCount++; + } + } else if (ch == '0') { + if (digitRightCount > 0) { + throw malformed("Unexpected '0'", pattern); + } + zeroDigitCount++; + if (groupingCount >= 0 && decimalPos < 0) { + groupingCount++; + } + } else if (ch == ',') { + groupingCount = 0; + } else if (ch == '.') { + if (decimalPos >= 0) { + throw malformed("Multiple decimal separators", pattern); + } + decimalPos = digitLeftCount + zeroDigitCount + digitRightCount; + } else if (ch == 'E') { + if (exponential) { + throw malformed("Multiple exponential symbols", pattern); + } + exponential = true; + exponentDigits = 0; + while (pos + 1 < len && pattern.charAt(pos + 1) == '0') { + exponentDigits++; + numberLength++; + pos++; + } + if (digitLeftCount + zeroDigitCount < 1 || exponentDigits < 1) { + throw malformed("Malformed exponential pattern", pattern); + } + numberLength++; + pos++; + phase = 2; + affix = suffix; + continue; + } else { + phase = 2; + affix = suffix; + continue; + } + numberLength++; + pos++; + continue; + } + // A prefix or a suffix. + if (inQuote) { + if (ch == QUOTE) { + if (pos + 1 < len && pattern.charAt(pos + 1) == QUOTE) { + pos++; + affix.append("''"); + } else { + inQuote = false; + } + pos++; + continue; + } + } else if (isNumberChar(ch)) { + // In a suffix too: the JDK goes back to reading the + // number, so "0.0x0" is "0.00x", and this does the same + // rather than reject a pattern the desktop accepts. + if (phase == 2 && !positive) { + pos = len; + continue; + } + phase = 1; + continue; + } else if (ch == CURRENCY_SIGN) { + boolean doubled = pos + 1 < len && pattern.charAt(pos + 1) == CURRENCY_SIGN; + if (doubled) { + pos++; + } + currency = true; + affix.append(QUOTE).append(CURRENCY_SIGN); + if (doubled) { + affix.append(CURRENCY_SIGN); + } + pos++; + continue; + } else if (ch == QUOTE) { + if (pos + 1 < len && pattern.charAt(pos + 1) == QUOTE) { + pos++; + affix.append("''"); + } else { + inQuote = true; + } + pos++; + continue; + } else if (ch == ';') { + if (phase == 0 || !positive) { + throw malformed("Unquoted special character ';'", pattern); + } + sawSeparator = true; + pos++; + break; + } else if (ch == '%' || ch == PER_MILLE_SIGN) { + if (partMultiplier != 1) { + throw malformed("Too many percent/per mille characters", pattern); + } + partMultiplier = ch == '%' ? 100 : 1000; + if (positive) { + newMultiplier = partMultiplier; + } + affix.append(QUOTE).append(ch); + pos++; + continue; + } else if (ch == '-') { + affix.append(QUOTE).append('-'); + pos++; + continue; + } + affix.append(ch); + pos++; + } + if (inQuote) { + throw malformed("Unterminated quote", pattern); + } + if (positive) { + newPosPrefix = prefix.toString(); + newPosSuffix = suffix.toString(); + } else { + newNegPrefix = prefix.toString(); + newNegSuffix = suffix.toString(); + gotNegative = true; + } + // Without a separator the whole pattern is read again as the + // negative one. Its affixes then equal the positive ones, which + // is what selects the default minus sign below. + start = sawSeparator ? pos : 0; + } + + if (len == 0) { + useExponentialNotation = false; + minExponentDigits = 0; + currencyFormat = false; + decimalSeparatorAlwaysShown = false; + setMinimumIntegerDigits(0); + setMaximumIntegerDigits(Integer.MAX_VALUE); + setMinimumFractionDigits(0); + setMaximumFractionDigits(Integer.MAX_VALUE); + } else { + if (zeroDigitCount == 0 && digitLeftCount > 0 && decimalPos >= 0) { + // "#.##", "#." and ".#" have one digit that is always + // written; it is the one next to the decimal point. + int n = decimalPos; + if (n == 0) { + n++; + } + digitRightCount = digitLeftCount - n; + digitLeftCount = n - 1; + zeroDigitCount = 1; + } + if ((decimalPos < 0 && digitRightCount > 0) + || (decimalPos >= 0 + && (decimalPos < digitLeftCount || decimalPos > digitLeftCount + zeroDigitCount)) + || groupingCount == 0) { + throw malformed("Malformed pattern", pattern); + } + int digitTotalCount = digitLeftCount + zeroDigitCount + digitRightCount; + if (decimalPos > digitTotalCount) { + decimalPos = digitTotalCount; + } + int effectiveDecimalPos = decimalPos >= 0 ? decimalPos : digitTotalCount; + useExponentialNotation = exponential; + minExponentDigits = exponentDigits; + // Reset first, so that neither setter is clamped by a previous + // pattern's limits. + setMinimumIntegerDigits(0); + setMinimumFractionDigits(0); + int minInt = effectiveDecimalPos - digitLeftCount; + setMaximumIntegerDigits(exponential ? digitLeftCount + minInt : Integer.MAX_VALUE); + setMinimumIntegerDigits(minInt); + setMaximumFractionDigits(decimalPos >= 0 ? digitTotalCount - decimalPos : 0); + setMinimumFractionDigits(decimalPos >= 0 ? digitLeftCount + zeroDigitCount - decimalPos : 0); + setGroupingUsed(groupingCount > 0); + groupingSize = groupingCount > 0 ? groupingCount : 0; + multiplier = newMultiplier; + currencyFormat = currency; + decimalSeparatorAlwaysShown = decimalPos == 0 || decimalPos == digitTotalCount; + } + posPrefixPattern = newPosPrefix; + posSuffixPattern = newPosSuffix; + // No negative subpattern, or one that says nothing new, means a + // minus sign in front of the positive one. + if (!gotNegative || (newNegPrefix.equals(newPosPrefix) && newNegSuffix.equals(newPosSuffix))) { + negPrefixPattern = "'-" + newPosPrefix; + negSuffixPattern = newPosSuffix; + } else { + negPrefixPattern = newNegPrefix; + negSuffixPattern = newNegSuffix; + } + expandAffixes(); + } + + private void expandAffixes() { + positivePrefix = expandAffix(posPrefixPattern); + positiveSuffix = expandAffix(posSuffixPattern); + negativePrefix = expandAffix(negPrefixPattern); + negativeSuffix = expandAffix(negSuffixPattern); + } + + private String expandAffix(String internal) { + if (internal.indexOf(QUOTE) < 0) { + return internal; + } + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < internal.length()) { + char c = internal.charAt(i++); + if (c != QUOTE || i >= internal.length()) { + sb.append(c); + continue; + } + c = internal.charAt(i++); + if (c == CURRENCY_SIGN) { + if (i < internal.length() && internal.charAt(i) == CURRENCY_SIGN) { + i++; + sb.append(symbols.getInternationalCurrencySymbol()); + } else { + sb.append(symbols.getCurrencySymbol()); + } + } else if (c == '%') { + sb.append(symbols.getPercent()); + } else if (c == PER_MILLE_SIGN) { + sb.append(symbols.getPerMill()); + } else if (c == '-') { + sb.append(symbols.getMinusSign()); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + private static String literalAffix(String literal) { + if (literal.indexOf(QUOTE) < 0) { + return literal; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < literal.length(); i++) { + char c = literal.charAt(i); + sb.append(c); + if (c == QUOTE) { + sb.append(QUOTE); + } + } + return sb.toString(); + } + + private static boolean needsQuoting(char c) { + return isNumberChar(c) || c == ';' || c == '%' || c == PER_MILLE_SIGN || c == CURRENCY_SIGN + || c == '-'; + } + + private static void appendAffixPattern(StringBuilder out, String internal) { + int i = 0; + while (i < internal.length()) { + char c = internal.charAt(i++); + if (c == QUOTE && i < internal.length()) { + c = internal.charAt(i++); + if (c == QUOTE) { + out.append("''"); + } else { + out.append(c); + if (c == CURRENCY_SIGN && i < internal.length() && internal.charAt(i) == CURRENCY_SIGN) { + out.append(CURRENCY_SIGN); + i++; + } + } + } else if (needsQuoting(c)) { + out.append(QUOTE).append(c).append(QUOTE); + } else { + out.append(c); + } + } + } + + private void appendNumberPattern(StringBuilder out) { + int minInt = getMinimumIntegerDigits(); + // Capped: an empty pattern allows any number of fraction digits, and + // spelling that out exhausts memory, as it does in the JDK. + int maxFrac = Math.min(getMaximumFractionDigits(), DOUBLE_FRACTION_DIGITS); + int minFrac = Math.min(getMinimumFractionDigits(), DOUBLE_FRACTION_DIGITS); + boolean grouping = isGroupingUsed() && groupingSize > 0; + int digitCount = useExponentialNotation + ? getMaximumIntegerDigits() + : Math.max(groupingSize, minInt) + 1; + for (int i = digitCount; i > 0; i--) { + // groupingSize is checked positive just above. + if (i != digitCount && grouping && i % groupingSize == 0) { + out.append(','); + } + out.append(i <= minInt ? '0' : '#'); + } + if (maxFrac > 0 || decimalSeparatorAlwaysShown) { + out.append('.'); + } + for (int i = 0; i < maxFrac; i++) { + out.append(i < minFrac ? '0' : '#'); + } + if (useExponentialNotation) { + out.append('E'); + for (int i = 0; i < minExponentDigits; i++) { + out.append('0'); + } + } + } + + /// A pattern that produces this format. It is equivalent to the pattern + /// that was applied, not necessarily the same text: `0.00` comes back as + /// `#0.00`. + public String toPattern() { + StringBuilder out = new StringBuilder(); + appendAffixPattern(out, posPrefixPattern); + appendNumberPattern(out); + appendAffixPattern(out, posSuffixPattern); + boolean defaultNegative = negSuffixPattern.equals(posSuffixPattern) + && negPrefixPattern.equals("'-" + posPrefixPattern); + if (!defaultNegative) { + out.append(';'); + appendAffixPattern(out, negPrefixPattern); + appendNumberPattern(out); + appendAffixPattern(out, negSuffixPattern); + } + return out.toString(); + } + + // ------------------------------------------------------------------ + // Properties + // ------------------------------------------------------------------ + + /// A copy of the symbols in use; changing it changes nothing until it + /// is passed to [#setDecimalFormatSymbols(DecimalFormatSymbols)]. + public DecimalFormatSymbols getDecimalFormatSymbols() { + Object copy = symbols.clone(); + return copy instanceof DecimalFormatSymbols ? (DecimalFormatSymbols) copy : symbols; + } + + public void setDecimalFormatSymbols(DecimalFormatSymbols newSymbols) { + Object copy = newSymbols.clone(); + if (copy instanceof DecimalFormatSymbols) { + symbols = (DecimalFormatSymbols) copy; + expandAffixes(); + } + } + + public String getPositivePrefix() { + return positivePrefix; + } + + public void setPositivePrefix(String newValue) { + positivePrefix = newValue; + posPrefixPattern = literalAffix(newValue); + } + + public String getNegativePrefix() { + return negativePrefix; + } + + public void setNegativePrefix(String newValue) { + negativePrefix = newValue; + negPrefixPattern = literalAffix(newValue); + } + + public String getPositiveSuffix() { + return positiveSuffix; + } + + public void setPositiveSuffix(String newValue) { + positiveSuffix = newValue; + posSuffixPattern = literalAffix(newValue); + } + + public String getNegativeSuffix() { + return negativeSuffix; + } + + public void setNegativeSuffix(String newValue) { + negativeSuffix = newValue; + negSuffixPattern = literalAffix(newValue); + } + + public int getMultiplier() { + return multiplier; + } + + public void setMultiplier(int newValue) { + multiplier = newValue; + } + + public int getGroupingSize() { + return groupingSize; + } + + public void setGroupingSize(int newValue) { + groupingSize = Math.max(0, newValue); + } + + public boolean isDecimalSeparatorAlwaysShown() { + return decimalSeparatorAlwaysShown; + } + + public void setDecimalSeparatorAlwaysShown(boolean newValue) { + decimalSeparatorAlwaysShown = newValue; + } + + /// A copy of this format. Written out field by field: `Object.clone()` + /// answers `null` on the device for anything but an array. + @Override + public Object clone() { + DecimalFormat copy = new DecimalFormat("", symbols); + copyNumberFormatTo(copy); + copy.posPrefixPattern = posPrefixPattern; + copy.posSuffixPattern = posSuffixPattern; + copy.negPrefixPattern = negPrefixPattern; + copy.negSuffixPattern = negSuffixPattern; + copy.positivePrefix = positivePrefix; + copy.positiveSuffix = positiveSuffix; + copy.negativePrefix = negativePrefix; + copy.negativeSuffix = negativeSuffix; + copy.multiplier = multiplier; + copy.groupingSize = groupingSize; + copy.decimalSeparatorAlwaysShown = decimalSeparatorAlwaysShown; + copy.useExponentialNotation = useExponentialNotation; + copy.minExponentDigits = minExponentDigits; + copy.currencyFormat = currencyFormat; + return copy; + } + + @Override + public boolean equals(Object obj) { + if (!super.equals(obj) || !(obj instanceof DecimalFormat)) { + return false; + } + DecimalFormat other = (DecimalFormat) obj; + return positivePrefix.equals(other.positivePrefix) + && positiveSuffix.equals(other.positiveSuffix) + && negativePrefix.equals(other.negativePrefix) + && negativeSuffix.equals(other.negativeSuffix) + && multiplier == other.multiplier + && groupingSize == other.groupingSize + && decimalSeparatorAlwaysShown == other.decimalSeparatorAlwaysShown + && useExponentialNotation == other.useExponentialNotation + && minExponentDigits == other.minExponentDigits + && symbols.equals(other.symbols); + } + + @Override + public int hashCode() { + return super.hashCode() * 37 + positivePrefix.hashCode(); + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/DecimalFormatSymbols.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/DecimalFormatSymbols.java new file mode 100644 index 00000000000..3a101bbb4f3 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/DecimalFormatSymbols.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import com.codename1.l10n.L10NManager; +import com.codename1.ui.Display; + +import java.util.Locale; + +/// `java.text.DecimalFormatSymbols` for the Codename One runtime: the +/// characters a `DecimalFormat` writes and reads -- decimal and grouping +/// separators, minus sign, percent, currency symbol and so on. +/// +/// #### Where the symbols come from +/// +/// The device library carries no locale data of its own; what it knows about +/// the user's locale it knows through `L10NManager`, which formats numbers +/// natively. So the symbols of the device's locale are read off numbers +/// `L10NManager` has formatted, together with its currency symbol, and +/// everything it cannot tell (percent, per mille, the exponent separator, the +/// infinity and not-a-number texts) is as in the US English locale. +/// +/// A request that names `en_US` gets the US English symbols outright. Any +/// other locale gets the device's symbols, since the device cannot format for +/// a locale other than its own. Before Codename One is initialized, and where +/// `L10NManager` answers nothing usable, the symbols are US English. +public class DecimalFormatSymbols implements Cloneable, java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private char zeroDigit = '0'; + private char groupingSeparator = ','; + private char decimalSeparator = '.'; + private char perMill = (char) 0x2030; + private char percent = '%'; + private char digit = '#'; + private char patternSeparator = ';'; + private String infinity = String.valueOf((char) 0x221E); + private String nan = String.valueOf((char) 0xFFFD); + private char minusSign = '-'; + private String currencySymbol = "$"; + private String intlCurrencySymbol = "USD"; + private char monetarySeparator = '.'; + private String exponentSeparator = "E"; + + public DecimalFormatSymbols() { + this(Locale.getDefault()); + } + + public DecimalFormatSymbols(Locale locale) { + if (locale == null) { + throw new NullPointerException(); + } + if (!isUsEnglish(locale)) { + readDeviceSymbols(); + } + } + + public static final DecimalFormatSymbols getInstance() { + return new DecimalFormatSymbols(); + } + + public static final DecimalFormatSymbols getInstance(Locale locale) { + return new DecimalFormatSymbols(locale); + } + + private static boolean isUsEnglish(Locale locale) { + return "en".equals(locale.getLanguage()) && "US".equals(locale.getCountry()); + } + + private static boolean isAsciiDigit(char c) { + return c >= '0' && c <= '9'; + } + + /// The first character of `text` after its leading digits, or 0 when it + /// is digits only. + private static char firstSeparator(String text) { + if (text == null) { + return 0; + } + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (!isAsciiDigit(c)) { + // A separator sits between digits; anything else is not one. + return i > 0 && i + 1 < text.length() && isAsciiDigit(text.charAt(i + 1)) ? c : 0; + } + } + return 0; + } + + private void readDeviceSymbols() { + if (!Display.isInitialized()) { + return; + } + try { + L10NManager l10n = L10NManager.getInstance(); + char decimal = firstSeparator(l10n.format(1.5, 1)); + if (decimal != 0) { + decimalSeparator = decimal; + monetarySeparator = decimal; + } + char grouping = firstSeparator(l10n.format(1234567.0, 0)); + if (grouping != 0 && grouping != decimalSeparator) { + groupingSeparator = grouping; + } else if (decimalSeparator == ',') { + // A locale that writes a decimal comma and no grouping on this + // platform still must not group with the same character. + groupingSeparator = '.'; + } + String currency = l10n.getCurrencySymbol(); + if (currency != null && currency.length() > 0) { + currencySymbol = currency; + intlCurrencySymbol = currency; + } + } catch (RuntimeException e) { + // A port whose L10NManager cannot answer leaves the US English + // symbols in place, which is the documented fallback. + decimalSeparator = '.'; + monetarySeparator = '.'; + groupingSeparator = ','; + } + } + + public char getZeroDigit() { + return zeroDigit; + } + + public void setZeroDigit(char zeroDigit) { + this.zeroDigit = zeroDigit; + } + + public char getGroupingSeparator() { + return groupingSeparator; + } + + public void setGroupingSeparator(char groupingSeparator) { + this.groupingSeparator = groupingSeparator; + } + + public char getDecimalSeparator() { + return decimalSeparator; + } + + public void setDecimalSeparator(char decimalSeparator) { + this.decimalSeparator = decimalSeparator; + } + + public char getPerMill() { + return perMill; + } + + public void setPerMill(char perMill) { + this.perMill = perMill; + } + + public char getPercent() { + return percent; + } + + public void setPercent(char percent) { + this.percent = percent; + } + + public char getDigit() { + return digit; + } + + public void setDigit(char digit) { + this.digit = digit; + } + + public char getPatternSeparator() { + return patternSeparator; + } + + public void setPatternSeparator(char patternSeparator) { + this.patternSeparator = patternSeparator; + } + + public String getInfinity() { + return infinity; + } + + public void setInfinity(String infinity) { + this.infinity = infinity; + } + + public String getNaN() { + return nan; + } + + public void setNaN(String nan) { + this.nan = nan; + } + + public char getMinusSign() { + return minusSign; + } + + public void setMinusSign(char minusSign) { + this.minusSign = minusSign; + } + + public String getCurrencySymbol() { + return currencySymbol; + } + + public void setCurrencySymbol(String currency) { + currencySymbol = currency; + } + + public String getInternationalCurrencySymbol() { + return intlCurrencySymbol; + } + + public void setInternationalCurrencySymbol(String currencyCode) { + intlCurrencySymbol = currencyCode; + } + + public char getMonetaryDecimalSeparator() { + return monetarySeparator; + } + + public void setMonetaryDecimalSeparator(char sep) { + monetarySeparator = sep; + } + + public String getExponentSeparator() { + return exponentSeparator; + } + + public void setExponentSeparator(String exp) { + if (exp == null) { + throw new NullPointerException(); + } + exponentSeparator = exp; + } + + /// A copy of these symbols. Written out field by field: `Object.clone()` + /// answers `null` on the device for anything but an array. + @Override + public Object clone() { + DecimalFormatSymbols copy = new DecimalFormatSymbols(new Locale("en", "US")); + copy.zeroDigit = zeroDigit; + copy.groupingSeparator = groupingSeparator; + copy.decimalSeparator = decimalSeparator; + copy.perMill = perMill; + copy.percent = percent; + copy.digit = digit; + copy.patternSeparator = patternSeparator; + copy.infinity = infinity; + copy.nan = nan; + copy.minusSign = minusSign; + copy.currencySymbol = currencySymbol; + copy.intlCurrencySymbol = intlCurrencySymbol; + copy.monetarySeparator = monetarySeparator; + copy.exponentSeparator = exponentSeparator; + return copy; + } + + private static boolean same(String a, String b) { + return a == null ? b == null : a.equals(b); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DecimalFormatSymbols)) { + return false; + } + DecimalFormatSymbols other = (DecimalFormatSymbols) obj; + return zeroDigit == other.zeroDigit + && groupingSeparator == other.groupingSeparator + && decimalSeparator == other.decimalSeparator + && percent == other.percent + && perMill == other.perMill + && digit == other.digit + && minusSign == other.minusSign + && patternSeparator == other.patternSeparator + && monetarySeparator == other.monetarySeparator + && same(infinity, other.infinity) + && same(nan, other.nan) + && same(currencySymbol, other.currencySymbol) + && same(intlCurrencySymbol, other.intlCurrencySymbol) + && same(exponentSeparator, other.exponentSeparator); + } + + @Override + public int hashCode() { + int result = zeroDigit; + result = result * 37 + groupingSeparator; + result = result * 37 + decimalSeparator; + return result; + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/EventObject.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/EventObject.java new file mode 100644 index 00000000000..05a5fd66245 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/EventObject.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.util.EventObject` for the Codename One runtime: the root of every +/// listener event, carrying the object the event happened on. +public class EventObject implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + /// The object the event initially occurred on. + protected transient Object source; + + public EventObject(Object source) { + if (source == null) { + throw new IllegalArgumentException("null source"); + } + this.source = source; + } + + public Object getSource() { + return source; + } + + @Override + public String toString() { + return getClass().getName() + "[source=" + source + "]"; + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ExecutionException.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ExecutionException.java new file mode 100644 index 00000000000..da72d1841f3 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ExecutionException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.util.concurrent.ExecutionException` for the Codename One runtime: +/// thrown when retrieving the result of a task that failed, with the failure +/// as its cause. +public class ExecutionException extends Exception { + + private static final long serialVersionUID = 1L; + + protected ExecutionException() { + super(); + } + + protected ExecutionException(String message) { + super(message); + } + + public ExecutionException(String message, Throwable cause) { + super(message, cause); + } + + public ExecutionException(Throwable cause) { + super(cause == null ? null : cause.toString(), cause); + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FieldPosition.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FieldPosition.java new file mode 100644 index 00000000000..81cf51c9937 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/FieldPosition.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.text.FieldPosition` for the Codename One runtime: names a field of +/// formatted output and receives the range of characters that field took. +/// `NumberFormat.INTEGER_FIELD` and `NumberFormat.FRACTION_FIELD` are the +/// fields the number formats here report. +public class FieldPosition { + + private final int field; + private int beginIndex; + private int endIndex; + + public FieldPosition(int field) { + this.field = field; + } + + public int getField() { + return field; + } + + public int getBeginIndex() { + return beginIndex; + } + + public int getEndIndex() { + return endIndex; + } + + public void setBeginIndex(int bi) { + beginIndex = bi; + } + + public void setEndIndex(int ei) { + endIndex = ei; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FieldPosition)) { + return false; + } + FieldPosition other = (FieldPosition) obj; + return field == other.field && beginIndex == other.beginIndex && endIndex == other.endIndex; + } + + @Override + public int hashCode() { + return (field << 24) | (beginIndex << 16) | endIndex; + } + + @Override + public String toString() { + return getClass().getName() + "[field=" + field + ",attribute=null,beginIndex=" + beginIndex + + ",endIndex=" + endIndex + ']'; + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Future.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Future.java new file mode 100644 index 00000000000..b0200cabef8 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Future.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.util.concurrent.Future` for the Codename One runtime: the result of +/// a computation that may not have finished yet. +public interface Future { + + boolean cancel(boolean mayInterruptIfRunning); + + boolean isCancelled(); + + boolean isDone(); + + V get() throws InterruptedException, ExecutionException; + + V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ListResourceBundle.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ListResourceBundle.java new file mode 100644 index 00000000000..e36cb24bba6 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ListResourceBundle.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/// `java.util.ListResourceBundle` for the Codename One runtime: a resource +/// bundle whose content a subclass supplies as an array of key and value +/// pairs. +public abstract class ListResourceBundle extends ResourceBundle { + + private Map lookup; + + public ListResourceBundle() { + super(); + } + + /// The bundle's content: each element is a pair, the key (a `String`) + /// and its value. + protected abstract Object[][] getContents(); + + private Map lookup() { + if (lookup == null) { + Map built = new HashMap(); + Object[][] contents = getContents(); + for (int i = 0; i < contents.length; i++) { + Object key = contents[i][0]; + Object value = contents[i][1]; + if (!(key instanceof String) || value == null) { + throw new NullPointerException(); + } + built.put((String) key, value); + } + lookup = built; + } + return lookup; + } + + @Override + public final Object handleGetObject(String key) { + if (key == null) { + throw new NullPointerException(); + } + return lookup().get(key); + } + + @Override + public Enumeration getKeys() { + return new ResourceBundle.KeyEnumeration(keySet().iterator()); + } + + @Override + protected Set handleKeySet() { + return lookup().keySet(); + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MalformedURLException.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MalformedURLException.java new file mode 100644 index 00000000000..89905ac5935 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MalformedURLException.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.io.IOException; + +/// `java.net.MalformedURLException` for the Codename One runtime: a string +/// could not be parsed as a URL, or names a protocol nothing handles. +public class MalformedURLException extends IOException { + + private static final long serialVersionUID = 1L; + + public MalformedURLException() { + super(); + } + + public MalformedURLException(String msg) { + super(msg); + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MessageFormat.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MessageFormat.java new file mode 100644 index 00000000000..89685e7c8f7 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MessageFormat.java @@ -0,0 +1,455 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/// `java.text.MessageFormat` for the Codename One runtime: builds a message +/// from a pattern and a list of arguments. +/// +/// #### Pattern syntax +/// +/// Text is copied as it is, and each `{...}` element is replaced by an +/// argument: +/// +/// - `{0}`: the argument at that index. A `Number` is formatted with +/// `NumberFormat.getInstance`, a `Date` with the short date and time +/// format, a `String` is copied, anything else contributes `toString()`. +/// - `{0,number}`, `{0,number,integer}`, `{0,number,percent}`, +/// `{0,number,currency}`, or `{0,number,#,##0.00}` with a [DecimalFormat] +/// pattern. +/// - `{0,date}` and `{0,time}`, optionally followed by `short`, `medium`, +/// `long` or `full`, or by a `SimpleDateFormat` pattern: +/// `{0,date,yyyy-MM-dd}`. The device's own `java.text.DateFormat` does the +/// formatting, so the named styles produce what the device produces. +/// - `{0,choice,0#none|1#one|1<{0} items}` with a [ChoiceFormat] pattern. +/// If the chosen text has elements of its own, they are formatted against +/// the same arguments. +/// +/// A single quote makes the characters up to the next one literal, which is +/// how a `{` is written (`'{'`), and two quotes in a row stand for one. An +/// apostrophe in a message must therefore be doubled: `it''s`. +/// +/// An argument that is `null` is written as `null`, and an element whose +/// index is past the end of the arguments is written back as it was, `{2}`. +/// +/// #### Differences from the JDK +/// +/// - No `parse`, and no `getFormats`/`setFormat` family: those hand out and +/// accept `java.text.Format` objects, which the number formats here are not +/// (see [NumberFormat]). +/// - `toPattern` returns the elements as they were written rather than a +/// canonical rewrite of them. +/// - Date and time elements ignore the locale passed to the constructor: the +/// device formats dates for its own locale only. +public class MessageFormat implements Cloneable { + + private static final int TYPE_NONE = 0; + private static final int TYPE_NUMBER = 1; + private static final int TYPE_DATE = 2; + private static final int TYPE_TIME = 3; + private static final int TYPE_CHOICE = 4; + + private Locale locale; + + /// The literal text before each element, and after the last one: always + /// one longer than the element arrays. + private String[] literals = new String[] {""}; + private int[] argumentNumbers = new int[0]; + private Object[] formats = new Object[0]; + private String[] elementTexts = new String[0]; + + public MessageFormat(String pattern) { + this(pattern, Locale.getDefault()); + } + + public MessageFormat(String pattern, Locale locale) { + this.locale = locale; + parsePattern(pattern); + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public Locale getLocale() { + return locale; + } + + public void applyPattern(String pattern) { + parsePattern(pattern); + } + + private void parsePattern(String pattern) { + List newLiterals = new ArrayList(); + List newNumbers = new ArrayList(); + List newFormats = new ArrayList(); + List newTexts = new ArrayList(); + StringBuilder raw = new StringBuilder(); + // The parts of the element being read: index, type, style. + StringBuilder[] parts = new StringBuilder[3]; + int part = -1; + boolean inQuote = false; + int braceDepth = 0; + for (int i = 0; i < pattern.length(); i++) { + char ch = pattern.charAt(i); + if (part < 0) { + if (ch == '\'') { + if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'') { + raw.append(ch); + i++; + } else { + inQuote = !inQuote; + } + } else if (ch == '{' && !inQuote) { + part = 0; + parts[0] = new StringBuilder(); + parts[1] = null; + parts[2] = null; + } else { + raw.append(ch); + } + continue; + } + if (inQuote) { + // Inside an element the quotes are the sub-pattern's own. + parts[part].append(ch); + if (ch == '\'') { + inQuote = false; + } + continue; + } + if (ch == ',') { + if (part < 2) { + part++; + parts[part] = new StringBuilder(); + } else { + parts[part].append(ch); + } + } else if (ch == '{') { + braceDepth++; + parts[part].append(ch); + } else if (ch == '}') { + if (braceDepth == 0) { + newLiterals.add(raw.toString()); + raw.setLength(0); + String type = parts[1] == null ? "" : parts[1].toString(); + String style = parts[2] == null ? "" : parts[2].toString(); + int number = parseArgumentNumber(parts[0].toString()); + newNumbers.add(Integer.valueOf(number)); + Object made = makeFormat(type, style); + newFormats.add(made); + newTexts.add(number + describe(typeOf(type), style, made)); + part = -1; + } else { + braceDepth--; + parts[part].append(ch); + } + } else if (ch == ' ') { + // Leading spaces of the type are not part of it. + if (part != 1 || parts[1].length() > 0) { + parts[part].append(ch); + } + } else { + if (ch == '\'') { + inQuote = true; + } + parts[part].append(ch); + } + } + if (braceDepth == 0 && part >= 0) { + throw new IllegalArgumentException("Unmatched braces in the pattern."); + } + newLiterals.add(raw.toString()); + int n = newNumbers.size(); + literals = newLiterals.toArray(new String[newLiterals.size()]); + argumentNumbers = new int[n]; + for (int i = 0; i < n; i++) { + argumentNumbers[i] = newNumbers.get(i).intValue(); + } + formats = newFormats.toArray(new Object[n]); + elementTexts = newTexts.toArray(new String[n]); + } + + private static int parseArgumentNumber(String text) { + boolean negative = text.startsWith("-"); + int from = negative ? 1 : 0; + if (text.length() == from || text.length() - from > 9) { + throw new IllegalArgumentException("can't parse argument number: " + text); + } + int value = 0; + for (int i = from; i < text.length(); i++) { + char c = text.charAt(i); + if (c < '0' || c > '9') { + throw new IllegalArgumentException("can't parse argument number: " + text); + } + value = value * 10 + (c - '0'); + } + if (negative) { + throw new IllegalArgumentException("negative argument number: " + text); + } + return value; + } + + private static int typeOf(String type) { + String t = type.trim(); + if (t.length() == 0) { + return TYPE_NONE; + } + if (t.equalsIgnoreCase("number")) { + return TYPE_NUMBER; + } + if (t.equalsIgnoreCase("date")) { + return TYPE_DATE; + } + if (t.equalsIgnoreCase("time")) { + return TYPE_TIME; + } + if (t.equalsIgnoreCase("choice")) { + return TYPE_CHOICE; + } + throw new IllegalArgumentException("unknown format type: " + type); + } + + /// The date style a keyword names, or -1 when the text is a pattern. + private static int dateStyle(String style) { + String s = style.trim(); + if (s.length() == 0) { + return DateFormat.DEFAULT; + } + if (s.equalsIgnoreCase("short")) { + return DateFormat.SHORT; + } + if (s.equalsIgnoreCase("medium")) { + return DateFormat.DEFAULT; + } + if (s.equalsIgnoreCase("long")) { + return DateFormat.LONG; + } + if (s.equalsIgnoreCase("full")) { + return DateFormat.FULL; + } + return -1; + } + + private Object makeFormat(String type, String style) { + int kind = typeOf(type); + String keyword = style.trim(); + if (kind == TYPE_NUMBER) { + if (keyword.length() == 0) { + return NumberFormat.getInstance(locale); + } + if (keyword.equalsIgnoreCase("currency")) { + return NumberFormat.getCurrencyInstance(locale); + } + if (keyword.equalsIgnoreCase("percent")) { + return NumberFormat.getPercentInstance(locale); + } + if (keyword.equalsIgnoreCase("integer")) { + return NumberFormat.getIntegerInstance(locale); + } + return new DecimalFormat(style, DecimalFormatSymbols.getInstance(locale)); + } + if (kind == TYPE_DATE) { + int dateStyle = dateStyle(style); + return dateStyle >= 0 ? DateFormat.getDateInstance(dateStyle) : new SimpleDateFormat(style); + } + if (kind == TYPE_TIME) { + int timeStyle = dateStyle(style); + return timeStyle >= 0 ? DateFormat.getTimeInstance(timeStyle) : new SimpleDateFormat(style); + } + if (kind == TYPE_CHOICE) { + try { + return new ChoiceFormat(style); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Choice Pattern incorrect: " + style); + } + } + return null; + } + + /// The type and style of an element as `toPattern` writes them: not the + /// text that was read but, as in the JDK, a description of the format it + /// produced -- `{0, number, #.##}` comes back as `{0,number,#0.##}`. + private String describe(int kind, String style, Object made) { + if (kind == TYPE_NUMBER) { + if (made.equals(NumberFormat.getInstance(locale))) { + return ",number"; + } + if (made.equals(NumberFormat.getCurrencyInstance(locale))) { + return ",number,currency"; + } + if (made.equals(NumberFormat.getPercentInstance(locale))) { + return ",number,percent"; + } + if (made.equals(NumberFormat.getIntegerInstance(locale))) { + return ",number,integer"; + } + return made instanceof DecimalFormat ? ",number," + ((DecimalFormat) made).toPattern() : ",number"; + } + if (kind == TYPE_CHOICE) { + return made instanceof ChoiceFormat ? ",choice," + ((ChoiceFormat) made).toPattern() : ",choice"; + } + if (kind == TYPE_DATE || kind == TYPE_TIME) { + String name = kind == TYPE_DATE ? ",date" : ",time"; + int dateStyle = dateStyle(style); + if (dateStyle == DateFormat.SHORT) { + return name + ",short"; + } + if (dateStyle == DateFormat.LONG) { + return name + ",long"; + } + if (dateStyle == DateFormat.FULL) { + return name + ",full"; + } + // A pattern of either type is a date pattern to the JDK. + return dateStyle >= 0 ? name : ",date," + style; + } + return ""; + } + + private static void appendLiteral(StringBuilder out, String text) { + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '\'') { + out.append("''"); + } else if (c == '{') { + out.append("'{'"); + } else { + out.append(c); + } + } + } + + public String toPattern() { + StringBuilder out = new StringBuilder(); + for (int i = 0; i < argumentNumbers.length; i++) { + appendLiteral(out, literals[i]); + out.append('{').append(elementTexts[i]).append('}'); + } + appendLiteral(out, literals[argumentNumbers.length]); + return out.toString(); + } + + public static String format(String pattern, Object... arguments) { + return new MessageFormat(pattern).format(arguments); + } + + /// Formats an array of arguments. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: if `arguments` is not an `Object[]`, or + /// an argument is not of the type its element formats + public final String format(Object arguments) { + return format(arguments, new StringBuffer(), new FieldPosition(0)).toString(); + } + + public final StringBuffer format(Object arguments, StringBuffer result, FieldPosition pos) { + if (arguments == null || arguments instanceof Object[]) { + return format((Object[]) arguments, result, pos); + } + throw new IllegalArgumentException("The arguments of a message are an Object[]"); + } + + public final StringBuffer format(Object[] arguments, StringBuffer result, FieldPosition pos) { + for (int i = 0; i < argumentNumbers.length; i++) { + result.append(literals[i]); + int number = argumentNumbers[i]; + if (arguments == null || number >= arguments.length) { + result.append('{').append(number).append('}'); + continue; + } + Object argument = arguments[number]; + Object format = formats[i]; + if (argument == null) { + result.append("null"); + } else if (format instanceof ChoiceFormat) { + String chosen = ((ChoiceFormat) format).format(argument); + if (chosen.indexOf('{') >= 0) { + new MessageFormat(chosen, locale).format(arguments, result, pos); + } else { + result.append(chosen); + } + } else if (format instanceof NumberFormat) { + result.append(((NumberFormat) format).format(argument)); + } else if (format instanceof DateFormat) { + result.append(formatDate((DateFormat) format, argument)); + } else if (argument instanceof Number) { + result.append(NumberFormat.getInstance(locale).format(argument)); + } else if (argument instanceof Date) { + DateFormat dateTime = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); + result.append(dateTime.format((Date) argument)); + } else if (argument instanceof String) { + result.append((String) argument); + } else { + result.append(String.valueOf(argument.toString())); + } + } + result.append(literals[argumentNumbers.length]); + return result; + } + + private static String formatDate(DateFormat format, Object argument) { + if (argument instanceof Date) { + return format.format((Date) argument); + } + if (argument instanceof Number) { + return format.format(new Date(((Number) argument).longValue())); + } + throw new IllegalArgumentException("Cannot format given Object as a Date"); + } + + /// A copy of this format, built by applying its pattern again: + /// `Object.clone()` answers `null` on the device for anything but an + /// array. + @Override + public Object clone() { + return new MessageFormat(toPattern(), locale); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass() || !(obj instanceof MessageFormat)) { + return false; + } + MessageFormat other = (MessageFormat) obj; + if (locale == null ? other.locale != null : !locale.equals(other.locale)) { + return false; + } + return toPattern().equals(other.toPattern()); + } + + @Override + public int hashCode() { + return toPattern().hashCode(); + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MissingResourceException.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MissingResourceException.java new file mode 100644 index 00000000000..a28929a11a7 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/MissingResourceException.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.util.MissingResourceException` for the Codename One runtime: a +/// resource bundle, or a key in one, could not be found. +public class MissingResourceException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final String className; + private final String key; + + public MissingResourceException(String s, String className, String key) { + super(s); + this.className = className; + this.key = key; + } + + public String getClassName() { + return className; + } + + public String getKey() { + return key; + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/NumberFormat.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/NumberFormat.java new file mode 100644 index 00000000000..108aafa7b10 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/NumberFormat.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.text.ParseException; +import java.util.Locale; + +/// `java.text.NumberFormat` for the Codename One runtime: the base of the +/// number formats, and the factory for the usual ones. +/// +/// The factory methods answer a [DecimalFormat] with these patterns: +/// +/// - `getInstance` / `getNumberInstance`: `#,##0.###` +/// - `getIntegerInstance`: `#,##0`, rounding to a whole number and parsing +/// only the integer part +/// - `getPercentInstance`: `#,##0%` +/// - `getCurrencyInstance`: the currency symbol followed by `#,##0.00`, and a +/// minus sign before the symbol for a negative amount +/// +/// The separators and the currency symbol are those of +/// [DecimalFormatSymbols]; read there how a locale is honoured. +/// +/// #### Differences from the JDK +/// +/// - It does not extend `java.text.Format`. The device's `Format` declares +/// `format(Object)` abstract where the JDK declares it final, so no class +/// can extend both. Everything `Format` offers is declared here directly -- +/// `format(Object)`, `parseObject(String)` -- but a number format cannot be +/// assigned to a variable of type `Format`. +/// - Rounding is always half-even, the JDK's default. There is no +/// `setRoundingMode`, as the device has no `java.math.RoundingMode`. +/// - There is no `getCurrency`/`setCurrency`, as the device has no +/// `java.util.Currency`. +public abstract class NumberFormat implements Cloneable, java.io.Serializable { + + private static final long serialVersionUID = 1L; + + /// Names the integer part of a formatted number to a [FieldPosition]. + public static final int INTEGER_FIELD = 0; + + /// Names the fraction part of a formatted number to a [FieldPosition]. + public static final int FRACTION_FIELD = 1; + + private boolean groupingUsed = true; + private int maximumIntegerDigits = 40; + private int minimumIntegerDigits = 1; + private int maximumFractionDigits = 3; + private int minimumFractionDigits; + private boolean parseIntegerOnly; + + protected NumberFormat() { + } + + public static final NumberFormat getInstance() { + return getNumberInstance(Locale.getDefault()); + } + + public static NumberFormat getInstance(Locale inLocale) { + return getNumberInstance(inLocale); + } + + public static final NumberFormat getNumberInstance() { + return getNumberInstance(Locale.getDefault()); + } + + public static NumberFormat getNumberInstance(Locale inLocale) { + return new DecimalFormat("#,##0.###", DecimalFormatSymbols.getInstance(inLocale)); + } + + public static final NumberFormat getIntegerInstance() { + return getIntegerInstance(Locale.getDefault()); + } + + public static NumberFormat getIntegerInstance(Locale inLocale) { + DecimalFormat format = new DecimalFormat("#,##0", DecimalFormatSymbols.getInstance(inLocale)); + format.setParseIntegerOnly(true); + return format; + } + + public static final NumberFormat getPercentInstance() { + return getPercentInstance(Locale.getDefault()); + } + + public static NumberFormat getPercentInstance(Locale inLocale) { + return new DecimalFormat("#,##0%", DecimalFormatSymbols.getInstance(inLocale)); + } + + public static final NumberFormat getCurrencyInstance() { + return getCurrencyInstance(Locale.getDefault()); + } + + public static NumberFormat getCurrencyInstance(Locale inLocale) { + String pattern = DecimalFormat.CURRENCY_SIGN + "#,##0.00"; + return new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(inLocale)); + } + + /// Formats a `Number`: an integral type as a `long`, anything else as a + /// `double`. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: if `number` is not a `Number` + public final String format(Object number) { + return format(number, new StringBuffer(), new FieldPosition(0)).toString(); + } + + public StringBuffer format(Object number, StringBuffer toAppendTo, FieldPosition pos) { + if (number instanceof Long || number instanceof Integer || number instanceof Short + || number instanceof Byte) { + return format(((Number) number).longValue(), toAppendTo, pos); + } + if (number instanceof Number) { + return format(((Number) number).doubleValue(), toAppendTo, pos); + } + throw new IllegalArgumentException("Cannot format given Object as a Number"); + } + + public final String format(double number) { + return format(number, new StringBuffer(), new FieldPosition(0)).toString(); + } + + public final String format(long number) { + return format(number, new StringBuffer(), new FieldPosition(0)).toString(); + } + + public abstract StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos); + + public abstract StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos); + + /// Parses a number starting at `parsePosition`, which is advanced past + /// what was read. On failure the position is left alone, its error index + /// is set, and `null` is returned. The result is a `Long` when the text is + /// a whole number that fits one, and a `Double` otherwise. + public abstract Number parse(String source, ParsePosition parsePosition); + + public Number parse(String source) throws ParseException { + ParsePosition parsePosition = new ParsePosition(0); + Number result = parse(source, parsePosition); + if (parsePosition.getIndex() == 0) { + throw new ParseException("Unparseable number: \"" + source + "\"", parsePosition.getErrorIndex()); + } + return result; + } + + public final Object parseObject(String source, ParsePosition pos) { + return parse(source, pos); + } + + public Object parseObject(String source) throws ParseException { + ParsePosition pos = new ParsePosition(0); + Object result = parseObject(source, pos); + if (pos.getIndex() == 0) { + throw new ParseException("Format.parseObject(String) failed", pos.getErrorIndex()); + } + return result; + } + + public boolean isParseIntegerOnly() { + return parseIntegerOnly; + } + + public void setParseIntegerOnly(boolean value) { + parseIntegerOnly = value; + } + + public boolean isGroupingUsed() { + return groupingUsed; + } + + public void setGroupingUsed(boolean newValue) { + groupingUsed = newValue; + } + + public int getMaximumIntegerDigits() { + return maximumIntegerDigits; + } + + public void setMaximumIntegerDigits(int newValue) { + maximumIntegerDigits = Math.max(0, newValue); + if (minimumIntegerDigits > maximumIntegerDigits) { + minimumIntegerDigits = maximumIntegerDigits; + } + } + + public int getMinimumIntegerDigits() { + return minimumIntegerDigits; + } + + public void setMinimumIntegerDigits(int newValue) { + minimumIntegerDigits = Math.max(0, newValue); + if (minimumIntegerDigits > maximumIntegerDigits) { + maximumIntegerDigits = minimumIntegerDigits; + } + } + + public int getMaximumFractionDigits() { + return maximumFractionDigits; + } + + public void setMaximumFractionDigits(int newValue) { + maximumFractionDigits = Math.max(0, newValue); + if (maximumFractionDigits < minimumFractionDigits) { + minimumFractionDigits = maximumFractionDigits; + } + } + + public int getMinimumFractionDigits() { + return minimumFractionDigits; + } + + public void setMinimumFractionDigits(int newValue) { + minimumFractionDigits = Math.max(0, newValue); + if (maximumFractionDigits < minimumFractionDigits) { + maximumFractionDigits = minimumFractionDigits; + } + } + + /// Copies the settings this class holds into `target`, for the `clone` + /// of a subclass: `Object.clone()` answers `null` on the device. + final void copyNumberFormatTo(NumberFormat target) { + target.groupingUsed = groupingUsed; + target.maximumIntegerDigits = maximumIntegerDigits; + target.minimumIntegerDigits = minimumIntegerDigits; + target.maximumFractionDigits = maximumFractionDigits; + target.minimumFractionDigits = minimumFractionDigits; + target.parseIntegerOnly = parseIntegerOnly; + } + + /// A copy of this format. Every subclass in this package implements it + /// by copying its fields. + @Override + public abstract Object clone(); + + @Override + public int hashCode() { + return maximumIntegerDigits * 37 + maximumFractionDigits; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (getClass() != obj.getClass() || !(obj instanceof NumberFormat)) { + return false; + } + NumberFormat other = (NumberFormat) obj; + return maximumIntegerDigits == other.maximumIntegerDigits + && minimumIntegerDigits == other.minimumIntegerDigits + && maximumFractionDigits == other.maximumFractionDigits + && minimumFractionDigits == other.minimumFractionDigits + && groupingUsed == other.groupingUsed + && parseIntegerOnly == other.parseIntegerOnly; + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Optional.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Optional.java new file mode 100644 index 00000000000..2fcdad39052 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/Optional.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.util.NoSuchElementException; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/// `java.util.Optional` for the Codename One runtime: a container that holds +/// a non-null value or nothing. +public final class Optional { + + private static final Optional EMPTY = new Optional(null); + + private final T value; + + private Optional(T value) { + this.value = value; + } + + @SuppressWarnings("unchecked") + public static Optional empty() { + return (Optional) EMPTY; + } + + public static Optional of(T value) { + if (value == null) { + throw new NullPointerException(); + } + return new Optional(value); + } + + public static Optional ofNullable(T value) { + if (value == null) { + return empty(); + } + return new Optional(value); + } + + public T get() { + if (value == null) { + throw new NoSuchElementException("No value present"); + } + return value; + } + + public boolean isPresent() { + return value != null; + } + + public void ifPresent(Consumer consumer) { + if (value != null) { + consumer.accept(value); + } + } + + public Optional filter(Predicate predicate) { + if (predicate == null) { + throw new NullPointerException(); + } + if (value == null) { + return this; + } + return predicate.test(value) ? this : Optional.empty(); + } + + public Optional map(Function mapper) { + if (mapper == null) { + throw new NullPointerException(); + } + if (value == null) { + return empty(); + } + return Optional.ofNullable(mapper.apply(value)); + } + + public Optional flatMap(Function> mapper) { + if (mapper == null) { + throw new NullPointerException(); + } + if (value == null) { + return empty(); + } + Optional result = mapper.apply(value); + if (result == null) { + throw new NullPointerException(); + } + return result; + } + + public T orElse(T other) { + return value != null ? value : other; + } + + public T orElseGet(Supplier other) { + return value != null ? value : other.get(); + } + + public T orElseThrow(Supplier exceptionSupplier) throws X { + if (value != null) { + return value; + } + throw exceptionSupplier.get(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Optional)) { + return false; + } + Object other = ((Optional) obj).value; + return value == null ? other == null : value.equals(other); + } + + @Override + public int hashCode() { + return value == null ? 0 : value.hashCode(); + } + + @Override + public String toString() { + return value != null ? "Optional[" + value + "]" : "Optional.empty"; + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ParsePosition.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ParsePosition.java new file mode 100644 index 00000000000..3cf536b3ba8 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ParsePosition.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.text.ParsePosition` for the Codename One runtime: where a parse +/// starts, where it stopped, and where it failed. +public class ParsePosition { + + private int index; + private int errorIndex = -1; + + public ParsePosition(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public void setErrorIndex(int ei) { + errorIndex = ei; + } + + public int getErrorIndex() { + return errorIndex; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ParsePosition)) { + return false; + } + ParsePosition other = (ParsePosition) obj; + return index == other.index && errorIndex == other.errorIndex; + } + + @Override + public int hashCode() { + return (errorIndex << 16) | index; + } + + @Override + public String toString() { + return getClass().getName() + "[index=" + index + ",errorIndex=" + errorIndex + ']'; + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/PropertyResourceBundle.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/PropertyResourceBundle.java new file mode 100644 index 00000000000..708ead921fd --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/PropertyResourceBundle.java @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/// `java.util.PropertyResourceBundle` for the Codename One runtime: a +/// resource bundle read from a `.properties` file. +/// +/// The parser implements the whole `java.util.Properties` text format, since +/// the device library has no `Properties` class to delegate to: `#` and `!` +/// comment lines, `=`, `:` or whitespace between key and value, a logical line +/// continued by a trailing backslash, and the escapes -- tab, newline, form +/// feed, carriage return, backslash-u followed by four hex digits, and a +/// backslash before any other character standing for that character. +/// +/// A stream is decoded as ISO-8859-1, one byte to one character, which is +/// what the JDK did for as long as `.properties` files have carried +/// backslash-u escapes for everything else. Pass a `Reader` to choose another +/// encoding. +public class PropertyResourceBundle extends ResourceBundle { + + private final Map lookup = new HashMap(); + + public PropertyResourceBundle(InputStream stream) throws IOException { + StringBuilder text = new StringBuilder(); + byte[] buffer = new byte[4096]; + int n = stream.read(buffer); + while (n >= 0) { + for (int i = 0; i < n; i++) { + text.append((char) (buffer[i] & 0xff)); + } + n = stream.read(buffer); + } + parse(text.toString(), lookup); + } + + public PropertyResourceBundle(Reader reader) throws IOException { + StringBuilder text = new StringBuilder(); + char[] buffer = new char[4096]; + int n = reader.read(buffer, 0, buffer.length); + while (n >= 0) { + text.append(buffer, 0, n); + n = reader.read(buffer, 0, buffer.length); + } + parse(text.toString(), lookup); + } + + @Override + public Object handleGetObject(String key) { + if (key == null) { + throw new NullPointerException(); + } + return lookup.get(key); + } + + @Override + public Enumeration getKeys() { + return new ResourceBundle.KeyEnumeration(keySet().iterator()); + } + + @Override + protected Set handleKeySet() { + return lookup.keySet(); + } + + private static boolean isBlank(char c) { + return c == ' ' || c == '\t' || c == '\f'; + } + + private static boolean isLineEnd(char c) { + return c == '\n' || c == '\r'; + } + + /// Reads `.properties` text into `out`, a later definition of a key + /// replacing an earlier one. + static void parse(String text, Map out) { + int n = text.length(); + int i = 0; + StringBuilder line = new StringBuilder(); + while (i < n) { + // Blank lines and the indentation of a logical line carry nothing. + while (i < n && (isBlank(text.charAt(i)) || isLineEnd(text.charAt(i)))) { + i++; + } + if (i >= n) { + break; + } + char c = text.charAt(i); + if (c == '#' || c == '!') { + // A comment runs to the end of its natural line and cannot be + // continued, whatever it ends with. + while (i < n && !isLineEnd(text.charAt(i))) { + i++; + } + continue; + } + line.setLength(0); + boolean escaped = false; + // A line that is nothing but a continuation is no entry, unless + // the input ends in the middle of it: then the JDK defines the + // empty key, and so does this. + boolean cutShort = false; + while (i < n) { + c = text.charAt(i); + if (isLineEnd(c)) { + if (!escaped) { + break; + } + // An odd run of backslashes before the terminator joins + // the next natural line, minus its indentation. + line.setLength(line.length() - 1); + i++; + cutShort = i >= n; + if (c == '\r' && i < n && text.charAt(i) == '\n') { + i++; + } + while (i < n && isBlank(text.charAt(i))) { + i++; + } + escaped = false; + continue; + } + line.append(c); + escaped = c == '\\' && !escaped; + i++; + } + if (escaped) { + // A lone backslash at the end of the input escapes nothing. + line.setLength(line.length() - 1); + cutShort = true; + } + if (line.length() > 0 || cutShort) { + store(line, out); + } + } + } + + private static void store(StringBuilder line, Map out) { + int len = line.length(); + int keyLen = 0; + int valueStart = len; + boolean hasSeparator = false; + boolean escaped = false; + while (keyLen < len) { + char c = line.charAt(keyLen); + if ((c == '=' || c == ':') && !escaped) { + valueStart = keyLen + 1; + hasSeparator = true; + break; + } + if (isBlank(c) && !escaped) { + valueStart = keyLen + 1; + break; + } + escaped = c == '\\' && !escaped; + keyLen++; + } + // Whitespace may surround the one separator character. + while (valueStart < len) { + char c = line.charAt(valueStart); + if (!isBlank(c)) { + if (!hasSeparator && (c == '=' || c == ':')) { + hasSeparator = true; + } else { + break; + } + } + valueStart++; + } + out.put(unescape(line, 0, keyLen), unescape(line, valueStart, len)); + } + + private static int hexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + throw new IllegalArgumentException("Malformed unicode escape in properties text"); + } + + private static String unescape(StringBuilder in, int from, int to) { + StringBuilder sb = new StringBuilder(to - from); + int i = from; + while (i < to) { + char c = in.charAt(i++); + if (c != '\\' || i >= to) { + if (c != '\\') { + sb.append(c); + } + continue; + } + c = in.charAt(i++); + if (c == 'u') { + if (i + 4 > to) { + throw new IllegalArgumentException("Malformed unicode escape in properties text"); + } + int value = 0; + for (int k = 0; k < 4; k++) { + value = (value << 4) | hexDigit(in.charAt(i++)); + } + sb.append((char) value); + } else if (c == 't') { + sb.append('\t'); + } else if (c == 'r') { + sb.append('\r'); + } else if (c == 'n') { + sb.append('\n'); + } else if (c == 'f') { + sb.append('\f'); + } else { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ResourceBundle.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ResourceBundle.java new file mode 100644 index 00000000000..2547798e615 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/ResourceBundle.java @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/// `java.util.ResourceBundle` for the Codename One runtime: locale specific +/// strings and objects, looked up by key, with a chain of less specific +/// bundles behind each one. +/// +/// #### How a bundle is found +/// +/// `getBundle("com.example.Messages", locale)` looks for three bundles, most +/// specific first -- `Messages__`, `Messages_` +/// and `Messages` -- and chains the ones that exist, so a key missing from +/// the first is answered by the next. When nothing more specific than the +/// base bundle exists for the requested locale, the default locale is tried +/// the same way before the base bundle is settled for. Finding none at all +/// throws `MissingResourceException`. +/// +/// Each of the three is resolved in this order: +/// +/// 1. A resource registered through [#cn1Register(String, String, String)]. +/// 2. A class of that name which extends `ResourceBundle`, a +/// `ListResourceBundle` typically, created through its public no-argument +/// constructor. +/// 3. The resource `/com/example/Messages_.properties`, read through +/// `Class.getResourceAsStream`. +/// +/// #### Flat resource namespaces +/// +/// Some Codename One targets package resources in one flat directory, where +/// `/com/example/Messages_fr.properties` does not exist under that name. +/// `cn1Register` is the hook for that: the build step that flattens the +/// resources generates one call per bundle file, mapping the bundle to the +/// name it was given. +/// +/// Bundles are cached for the life of the application, or until +/// [#clearCache()]. `ResourceBundle.Control`, and the lookups that take one, +/// are not provided. +public abstract class ResourceBundle { + + /// Bundle files a build step has renamed, keyed by base name and locale + /// suffix. + private static final Map REGISTERED = new HashMap(); + + /// Every bundle already loaded, under the same key. `MISSING` records a + /// bundle that is known not to exist, so it is looked for once. + private static final Map CACHE = new HashMap(); + + private static final Object MISSING = new Object(); + + /// The bundle consulted for a key this one does not have, or `null`. + protected ResourceBundle parent; + + private Locale locale; + private String baseName; + + public ResourceBundle() { + } + + /// Maps one bundle file to the resource it is packaged as. A build step + /// that flattens or renames resources calls this, before the application + /// asks for the bundle, once for every `.properties` file it moved. + /// + /// #### Parameters + /// + /// - `baseName`: the bundle's base name as `getBundle` receives it, + /// `com.example.Messages`. The path form `com/example/Messages` is + /// accepted too. + /// + /// - `localeSuffix`: the part of the file name that selects the locale, + /// with its leading underscore -- `_fr`, `_fr_CA` -- or the empty string + /// for the base bundle. + /// + /// - `resourceName`: the name to pass to `Class.getResourceAsStream`, + /// `/Messages_fr.properties`. A missing leading slash is added. + public static void cn1Register(String baseName, String localeSuffix, String resourceName) { + if (baseName == null || resourceName == null) { + throw new NullPointerException(); + } + String suffix = localeSuffix == null ? "" : localeSuffix; + String resource = resourceName.startsWith("/") ? resourceName : "/" + resourceName; + String key = cacheKey(baseName.replace('/', '.'), suffix); + REGISTERED.put(key, resource); + CACHE.remove(key); + } + + public static final ResourceBundle getBundle(String baseName) { + return getBundle(baseName, Locale.getDefault()); + } + + /// Looks the bundle up as `getBundle(String, Locale)` does. The class + /// loader is not used: an application has one. + public static ResourceBundle getBundle(String baseName, Locale locale, ClassLoader loader) { + return getBundle(baseName, locale); + } + + public static final ResourceBundle getBundle(String baseName, Locale locale) { + if (baseName == null || locale == null) { + throw new NullPointerException(); + } + String name = baseName.replace('/', '.'); + String language = nonNull(locale.getLanguage()); + String country = nonNull(locale.getCountry()); + ResourceBundle found = findChain(name, language, country); + if (found == null || found.locale.getLanguage().length() == 0) { + // Nothing specific to the requested locale: the default locale's + // bundle is preferred to the base bundle. + Locale fallback = Locale.getDefault(); + String fallbackLanguage = nonNull(fallback.getLanguage()); + String fallbackCountry = nonNull(fallback.getCountry()); + boolean requestedRoot = language.length() == 0; + boolean sameLocale = fallbackLanguage.equals(language) && fallbackCountry.equals(country); + if (!sameLocale && !(requestedRoot && found != null)) { + ResourceBundle viaDefault = findChain(name, fallbackLanguage, fallbackCountry); + if (viaDefault != null) { + found = viaDefault; + } + } + } + if (found == null) { + String localeName = localeSuffix(language, country); + throw new MissingResourceException( + "Can't find bundle for base name " + baseName + ", locale " + + (localeName.length() == 0 ? "" : localeName.substring(1)), + baseName + localeName, ""); + } + return found; + } + + public static final void clearCache() { + CACHE.clear(); + } + + private static String nonNull(String s) { + return s == null ? "" : s; + } + + private static String cacheKey(String baseName, String suffix) { + return baseName + '\n' + suffix; + } + + private static String localeSuffix(String language, String country) { + if (language.length() == 0) { + return ""; + } + return country.length() == 0 ? "_" + language : "_" + language + "_" + country; + } + + /// The most specific bundle that exists for the locale, with the less + /// specific ones chained behind it, or `null` when not even the base + /// bundle exists. + private static ResourceBundle findChain(String baseName, String language, String country) { + ResourceBundle result = load(baseName, "", "", "", null); + if (language.length() > 0) { + ResourceBundle forLanguage = load(baseName, "_" + language, language, "", result); + if (forLanguage != null) { + result = forLanguage; + } + if (country.length() > 0) { + ResourceBundle forCountry = + load(baseName, "_" + language + "_" + country, language, country, result); + if (forCountry != null) { + result = forCountry; + } + } + } + return result; + } + + private static ResourceBundle load(String baseName, String suffix, String language, String country, + ResourceBundle parent) { + String key = cacheKey(baseName, suffix); + Object cached = CACHE.get(key); + if (cached instanceof ResourceBundle) { + return (ResourceBundle) cached; + } + if (cached != null) { + return null; + } + ResourceBundle bundle = create(baseName, suffix, REGISTERED.get(key)); + if (bundle == null) { + CACHE.put(key, MISSING); + return null; + } + if (bundle.parent == null) { + // The less specific bundle of a name is the same whichever locale + // led here, so the chain is fixed the first time it is built. + bundle.parent = parent; + } + bundle.locale = new Locale(language, country); + bundle.baseName = baseName; + CACHE.put(key, bundle); + return bundle; + } + + private static ResourceBundle create(String baseName, String suffix, String registered) { + if (registered != null) { + ResourceBundle fromRegistered = read(registered); + if (fromRegistered != null) { + return fromRegistered; + } + } + try { + Object instance = Class.forName(baseName + suffix).newInstance(); + if (instance instanceof ResourceBundle) { + return (ResourceBundle) instance; + } + } catch (ClassNotFoundException e) { + // No class of this name: the bundle is a properties file, or absent. + } catch (InstantiationException e) { + // Not a class this lookup can create; fall through to the file. + } catch (IllegalAccessException e) { + // As above. + } + return read("/" + baseName.replace('.', '/') + suffix + ".properties"); + } + + private static ResourceBundle read(String resource) { + InputStream in = ResourceBundle.class.getResourceAsStream(resource); + if (in == null) { + return null; + } + try { + return new PropertyResourceBundle(in); + } catch (IOException e) { + // A bundle that cannot be read is a bundle that is not there. + return null; + } finally { + try { + in.close(); + } catch (IOException e) { + // Nothing to do about a failed close of a stream already read. + } + } + } + + public final String getString(String key) { + Object value = getObject(key); + if (value instanceof String) { + return (String) value; + } + throw new ClassCastException("Resource " + key + " is not a String"); + } + + public final String[] getStringArray(String key) { + Object value = getObject(key); + if (value instanceof String[]) { + return (String[]) value; + } + throw new ClassCastException("Resource " + key + " is not a String array"); + } + + public final Object getObject(String key) { + if (key == null) { + throw new NullPointerException(); + } + for (ResourceBundle b = this; b != null; b = b.parent) { + Object value = b.handleGetObject(key); + if (value != null) { + return value; + } + } + throw new MissingResourceException( + "Can't find resource for bundle " + getClass().getName() + ", key " + key, + getClass().getName(), key); + } + + /// The locale this bundle was found for, which may be less specific than + /// the one asked for. The base bundle's has an empty language. + public Locale getLocale() { + return locale; + } + + public String getBaseBundleName() { + return baseName; + } + + protected void setParent(ResourceBundle parent) { + this.parent = parent; + } + + public boolean containsKey(String key) { + if (key == null) { + throw new NullPointerException(); + } + for (ResourceBundle b = this; b != null; b = b.parent) { + if (b.handleKeySet().contains(key)) { + return true; + } + } + return false; + } + + /// Every key of this bundle and of the bundles behind it. + public Set keySet() { + Set keys = new HashSet(); + for (ResourceBundle b = this; b != null; b = b.parent) { + keys.addAll(b.handleKeySet()); + } + return keys; + } + + /// The keys this bundle itself defines. The default derives them from + /// [#getKeys()]; a subclass that has them at hand should override it. + protected Set handleKeySet() { + Set keys = new HashSet(); + Enumeration all = getKeys(); + while (all.hasMoreElements()) { + String key = all.nextElement(); + if (handleGetObject(key) != null) { + keys.add(key); + } + } + return keys; + } + + protected abstract Object handleGetObject(String key); + + public abstract Enumeration getKeys(); + + /// An `Enumeration` over an iterator, for the `getKeys` implementations. + static final class KeyEnumeration implements Enumeration { + private final Iterator it; + + KeyEnumeration(Iterator it) { + this.it = it; + } + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public String nextElement() { + return it.next(); + } + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/StringJoiner.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/StringJoiner.java new file mode 100644 index 00000000000..76321393fea --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/StringJoiner.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.util.StringJoiner` for the Codename One runtime: builds a sequence +/// of strings separated by a delimiter, optionally between a prefix and a +/// suffix. +public final class StringJoiner { + + private final String prefix; + private final String delimiter; + private final String suffix; + private StringBuilder value; + private String emptyValue; + + public StringJoiner(CharSequence delimiter) { + this(delimiter, "", ""); + } + + public StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix) { + if (prefix == null) { + throw new NullPointerException("The prefix must not be null"); + } + if (delimiter == null) { + throw new NullPointerException("The delimiter must not be null"); + } + if (suffix == null) { + throw new NullPointerException("The suffix must not be null"); + } + this.prefix = prefix.toString(); + this.delimiter = delimiter.toString(); + this.suffix = suffix.toString(); + this.emptyValue = this.prefix + this.suffix; + } + + public StringJoiner setEmptyValue(CharSequence emptyValue) { + if (emptyValue == null) { + throw new NullPointerException("The empty value must not be null"); + } + this.emptyValue = emptyValue.toString(); + return this; + } + + @Override + public String toString() { + if (value == null) { + return emptyValue; + } + if (suffix.length() == 0) { + return value.toString(); + } + return value.toString() + suffix; + } + + public StringJoiner add(CharSequence newElement) { + prepareBuilder().append(String.valueOf(newElement)); + return this; + } + + public StringJoiner merge(StringJoiner other) { + if (other == null) { + throw new NullPointerException(); + } + if (other.value != null) { + // Taken before the builder is prepared, so that merging a joiner + // into itself appends its content once. + String content = other.value.toString().substring(other.prefix.length()); + prepareBuilder().append(content); + } + return this; + } + + private StringBuilder prepareBuilder() { + if (value != null) { + value.append(delimiter); + } else { + value = new StringBuilder().append(prefix); + } + return value; + } + + public int length() { + return value != null ? value.length() + suffix.length() : emptyValue.length(); + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/TimeUnit.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/TimeUnit.java new file mode 100644 index 00000000000..84de82361b5 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/TimeUnit.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.util.concurrent.TimeUnit` for the Codename One runtime: a duration +/// granularity and the conversions between granularities. Conversions to a +/// coarser unit truncate; conversions to a finer one saturate at +/// `Long.MIN_VALUE` and `Long.MAX_VALUE` instead of overflowing, as the JDK's +/// do. +/// +/// `timedWait` and `timedJoin` are not provided: they exist to coordinate +/// threads through a monitor, and Codename One application code is confined +/// to one thread. +public enum TimeUnit { + NANOSECONDS(1L), + MICROSECONDS(1000L), + MILLISECONDS(1000L * 1000L), + SECONDS(1000L * 1000L * 1000L), + MINUTES(60L * 1000L * 1000L * 1000L), + HOURS(60L * 60L * 1000L * 1000L * 1000L), + DAYS(24L * 60L * 60L * 1000L * 1000L * 1000L); + + /// Nanoseconds in one of this unit. Each unit's value is an exact + /// multiple of every finer unit's, and never zero, so the divisions below + /// are exact and cannot divide by zero. + private final long nanos; + + TimeUnit(long nanos) { + this.nanos = nanos; + } + + private static long scale(long duration, long from, long to) { + if (from == to) { + return duration; + } + if (from < to) { + return duration / (to / from); + } + long ratio = from / to; + long limit = Long.MAX_VALUE / ratio; + if (duration > limit) { + return Long.MAX_VALUE; + } + if (duration < -limit) { + return Long.MIN_VALUE; + } + return duration * ratio; + } + + public long convert(long sourceDuration, TimeUnit sourceUnit) { + return scale(sourceDuration, sourceUnit.nanos, nanos); + } + + public long toNanos(long duration) { + return scale(duration, nanos, NANOSECONDS.nanos); + } + + public long toMicros(long duration) { + return scale(duration, nanos, MICROSECONDS.nanos); + } + + public long toMillis(long duration) { + return scale(duration, nanos, MILLISECONDS.nanos); + } + + public long toSeconds(long duration) { + return scale(duration, nanos, SECONDS.nanos); + } + + public long toMinutes(long duration) { + return scale(duration, nanos, MINUTES.nanos); + } + + public long toHours(long duration) { + return scale(duration, nanos, HOURS.nanos); + } + + public long toDays(long duration) { + return scale(duration, nanos, DAYS.nanos); + } + + /// Sleeps for the duration, rounded down to whole milliseconds; a + /// duration of zero or less returns at once. + public void sleep(long timeout) throws InterruptedException { + long millis = toMillis(timeout); + if (millis > 0) { + Thread.sleep(millis); + } + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/TimeoutException.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/TimeoutException.java new file mode 100644 index 00000000000..6b89db2724b --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/TimeoutException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +/// `java.util.concurrent.TimeoutException` for the Codename One runtime: a +/// blocking operation timed out. `Future.get(long, TimeUnit)` declares it. +public class TimeoutException extends Exception { + + private static final long serialVersionUID = 1L; + + public TimeoutException() { + super(); + } + + public TimeoutException(String message) { + super(message); + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/URL.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/URL.java new file mode 100644 index 00000000000..40bb3e53187 --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/URL.java @@ -0,0 +1,759 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import com.codename1.io.ConnectionRequest; +import com.codename1.io.NetworkManager; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; + +/// `java.net.URL` for the Codename One runtime: a parsed URL, and a way to +/// read what it points at. +/// +/// Parsing follows the JDK class, including the resolution of a relative +/// reference against a context URL. The protocols accepted are `http`, +/// `https`, `file`, `ftp`, `jar`, `mailto` and `cn1res`; any other throws +/// `MalformedURLException`, as an unknown protocol does on the JDK. +/// +/// #### Reading +/// +/// [#openStream()] reads: +/// +/// - `file:` through the application's file system (`FileInputStream` of +/// this package). +/// - `http:` and `https:` through a Codename One `ConnectionRequest`, run +/// to completion before the stream is returned: the stream is over the +/// whole downloaded body, so it suits configuration files and images, not +/// endless or very large responses. +/// - `cn1res:` from the application's packaged resources. +/// +/// #### The `cn1res` scheme +/// +/// `cn1res:/images/logo.png` names the resource `/images/logo.png` of the +/// application, the one `Class.getResourceAsStream("/images/logo.png")` +/// opens. The scheme exists because the device has no `Class.getResource`: +/// a later step of the build rewrites calls to `Class.getResource(String)` so +/// that they return a URL of this scheme, and code that goes on to call +/// `openStream()`, `toString()` or `toExternalForm()` on the result keeps +/// working. A `cn1res` URL has no host; its path is the absolute resource +/// name. +/// +/// #### Differences from the JDK +/// +/// - No `openConnection`, `getContent` or stream handler factories: the +/// device has no `URLConnection`. +/// - `equals` and `hashCode` compare host names as text, ignoring case, and +/// never resolve them. +public final class URL implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private final String protocol; + private final String host; + private final int port; + private final String authority; + private final String userInfo; + private final String path; + private final String query; + private final String ref; + + public URL(String spec) throws MalformedURLException { + this(null, spec); + } + + public URL(String protocol, String host, String file) throws MalformedURLException { + this(protocol, host, -1, file); + } + + public URL(String protocol, String host, int port, String file) throws MalformedURLException { + String scheme = asciiLower(protocol); + if (!isKnownProtocol(scheme)) { + throw new MalformedURLException("unknown protocol: " + protocol); + } + this.protocol = scheme; + if (host != null) { + String h = host; + if (h.indexOf(':') >= 0 && !h.startsWith("[")) { + h = "[" + h + "]"; + } + if (port < -1) { + throw new MalformedURLException("Invalid port number :" + port); + } + this.host = h; + this.port = port; + this.authority = port == -1 ? h : h + ":" + port; + } else { + this.host = null; + this.port = -1; + this.authority = null; + } + this.userInfo = null; + String rest = file; + int hash = rest.indexOf('#'); + this.ref = hash < 0 ? null : rest.substring(hash + 1); + if (hash >= 0) { + rest = rest.substring(0, hash); + } + int question = rest.lastIndexOf('?'); + if (question >= 0) { + this.query = rest.substring(question + 1); + this.path = rest.substring(0, question); + } else { + this.query = null; + this.path = rest; + } + } + + /// Parses `spec`, resolving it against `context` when it is a relative + /// reference; `context` may be `null`. + public URL(URL context, String spec) throws MalformedURLException { + if (spec == null) { + throw new MalformedURLException("no protocol: null"); + } + int limit = spec.length(); + while (limit > 0 && spec.charAt(limit - 1) <= ' ') { + limit--; + } + int start = 0; + while (start < limit && spec.charAt(start) <= ' ') { + start++; + } + if (spec.regionMatches(true, start, "url:", 0, 4)) { + start += 4; + } + boolean isRef = start < spec.length() && spec.charAt(start) == '#'; + String newProtocol = null; + for (int i = start; !isRef && i < limit; i++) { + char c = spec.charAt(i); + if (c == '/') { + break; + } + if (c == ':') { + String s = asciiLower(spec.substring(start, i)); + if (isValidProtocol(s)) { + newProtocol = s; + start = i + 1; + } + break; + } + } + + String p = newProtocol; + String h = null; + int prt = -1; + String auth = null; + String user = null; + String pth = null; + String qry = null; + String rf = null; + boolean isRelative = false; + if (context != null && (newProtocol == null || newProtocol.equals(context.protocol))) { + // A scheme repeated in front of a relative reference to a + // hierarchical URL is ignored. + if (context.path != null && context.path.startsWith("/")) { + newProtocol = null; + } + if (newProtocol == null) { + p = context.protocol; + auth = context.authority; + user = context.userInfo; + h = context.host; + prt = context.port; + pth = context.path; + isRelative = true; + } + } + if (p == null) { + throw new MalformedURLException("no protocol: " + spec); + } + if (!isKnownProtocol(p)) { + throw new MalformedURLException("unknown protocol: " + p); + } + int hash = spec.indexOf('#', start); + if (hash >= 0) { + rf = spec.substring(hash + 1, limit); + limit = hash; + } + if (isRelative && start == limit) { + // An empty reference is the context itself. + qry = context.query; + if (rf == null) { + rf = context.ref; + } + } + + if ("jar".equals(p) || "mailto".equals(p)) { + // Not hierarchical: everything after the scheme is the file. + String file; + if ("mailto".equals(p)) { + file = spec.substring(start, limit); + if (file.trim().length() == 0) { + throw new MalformedURLException("No email address"); + } + } else if (hash == start && isRelative) { + file = context.getFile(); + } else { + file = jarFile(isRelative ? context.getFile() : null, spec.substring(start, limit), + newProtocol != null); + } + if ("mailto".equals(p)) { + // As in the JDK, an address has no fragment. + rf = null; + } + int question = file.lastIndexOf('?'); + this.protocol = p; + this.host = ""; + this.port = -1; + this.authority = null; + this.userInfo = null; + this.path = question < 0 ? file : file.substring(0, question); + this.query = question < 0 ? null : file.substring(question + 1); + this.ref = rf; + return; + } + + // From here on: the authority, then the path and the query. + String s = spec; + boolean isRelPath = false; + boolean queryOnly = false; + if (start < limit) { + int queryStart = s.indexOf('?'); + queryOnly = queryStart == start; + if (queryStart != -1 && queryStart < limit) { + qry = s.substring(queryStart + 1, limit); + limit = queryStart; + s = s.substring(0, queryStart); + } + } + boolean fourSlashes = start <= limit - 4 && s.charAt(start) == '/' && s.charAt(start + 1) == '/' + && s.charAt(start + 2) == '/' && s.charAt(start + 3) == '/'; + if (!fourSlashes && start <= limit - 2 && s.charAt(start) == '/' && s.charAt(start + 1) == '/') { + start += 2; + int end = s.indexOf('/', start); + if (end < 0 || end > limit) { + end = s.indexOf('?', start); + if (end < 0 || end > limit) { + end = limit; + } + } + auth = s.substring(start, end); + h = auth; + int at = auth.indexOf('@'); + if (at != -1) { + if (at != auth.lastIndexOf('@')) { + user = null; + h = null; + } else { + user = auth.substring(0, at); + h = auth.substring(at + 1); + } + } else { + user = null; + } + prt = -1; + if (h != null) { + String portText = null; + if (h.length() > 0 && h.charAt(0) == '[') { + int close = h.indexOf(']'); + if (close <= 2) { + throw new MalformedURLException("Invalid authority field: " + auth); + } + if (h.length() > close + 1) { + if (h.charAt(close + 1) != ':') { + throw new MalformedURLException("Invalid authority field: " + auth); + } + portText = h.substring(close + 2); + } + h = h.substring(0, close + 1); + } else { + int colon = h.indexOf(':'); + if (colon >= 0) { + portText = h.substring(colon + 1); + h = h.substring(0, colon); + } + } + if (portText != null && portText.length() > 0) { + prt = parsePort(portText); + } + } else { + h = ""; + } + start = end; + if (auth.length() > 0) { + // With an authority, the path comes from the reference alone. + pth = ""; + } + } + if (h == null) { + h = ""; + } + if (start < limit) { + if (s.charAt(start) == '/') { + pth = s.substring(start, limit); + } else if (pth != null && pth.length() > 0) { + isRelPath = true; + int slash = pth.lastIndexOf('/'); + String separator = slash == -1 && auth != null ? "/" : ""; + pth = pth.substring(0, slash + 1) + separator + s.substring(start, limit); + } else { + String separator = auth != null ? "/" : ""; + pth = separator + s.substring(start, limit); + } + } else if (queryOnly && pth != null) { + int slash = pth.lastIndexOf('/'); + if (slash < 0) { + slash = 0; + } + pth = pth.substring(0, slash) + "/"; + } + if (pth == null) { + pth = ""; + } + if (isRelPath) { + pth = removeDots(pth); + } + this.protocol = p; + this.host = h; + this.port = prt; + this.authority = auth; + this.userInfo = user; + this.path = pth; + this.query = qry; + this.ref = rf; + } + + /// The index of the `/` of the last `!/` in a `jar:` file, or -1. + private static int indexOfBangSlash(String file) { + int bang = file.lastIndexOf('!'); + while (bang >= 0) { + if (bang != file.length() - 1 && file.charAt(bang + 1) == '/') { + return bang + 1; + } + bang = bang == 0 ? -1 : file.lastIndexOf('!', bang - 1); + } + return -1; + } + + /// The file of a `jar:` URL: an archive's own URL, `!/`, and the entry. + /// A relative reference is resolved inside the archive, never out of it. + private static String jarFile(String contextFile, String part, boolean absolute) + throws MalformedURLException { + if (absolute) { + int bangSlash = indexOfBangSlash(part); + if (bangSlash < 0) { + throw new MalformedURLException("no !/ in spec"); + } + try { + new URL(part.substring(0, bangSlash - 1)); + } catch (MalformedURLException e) { + throw new MalformedURLException("invalid url: " + part + " (" + e.getMessage() + ")"); + } + return part; + } + if (contextFile == null) { + throw new MalformedURLException("no !/ in spec"); + } + String file = contextFile; + if (part.startsWith("/")) { + int bangSlash = indexOfBangSlash(file); + if (bangSlash < 0) { + throw new MalformedURLException("malformed context url: no !/"); + } + file = file.substring(0, bangSlash); + } else if (!file.endsWith("/")) { + int lastSlash = file.lastIndexOf('/'); + if (lastSlash < 0) { + throw new MalformedURLException("malformed context url"); + } + file = file.substring(0, lastSlash + 1); + } + file = file + part; + int bangSlash = indexOfBangSlash(file); + if (bangSlash < 0) { + throw new MalformedURLException("no !/ in spec"); + } + return file.substring(0, bangSlash) + canonizeEntry(file.substring(bangSlash)); + } + + /// Resolves `.` and `..` in the entry part of a `jar:` file. + private static String canonizeEntry(String entry) { + String file = entry; + int i = file.indexOf("/../"); + while (i >= 0) { + int lim = i > 0 ? file.lastIndexOf('/', i - 1) : -1; + file = lim >= 0 ? file.substring(0, lim) + file.substring(i + 3) : file.substring(i + 3); + i = file.indexOf("/../"); + } + i = file.indexOf("/./"); + while (i >= 0) { + file = file.substring(0, i) + file.substring(i + 2); + i = file.indexOf("/./"); + } + while (file.endsWith("/..")) { + i = file.indexOf("/.."); + int lim = i > 0 ? file.lastIndexOf('/', i - 1) : -1; + file = lim >= 0 ? file.substring(0, lim + 1) : file.substring(0, i); + } + if (file.endsWith("/.")) { + file = file.substring(0, file.length() - 1); + } + return file; + } + + /// A port as the JDK reads one: a decimal `int` with an optional sign, + /// of which -1, "no port", is the only negative accepted. + private static int parsePort(String text) throws MalformedURLException { + int from = 0; + boolean negative = false; + if (text.charAt(0) == '-' || text.charAt(0) == '+') { + negative = text.charAt(0) == '-'; + from = 1; + } + long value = 0; + if (text.length() == from || text.length() - from > 10) { + throw new MalformedURLException("For input string: \"" + text + "\""); + } + for (int i = from; i < text.length(); i++) { + char c = text.charAt(i); + if (c < '0' || c > '9') { + throw new MalformedURLException("For input string: \"" + text + "\""); + } + value = value * 10 + (c - '0'); + } + if (negative) { + value = -value; + } + if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) { + throw new MalformedURLException("For input string: \"" + text + "\""); + } + if (value < -1) { + throw new MalformedURLException("Invalid port number :" + value); + } + return (int) value; + } + + /// Resolves the `.` and `..` segments a relative reference brought into + /// a path. + private static String removeDots(String in) { + String path = in; + int i = path.indexOf("/./"); + while (i >= 0) { + path = path.substring(0, i) + path.substring(i + 2); + i = path.indexOf("/./"); + } + i = path.indexOf("/../"); + while (i >= 0) { + // "/a/b/../c" becomes "/a/c"; "/../../a" has nothing to cancel + // and stays. + int previous = i > 0 ? path.lastIndexOf('/', i - 1) : -1; + if (previous >= 0 && path.indexOf("/../", previous) != 0) { + path = path.substring(0, previous) + path.substring(i + 3); + i = path.indexOf("/../"); + } else { + i = path.indexOf("/../", i + 3); + } + } + while (path.endsWith("/..")) { + i = path.indexOf("/.."); + int previous = i > 0 ? path.lastIndexOf('/', i - 1) : -1; + if (previous < 0) { + break; + } + path = path.substring(0, previous + 1); + } + if (path.startsWith("./") && path.length() > 2) { + path = path.substring(2); + } + if (path.endsWith("/.")) { + path = path.substring(0, path.length() - 1); + } + return path; + } + + /// Lower case for the ASCII letters only. A scheme is ASCII by + /// specification, and `String.toLowerCase()` follows the device's locale. + private static String asciiLower(String s) { + StringBuilder sb = null; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c >= 'A' && c <= 'Z') { + if (sb == null) { + sb = new StringBuilder(s); + } + sb.setCharAt(i, (char) (c + ('a' - 'A'))); + } + } + return sb == null ? s : sb.toString(); + } + + private static boolean isAsciiLetter(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + private static boolean isValidProtocol(String protocol) { + int len = protocol.length(); + if (len < 1 || !isAsciiLetter(protocol.charAt(0))) { + return false; + } + for (int i = 1; i < len; i++) { + char c = protocol.charAt(i); + boolean digit = c >= '0' && c <= '9'; + if (!isAsciiLetter(c) && !digit && c != '.' && c != '+' && c != '-') { + return false; + } + } + return true; + } + + private static boolean isKnownProtocol(String protocol) { + return "http".equals(protocol) || "https".equals(protocol) || "file".equals(protocol) + || "cn1res".equals(protocol) || "ftp".equals(protocol) || "jar".equals(protocol) + || "mailto".equals(protocol); + } + + public String getProtocol() { + return protocol; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public int getDefaultPort() { + if ("http".equals(protocol)) { + return 80; + } + if ("https".equals(protocol)) { + return 443; + } + if ("ftp".equals(protocol)) { + return 21; + } + return -1; + } + + public String getAuthority() { + return authority; + } + + public String getUserInfo() { + return userInfo; + } + + public String getPath() { + return path; + } + + public String getQuery() { + return query; + } + + public String getRef() { + return ref; + } + + public String getFile() { + return query == null ? path : path + "?" + query; + } + + public String toExternalForm() { + StringBuilder sb = new StringBuilder(); + sb.append(protocol).append(':'); + if (authority != null && authority.length() > 0) { + sb.append("//").append(authority); + } + if (path != null) { + sb.append(path); + } + if (query != null) { + sb.append('?').append(query); + } + if (ref != null) { + sb.append('#').append(ref); + } + return sb.toString(); + } + + @Override + public String toString() { + return toExternalForm(); + } + + public URI toURI() throws URISyntaxException { + return new URI(toString()); + } + + private static boolean same(String a, String b) { + return a == null ? b == null : a.equals(b); + } + + private int effectivePort() { + return port != -1 ? port : getDefaultPort(); + } + + /// Whether the two URLs name the same resource, the fragment aside. + public boolean sameFile(URL other) { + if (other == null) { + return false; + } + boolean hostsEqual = host == null || other.host == null + ? host == null && other.host == null + : host.equalsIgnoreCase(other.host); + return protocol.equals(other.protocol) && same(getFile(), other.getFile()) + && effectivePort() == other.effectivePort() && hostsEqual; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof URL)) { + return false; + } + URL other = (URL) obj; + return same(ref, other.ref) && sameFile(other); + } + + @Override + public int hashCode() { + int h = protocol.hashCode(); + if (host != null) { + h += asciiLower(host).hashCode(); + } + String file = getFile(); + if (file != null) { + h += file.hashCode(); + } + h += effectivePort(); + if (ref != null) { + h += ref.hashCode(); + } + return h; + } + + private static int hex(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return -1; + } + + /// Replaces each `%XX` by the byte it stands for and reads the result + /// as UTF-8. A `%` not followed by two hex digits is kept. + private static String percentDecode(String s) throws IOException { + if (s.indexOf('%') < 0) { + return s; + } + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int i = 0; + while (i < s.length()) { + if (s.charAt(i) == '%' && i + 2 < s.length() && hex(s.charAt(i + 1)) >= 0 + && hex(s.charAt(i + 2)) >= 0) { + bytes.write((hex(s.charAt(i + 1)) << 4) | hex(s.charAt(i + 2))); + i += 3; + } else { + // Up to the next escape in one piece, so that a surrogate + // pair is encoded as the one character it is. + int next = s.indexOf('%', i + 1); + if (next < 0) { + next = s.length(); + } + bytes.write(s.substring(i, next).getBytes("UTF-8")); + i = next; + } + } + return new String(bytes.toByteArray(), "UTF-8"); + } + + /// Opens the resource for reading; see the class description for what + /// each protocol reads. + /// + /// #### Throws + /// + /// - `FileNotFoundException`: if the file or resource does not exist, or + /// the server answers 404 or 410 + /// + /// - `IOException`: if the request fails, the server answers another + /// error status, or the protocol cannot be read on the device + public InputStream openStream() throws IOException { + if ("file".equals(protocol)) { + return new FileInputStream(new File(percentDecode(path))); + } + if ("cn1res".equals(protocol)) { + String name = percentDecode(path); + if (!name.startsWith("/")) { + name = "/" + name; + } + InputStream in = URL.class.getResourceAsStream(name); + if (in == null) { + throw new FileNotFoundException(toExternalForm()); + } + return in; + } + if ("http".equals(protocol) || "https".equals(protocol)) { + return openHttp(); + } + throw new IOException("Cannot read the " + protocol + " protocol: " + toExternalForm()); + } + + private InputStream openHttp() throws IOException { + StringBuilder address = new StringBuilder(); + address.append(protocol).append("://").append(authority == null ? "" : authority).append(path); + if (query != null) { + address.append('?').append(query); + } + ConnectionRequest request = new ConnectionRequest(); + request.setUrl(address.toString()); + request.setPost(false); + // Failures are reported to the caller as exceptions, not to the user + // as the default error dialog. + request.setFailSilently(true); + NetworkManager.getInstance().addToQueueAndWait(request); + int code = request.getResponseCode(); + if (code == 404 || code == 410) { + throw new FileNotFoundException(toExternalForm()); + } + if (code >= 400) { + throw new IOException("Server returned HTTP response code: " + code + " for URL: " + toExternalForm()); + } + byte[] data = request.getResponseData(); + if (data == null) { + throw new IOException("No response from " + toExternalForm()); + } + return new ByteArrayInputStream(data); + } +} diff --git a/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/package-info.java b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/package-info.java new file mode 100644 index 00000000000..4a1d573d0bc --- /dev/null +++ b/maven/compat-jdk/src/main/java/com/codename1/compat/jdk/package-info.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// JDK classes that code written for another platform expects and the +/// Codename One device runtime does not have, implemented over what the +/// device does have. +/// +/// Every class here has the simple name and the public API of the JDK class +/// it stands in for. Nothing refers to these classes by this package name: the +/// build's remap step rewrites an application's references to the JDK names +/// (`java.io.File`, `java.util.Optional`, `java.text.DecimalFormat`, +/// `java.net.URL`, ...) so that they point here, and copies the classes into +/// the application. +/// +/// Three groups live in the package: the file and buffered stream classes of +/// `java.io`, the `java.util` and `java.util.concurrent` classes the device +/// library leaves out (`Optional`, `ResourceBundle`, `ConcurrentHashMap`, +/// `CopyOnWriteArrayList`, ...), and the number and message formats of +/// `java.text` together with `java.net.URL`. +/// +/// Codename One runs user interface code on one thread. The classes named +/// after `java.util.concurrent` keep the API and the iteration guarantees of +/// the originals and do not synchronize; see each class. +package com.codename1.compat.jdk; diff --git a/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/DecimalFormatDifferentialTest.java b/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/DecimalFormatDifferentialTest.java new file mode 100644 index 00000000000..606b582727e --- /dev/null +++ b/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/DecimalFormatDifferentialTest.java @@ -0,0 +1,513 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/// Runs the same patterns and values through this package's `DecimalFormat` +/// and the JDK's, and requires the same text, the same parsed numbers and the +/// same parse positions. +public class DecimalFormatDifferentialTest { + + private static final Locale US = new Locale("en", "US"); + private static final String CURRENCY = String.valueOf((char) 0xA4); + private static final String PER_MILLE = String.valueOf((char) 0x2030); + + private static final String[] PATTERNS = { + "0", "#", "0.0", "0.00", "#.##", "#.00", "#,##0.00", "#,###", "#,##0.###", "000.000", + "0.###E0", "00.###E0", "##0.#####E0", "0.00E00", "##0.##E0", "#E0", "0E0", "000E0", "0.0E0", + "#,##0.00E0", "###0.###E00", + "#%", "0.0%", "#,##0%", "#" + PER_MILLE, "0.00" + PER_MILLE, + "#,##0.00;(#,##0.00)", "0.0;0.0-", "-0;+0", "+0.0;-0.0", + "'#'#", "$#,##0.00", "0.0 'units'", "'x'''0", "0 'o''clock'", "EUR 0.00", "0.0 kg", + "#,####.##", "#,##,##0.0", "#,#", "0.#", ".00", "#.", "0.", "000", ".##", "#.0#", + CURRENCY + "#,##0.00", CURRENCY + CURRENCY + " 0.00", "#,##0.00 " + CURRENCY, + "0.0000000000", "#.##########", "0.000000000000000000", "00000000000000000000.0", + "", "abc", "'%'0", "0'%'", "a0b;c0d", "#,##0.###;-#", + }; + + private static final double[] DOUBLES = { + 0, -0.0, 1, -1, 0.5, -0.5, 1.5, 2.5, 3.5, 0.125, 0.135, 0.145, 0.15, 0.25, 0.35, 0.45, 0.05, + 0.005, 0.015, 0.025, 1.005, 1.015, 1.025, 2.675, 4.35, 8.345, 9.995, 99.995, 0.999, 0.9999, + 999.9995, 1234.5678, -1234.5678, 0.001, 0.0005, 0.00049, 0.00051, 1e-7, 1e-10, 123456789.123, + 1e15, 1e16, 1e17, 1e20, 1e22, 1e23, 1.23e25, 12345, 100, 1000, 999, 999.5, 9999.5, 0.1 + 0.2, + 1.0 / 3, 2.0 / 3, 10.0 / 3, 1e3, 1e6, 123456.789, 0.000123456, 1.5e-5, 2.5e-5, 6.02214076e23, + Double.MAX_VALUE, Double.MIN_VALUE, 5e-324, Double.NaN, Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, 0.5000000000000001, 0.49999999999999994, 1.0000000000000002, + 4503599627370496.5, 9007199254740993.0, 72057594037927936.0, -0.001, -0.004, -0.006, + 0.285, 1.115, 5.015, 1.45, 1.55, 1.65, 10.5, 11.5, 0.3, 0.7, 1e-320, + }; + + private static final long[] LONGS = { + 0, 1, -1, 9, 10, 12, 99, 100, 999, 1000, 1234567, -987654321, 1500, 2500, 12345, 15, 25, 125, + Long.MAX_VALUE, Long.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, 1000000000000L, + }; + + private static DecimalFormat shim(String pattern) { + return new DecimalFormat(pattern, new DecimalFormatSymbols(US)); + } + + private static java.text.DecimalFormat jdk(String pattern) { + return new java.text.DecimalFormat(pattern, new java.text.DecimalFormatSymbols(US)); + } + + private static void report(List failures) { + if (!failures.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(failures.size()).append(" difference(s) from the JDK:\n"); + for (int i = 0; i < failures.size() && i < 60; i++) { + sb.append(failures.get(i)).append('\n'); + } + fail(sb.toString()); + } + } + + @Test + public void formatsDoublesLikeTheJdk() { + List failures = new ArrayList(); + for (String pattern : PATTERNS) { + DecimalFormat mine = shim(pattern); + java.text.DecimalFormat theirs = jdk(pattern); + for (double value : DOUBLES) { + String expected = theirs.format(value); + String actual = mine.format(value); + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] " + value + ": jdk=" + expected + " shim=" + actual); + } + } + } + report(failures); + } + + @Test + public void formatsLongsLikeTheJdk() { + List failures = new ArrayList(); + for (String pattern : PATTERNS) { + DecimalFormat mine = shim(pattern); + java.text.DecimalFormat theirs = jdk(pattern); + for (long value : LONGS) { + String expected = theirs.format(value); + String actual = mine.format(value); + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] " + value + "L: jdk=" + expected + " shim=" + actual); + } + } + } + report(failures); + } + + /// Random values across the whole exponent range, and values built to sit + /// on or next to a rounding boundary, where half-even and the binary + /// representation both matter. + @Test + public void formatsRandomDoublesLikeTheJdk() { + String[] patterns = { + "0", "0.0", "0.00", "0.000", "#.####", "#,##0.00", "0.###E0", "##0.##E0", "0.00%", "0.0000000", + }; + Random random = new Random(20240607L); + List failures = new ArrayList(); + for (String pattern : patterns) { + DecimalFormat mine = shim(pattern); + java.text.DecimalFormat theirs = jdk(pattern); + for (int i = 0; i < 4000; i++) { + double value; + int kind = i % 4; + if (kind == 0) { + value = Double.longBitsToDouble(random.nextLong()); + } else if (kind == 1) { + value = (random.nextDouble() - 0.5) * 2000; + } else if (kind == 2) { + // k / 1000 and k / 8: decimal halves, some exact in binary. + value = random.nextInt(200000) / (i % 8 < 4 ? 1000.0 : 8.0); + } else { + value = (random.nextInt(2000) + 0.5) / Math.pow(10, random.nextInt(6)); + } + String expected = theirs.format(value); + String actual = mine.format(value); + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] " + value + ": jdk=" + expected + " shim=" + actual); + } + } + } + report(failures); + } + + @Test + public void reproducesPatternsLikeTheJdk() { + List failures = new ArrayList(); + for (String pattern : PATTERNS) { + if (pattern.length() == 0) { + // The empty pattern allows any number of fraction digits, and + // the JDK runs out of memory writing a '#' for each. + continue; + } + String expected = jdk(pattern).toPattern(); + String actual = shim(pattern).toPattern(); + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] toPattern: jdk=" + expected + " shim=" + actual); + } + } + report(failures); + } + + @Test + public void readsPatternSettingsLikeTheJdk() { + List failures = new ArrayList(); + for (String pattern : PATTERNS) { + java.text.DecimalFormat t = jdk(pattern); + DecimalFormat m = shim(pattern); + String expected = t.getMinimumIntegerDigits() + "/" + t.getMaximumIntegerDigits() + "/" + + t.getMinimumFractionDigits() + "/" + t.getMaximumFractionDigits() + "/" + + t.isGroupingUsed() + "/" + t.getGroupingSize() + "/" + t.getMultiplier() + "/" + + t.isDecimalSeparatorAlwaysShown() + "/" + t.getPositivePrefix() + "/" + + t.getPositiveSuffix() + "/" + t.getNegativePrefix() + "/" + t.getNegativeSuffix(); + String actual = m.getMinimumIntegerDigits() + "/" + m.getMaximumIntegerDigits() + "/" + + m.getMinimumFractionDigits() + "/" + m.getMaximumFractionDigits() + "/" + + m.isGroupingUsed() + "/" + m.getGroupingSize() + "/" + m.getMultiplier() + "/" + + m.isDecimalSeparatorAlwaysShown() + "/" + m.getPositivePrefix() + "/" + + m.getPositiveSuffix() + "/" + m.getNegativePrefix() + "/" + m.getNegativeSuffix(); + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] settings: jdk=" + expected + " shim=" + actual); + } + } + report(failures); + } + + @Test + public void rejectsTheMalformedPatternsTheJdkRejects() { + String[] bad = { + "0.0.0", "#0#", "0#.0", "0.#0", "0E", "E0", "0E0E0", "#,", "0;0;0", "0'", "%%0", "0;", + "0.0#0", "#,##0.00;", ";0", "0.0x0", "0x0;y0z0", "0%;0%%", "0E0x0", "0.0x#", + }; + List failures = new ArrayList(); + for (String pattern : bad) { + boolean jdkRejects = false; + boolean shimRejects = false; + try { + jdk(pattern); + } catch (IllegalArgumentException e) { + jdkRejects = true; + } + try { + shim(pattern); + } catch (IllegalArgumentException e) { + shimRejects = true; + } + if (jdkRejects != shimRejects) { + failures.add("[" + pattern + "] jdk rejects=" + jdkRejects + " shim rejects=" + shimRejects); + } + if (!jdkRejects && !shimRejects) { + // Accepted by both: then it has to mean the same thing. + String expected = jdk(pattern).toPattern() + " " + jdk(pattern).format(-1234.5678); + String actual = shim(pattern).toPattern() + " " + shim(pattern).format(-1234.5678); + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] jdk=" + expected + " shim=" + actual); + } + } + } + report(failures); + } + + private static String describe(Number n, int index, int errorIndex) { + String value = n == null ? "null" : n.getClass().getSimpleName() + ":" + n; + return value + " index=" + index + " error=" + errorIndex; + } + + @Test + public void parsesLikeTheJdk() { + String[] patterns = { + "#,##0.###", "0.00", "#", "#%", "0.0%", "#,##0.00;(#,##0.00)", "$#,##0.00", "0.###E0", + "0 'units'", "#" + PER_MILLE, "-0;+0", "#,####.##", + }; + String[] texts = { + "0", "1", "12", "-12", "1,234", "1,234.5", "1,234.50", "-1,234.5", "12.0", "12.50", ".5", "0.5", + "-0", "-0.0", "1.", "1,", "1,234,", "1,,2", "abc", "", " 1", "1 ", "1abc", "1.2.3", "--1", + "50%", "12.5%", "200%", "150.0%", "-5%", "(1,234.50)", "(12", "$5.00", "-$5.00", "$", "$x", + "1E3", "1.5E3", "1E-3", "1E", "1Ex", "-1.25E2", "1e3", "5 units", "5 unit", "5", "+5", "-5", + "9223372036854775807", "9223372036854775808", "-9223372036854775808", "-9223372036854775809", + "123456789012345678901234567890", "0.000", "000123", "00.50", "1" + PER_MILLE, + "1500" + PER_MILLE, "1,2,3", "1.5E400", "1.0E-400", "0E5", "1,234E2", + String.valueOf((char) 0x221E), "-" + (char) 0x221E, String.valueOf((char) 0xFFFD), "12,34.5", + }; + List failures = new ArrayList(); + for (String pattern : patterns) { + for (int integerOnly = 0; integerOnly < 2; integerOnly++) { + DecimalFormat mine = shim(pattern); + java.text.DecimalFormat theirs = jdk(pattern); + mine.setParseIntegerOnly(integerOnly == 1); + theirs.setParseIntegerOnly(integerOnly == 1); + for (String text : texts) { + for (int from = 0; from < 2; from++) { + String input = from == 0 ? text : "x" + text; + java.text.ParsePosition theirPosition = new java.text.ParsePosition(from); + ParsePosition myPosition = new ParsePosition(from); + Number expected = theirs.parse(input, theirPosition); + Number actual = mine.parse(input, myPosition); + String e = describe(expected, theirPosition.getIndex(), theirPosition.getErrorIndex()); + String a = describe(actual, myPosition.getIndex(), myPosition.getErrorIndex()); + if (!e.equals(a)) { + failures.add("[" + pattern + (integerOnly == 1 ? ",integerOnly" : "") + "] \"" + + input + "\"@" + from + ": jdk=" + e + " shim=" + a); + } + } + } + } + } + report(failures); + } + + @Test + public void parseOfAStringThrowsWhereTheJdkThrows() throws Exception { + assertEquals(java.text.NumberFormat.getInstance(Locale.US).parse("1,234.5"), + NumberFormat.getInstance(US).parse("1,234.5")); + try { + NumberFormat.getInstance(US).parse("abc"); + fail("expected a ParseException"); + } catch (java.text.ParseException e) { + assertEquals(0, e.getErrorOffset()); + assertEquals("Unparseable number: \"abc\"", e.getMessage()); + } + } + + @Test + public void factoriesFormatLikeTheJdk() { + double[] values = {0, 1, -1, 1234567.891, 0.5, 0.125, 0.126, -0.5, 1.5, 2.5, 1e10, 0.0049, 12.345}; + List failures = new ArrayList(); + for (double v : values) { + compare(failures, "getInstance", v, java.text.NumberFormat.getInstance(Locale.US).format(v), + NumberFormat.getInstance(US).format(v)); + compare(failures, "getNumberInstance", v, java.text.NumberFormat.getNumberInstance(Locale.US).format(v), + NumberFormat.getNumberInstance(US).format(v)); + compare(failures, "getIntegerInstance", v, + java.text.NumberFormat.getIntegerInstance(Locale.US).format(v), + NumberFormat.getIntegerInstance(US).format(v)); + compare(failures, "getPercentInstance", v, + java.text.NumberFormat.getPercentInstance(Locale.US).format(v), + NumberFormat.getPercentInstance(US).format(v)); + if (v >= 0) { + compare(failures, "getCurrencyInstance", v, + java.text.NumberFormat.getCurrencyInstance(Locale.US).format(v), + NumberFormat.getCurrencyInstance(US).format(v)); + } + } + report(failures); + } + + private static void compare(List failures, String what, double value, String expected, String actual) { + if (!expected.equals(actual)) { + failures.add(what + " " + value + ": jdk=" + expected + " shim=" + actual); + } + } + + /// The one intended difference in the factories. JDK 8's locale data + /// writes a negative US amount in parentheses, `($1.50)`; later JDKs, with + /// CLDR data, write `-$1.50`. The shim follows the current form. + @Test + public void negativeCurrencyUsesAMinusSign() { + assertEquals("-$1.50", NumberFormat.getCurrencyInstance(US).format(-1.5)); + assertEquals("-$1,234.57", NumberFormat.getCurrencyInstance(US).format(-1234.567)); + } + + @Test + public void settersBehaveLikeTheJdk() { + DecimalFormat mine = shim("#,##0.###"); + java.text.DecimalFormat theirs = jdk("#,##0.###"); + mine.setMaximumFractionDigits(1); + theirs.setMaximumFractionDigits(1); + assertEquals(theirs.format(1234.56), mine.format(1234.56)); + mine.setMinimumFractionDigits(4); + theirs.setMinimumFractionDigits(4); + assertEquals(theirs.format(1234.56), mine.format(1234.56)); + assertEquals(theirs.getMaximumFractionDigits(), mine.getMaximumFractionDigits()); + mine.setGroupingUsed(false); + theirs.setGroupingUsed(false); + assertEquals(theirs.format(1234567), mine.format(1234567)); + mine.setMinimumIntegerDigits(10); + theirs.setMinimumIntegerDigits(10); + assertEquals(theirs.format(42), mine.format(42)); + mine.setMaximumIntegerDigits(2); + theirs.setMaximumIntegerDigits(2); + assertEquals(theirs.format(1997), mine.format(1997)); + assertEquals(theirs.format(1997.5), mine.format(1997.5)); + assertEquals(theirs.getMinimumIntegerDigits(), mine.getMinimumIntegerDigits()); + mine.setGroupingUsed(true); + theirs.setGroupingUsed(true); + mine.setGroupingSize(2); + theirs.setGroupingSize(2); + mine.setMaximumIntegerDigits(20); + theirs.setMaximumIntegerDigits(20); + mine.setMinimumIntegerDigits(1); + theirs.setMinimumIntegerDigits(1); + assertEquals(theirs.format(1234567), mine.format(1234567)); + assertEquals(theirs.toPattern(), mine.toPattern()); + mine.setPositivePrefix("+"); + theirs.setPositivePrefix("+"); + mine.setNegativeSuffix(" CR"); + theirs.setNegativeSuffix(" CR"); + mine.setDecimalSeparatorAlwaysShown(true); + theirs.setDecimalSeparatorAlwaysShown(true); + assertEquals(theirs.format(5), mine.format(5)); + assertEquals(theirs.format(-5), mine.format(-5)); + assertEquals(theirs.toPattern(), mine.toPattern()); + mine.setMultiplier(7); + theirs.setMultiplier(7); + assertEquals(theirs.format(6), mine.format(6)); + assertEquals(theirs.format(0.5), mine.format(0.5)); + assertEquals(theirs.format(Long.MAX_VALUE), mine.format(Long.MAX_VALUE)); + mine.setMultiplier(-3); + theirs.setMultiplier(-3); + assertEquals(theirs.format(6), mine.format(6)); + assertEquals(theirs.format(-2.5), mine.format(-2.5)); + } + + @Test + public void formatsObjectsByTheirType() { + NumberFormat mine = NumberFormat.getInstance(US); + java.text.NumberFormat theirs = java.text.NumberFormat.getInstance(Locale.US); + Object[] numbers = { + Integer.valueOf(1234), Long.valueOf(-99999L), Short.valueOf((short) 7), Byte.valueOf((byte) -3), + Double.valueOf(1234.5678), Float.valueOf(0.5f), + }; + for (Object number : numbers) { + assertEquals(theirs.format(number), mine.format(number)); + } + try { + mine.format("not a number"); + fail("expected an IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // The JDK throws the same for a non-number. + } + } + + @Test + public void reportsFieldPositionsLikeTheJdk() { + String[] patterns = {"#,##0.00", "0.###E0", "$0.0#", "#"}; + double[] values = {1234.5, -0.25, 7, 1e9}; + for (String pattern : patterns) { + for (double value : values) { + for (int field = 0; field < 2; field++) { + java.text.FieldPosition theirs = new java.text.FieldPosition(field); + FieldPosition mine = new FieldPosition(field); + String expected = jdk(pattern).format(value, new StringBuffer("ab"), theirs).toString(); + String actual = shim(pattern).format(value, new StringBuffer("ab"), mine).toString(); + assertEquals(expected, actual); + String where = pattern + " " + value + " field " + field; + assertEquals(where, theirs.getBeginIndex(), mine.getBeginIndex()); + assertEquals(where, theirs.getEndIndex(), mine.getEndIndex()); + } + } + } + } + + @Test + public void symbolsChangeTheOutput() { + DecimalFormatSymbols mySymbols = new DecimalFormatSymbols(US); + java.text.DecimalFormatSymbols theirSymbols = new java.text.DecimalFormatSymbols(Locale.US); + mySymbols.setDecimalSeparator(','); + theirSymbols.setDecimalSeparator(','); + mySymbols.setGroupingSeparator('.'); + theirSymbols.setGroupingSeparator('.'); + mySymbols.setMinusSign('~'); + theirSymbols.setMinusSign('~'); + mySymbols.setPercent('P'); + theirSymbols.setPercent('P'); + mySymbols.setCurrencySymbol("EUR"); + theirSymbols.setCurrencySymbol("EUR"); + mySymbols.setExponentSeparator("x10^"); + theirSymbols.setExponentSeparator("x10^"); + mySymbols.setInfinity("inf"); + theirSymbols.setInfinity("inf"); + mySymbols.setNaN("nan"); + theirSymbols.setNaN("nan"); + String[] patterns = {"#,##0.00", "0.0%", CURRENCY + " #,##0.00", "0.##E0"}; + double[] values = {1234567.891, -0.5, 0.000123, Double.NaN, Double.NEGATIVE_INFINITY}; + for (String pattern : patterns) { + DecimalFormat mine = new DecimalFormat(pattern, mySymbols); + java.text.DecimalFormat theirs = new java.text.DecimalFormat(pattern, theirSymbols); + for (double value : values) { + assertEquals(pattern + " " + value, theirs.format(value), mine.format(value)); + } + String text = theirs.format(-1234.5); + assertEquals(pattern, theirs.parse(text, new java.text.ParsePosition(0)), + mine.parse(text, new ParsePosition(0))); + } + // The format holds its own copy, as the JDK's does. + DecimalFormat mine = new DecimalFormat("0.0", mySymbols); + mySymbols.setDecimalSeparator('!'); + assertEquals("1,5", mine.format(1.5)); + DecimalFormatSymbols handedOut = mine.getDecimalFormatSymbols(); + handedOut.setDecimalSeparator('?'); + assertEquals("1,5", mine.format(1.5)); + mine.setDecimalFormatSymbols(handedOut); + assertEquals("1?5", mine.format(1.5)); + } + + @Test + public void cloneIsAnIndependentEqualCopy() { + DecimalFormat original = shim("#,##0.00;(#)"); + Object copy = original.clone(); + assertTrue(copy instanceof DecimalFormat); + assertNotSame(original, copy); + assertEquals(original, copy); + assertEquals(original.hashCode(), copy.hashCode()); + DecimalFormat cloned = (DecimalFormat) copy; + assertEquals(original.format(-1234.5), cloned.format(-1234.5)); + assertEquals(original.toPattern(), cloned.toPattern()); + cloned.setMaximumFractionDigits(0); + assertFalse(original.equals(cloned)); + assertEquals("1,234.50", original.format(1234.5)); + Object symbolsCopy = new DecimalFormatSymbols(US).clone(); + assertEquals(new DecimalFormatSymbols(US), symbolsCopy); + } + + @Test + public void parseFailureLeavesThePositionAlone() { + ParsePosition position = new ParsePosition(2); + assertNull(shim("0.00").parse("ab?", position)); + assertEquals(2, position.getIndex()); + assertEquals(2, position.getErrorIndex()); + } + + /// Before Codename One is initialized there is no `L10NManager` to ask, + /// and every locale gets the US English symbols. + @Test + public void symbolsFallBackToUsEnglishWithoutADisplay() { + DecimalFormatSymbols symbols = new DecimalFormatSymbols(new Locale("fr", "FR")); + assertEquals('.', symbols.getDecimalSeparator()); + assertEquals(',', symbols.getGroupingSeparator()); + assertEquals("$", symbols.getCurrencySymbol()); + assertEquals(new DecimalFormatSymbols(US), symbols); + } +} diff --git a/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/MessageFormatDifferentialTest.java b/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/MessageFormatDifferentialTest.java new file mode 100644 index 00000000000..85005779a5e --- /dev/null +++ b/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/MessageFormatDifferentialTest.java @@ -0,0 +1,360 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; + +/// Holds the `MessageFormat` and `ChoiceFormat` shims against the JDK's own +/// classes: the same pattern and arguments have to give the same text. +public class MessageFormatDifferentialTest { + + private Locale saved; + + @Before + public void useUsLocale() { + saved = Locale.getDefault(); + Locale.setDefault(Locale.US); + } + + @After + public void restoreLocale() { + Locale.setDefault(saved); + } + + private static final String[] PATTERNS = { + "plain text", + "{0}", + "Hello {0}, you have {1} messages", + "{1} before {0}", + "{0}{0}{0}", + "'{0}' is literal, {0} is not", + "It''s {0}", + "'quoted '' quote' {0}", + "a '{' b {0} c '}' d", + "{0,number}", + "{0,number,integer}", + "{0,number,percent}", + "{0,number,currency}", + "{0,number,#.##}", + "{0,number,000.0}", + "{0,number,#,##0.00;(#,##0.00)}", + "{0, number , integer }", + "{0,choice,0#no files|1#one file|1<{0,number,integer} files}", + "{0,choice,-1#negative|0#zero or more}", + "missing {5} argument", + "{2}", + "trailing quote '", + "{0} and '{1}' and {1}", + "{0,number,'#'#}", + "", + }; + + private static final Object[][] ARGUMENTS = { + {}, + {"World", Integer.valueOf(3)}, + {Integer.valueOf(0), "x"}, + {Integer.valueOf(1), "x"}, + {Integer.valueOf(2), "x"}, + {Double.valueOf(1234.5678), Long.valueOf(-7)}, + {Double.valueOf(-0.256), null}, + {null, null, null}, + {Long.valueOf(1234567), "it's", Boolean.TRUE}, + {"text", "more"}, + {Integer.valueOf(-5)}, + }; + + @Test + public void formatsLikeTheJdk() { + List failures = new ArrayList(); + for (String pattern : PATTERNS) { + for (Object[] arguments : ARGUMENTS) { + String expected; + try { + expected = new java.text.MessageFormat(pattern, Locale.US).format(arguments); + } catch (IllegalArgumentException e) { + expected = "IllegalArgumentException"; + } + String actual; + try { + actual = new MessageFormat(pattern, Locale.US).format(arguments); + } catch (IllegalArgumentException e) { + actual = "IllegalArgumentException"; + } + if (pattern.indexOf("currency") >= 0 && expected.startsWith("(")) { + // Intended difference. A JDK reading its own locale + // data writes a negative amount in parentheses; the shim + // writes the minus sign that current locale data asks + // for, and that newer JDKs write too. + expected = "-" + expected.substring(1, expected.length() - 1); + } + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] " + Arrays.toString(arguments) + ": jdk=" + expected + + " shim=" + actual); + } + } + } + report(failures); + } + + @Test + public void staticFormatMatchesTheJdk() { + assertEquals(java.text.MessageFormat.format("{0} of {1}", "a", Integer.valueOf(12345)), + MessageFormat.format("{0} of {1}", "a", Integer.valueOf(12345))); + assertEquals(java.text.MessageFormat.format("no arguments", new Object[0]), + MessageFormat.format("no arguments", new Object[0])); + assertEquals(java.text.MessageFormat.format("{0}", (Object[]) null), + MessageFormat.format("{0}", (Object[]) null)); + } + + @Test + public void rejectsWhatTheJdkRejects() { + String[] patterns = { + "{", "{0", "{x}", "{-1}", "{0,bogus}", "{0,number,#.#.#}", "{0}}", "}", "{ 0 }", "{0,}", "{}", + "{0,number", "{0,choice,1#a|0#b}", "{1.5}", + }; + List failures = new ArrayList(); + for (String pattern : patterns) { + boolean jdkRejects = false; + boolean shimRejects = false; + String expected = null; + String actual = null; + try { + expected = new java.text.MessageFormat(pattern, Locale.US).format(new Object[] {Integer.valueOf(1)}); + } catch (IllegalArgumentException e) { + jdkRejects = true; + } + try { + actual = new MessageFormat(pattern, Locale.US).format(new Object[] {Integer.valueOf(1)}); + } catch (IllegalArgumentException e) { + shimRejects = true; + } + if (jdkRejects != shimRejects) { + failures.add("[" + pattern + "] jdk rejects=" + jdkRejects + " shim rejects=" + shimRejects); + } else if (!jdkRejects && !expected.equals(actual)) { + failures.add("[" + pattern + "] jdk=" + expected + " shim=" + actual); + } + } + report(failures); + } + + @Test + public void reproducesPatternsLikeTheJdk() { + List failures = new ArrayList(); + for (String pattern : PATTERNS) { + String expected = new java.text.MessageFormat(pattern, Locale.US).toPattern(); + String actual = new MessageFormat(pattern, Locale.US).toPattern(); + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] toPattern: jdk=" + expected + " shim=" + actual); + } + } + report(failures); + } + + @Test + public void formatsDatesLikeTheJdk() { + Date when = new Date(1700000000000L); + String[] patterns = { + "{0,date}", "{0,date,short}", "{0,date,medium}", "{0,date,long}", "{0,time}", "{0,time,short}", + "{0,date,yyyy-MM-dd}", "{0,time,HH:mm}", "{0}", "{0, date, full}", "{0,time,long}", + }; + List failures = new ArrayList(); + for (String pattern : patterns) { + String expected = new java.text.MessageFormat(pattern, Locale.US).format(new Object[] {when}); + String actual = new MessageFormat(pattern, Locale.US).format(new Object[] {when}); + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] jdk=" + expected + " shim=" + actual); + } + expected = new java.text.MessageFormat(pattern, Locale.US).toPattern(); + actual = new MessageFormat(pattern, Locale.US).toPattern(); + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] toPattern: jdk=" + expected + " shim=" + actual); + } + } + report(failures); + } + + @Test + public void appendsAndComparesLikeAFormat() { + MessageFormat format = new MessageFormat("{0}-{1}"); + StringBuffer out = new StringBuffer("> "); + format.format(new Object[] {"a", "b"}, out, new FieldPosition(0)); + assertEquals("> a-b", out.toString()); + assertEquals(format, new MessageFormat("{0}-{1}")); + assertEquals(format.hashCode(), new MessageFormat("{0}-{1}").hashCode()); + assertNotEquals(format, new MessageFormat("{0}+{1}")); + Object copy = format.clone(); + assertEquals(format, copy); + format.applyPattern("{0}"); + assertNotEquals(format, copy); + } + + // ------------------------------------------------------------------ + // ChoiceFormat + // ------------------------------------------------------------------ + + // Written as casts: the sources are ASCII. + private static final char INFINITY = (char) 0x221E; + private static final char AT_LEAST = (char) 0x2264; + + private static final String[] CHOICES = { + "0#zero|1#one|2#two", + "0#no files|1#one file|1 failures = new ArrayList(); + for (String pattern : CHOICES) { + java.text.ChoiceFormat theirs = new java.text.ChoiceFormat(pattern); + ChoiceFormat mine = new ChoiceFormat(pattern); + for (double value : CHOICE_VALUES) { + String expected = theirs.format(value); + String actual = mine.format(value); + if (!expected.equals(actual)) { + failures.add("[" + pattern + "] " + value + ": jdk=" + expected + " shim=" + actual); + } + } + if (!theirs.format(3L).equals(mine.format(3L))) { + failures.add("[" + pattern + "] 3L"); + } + if (!theirs.toPattern().equals(mine.toPattern())) { + failures.add("[" + pattern + "] toPattern: jdk=" + theirs.toPattern() + " shim=" + mine.toPattern()); + } + if (!Arrays.equals(theirs.getLimits(), mine.getLimits())) { + failures.add("[" + pattern + "] limits: jdk=" + Arrays.toString(theirs.getLimits()) + " shim=" + + Arrays.toString(mine.getLimits())); + } + if (!Arrays.equals(theirs.getFormats(), mine.getFormats())) { + failures.add("[" + pattern + "] formats"); + } + } + report(failures); + } + + @Test + public void choiceFromArraysMatchesTheJdk() { + double[] limits = {0, 1, java.text.ChoiceFormat.nextDouble(1)}; + String[] formats = {"none", "single", "several"}; + java.text.ChoiceFormat theirs = new java.text.ChoiceFormat(limits, formats); + ChoiceFormat mine = new ChoiceFormat(limits, formats); + assertEquals(theirs.toPattern(), mine.toPattern()); + for (double value : CHOICE_VALUES) { + assertEquals(theirs.format(value), mine.format(value)); + } + assertArrayEquals(theirs.getLimits(), mine.getLimits(), 0.0); + try { + new ChoiceFormat(new double[] {0, 1}, new String[] {"a"}); + fail("limits and formats of different lengths"); + } catch (IllegalArgumentException expected) { + // As the JDK. + } + } + + @Test + public void choiceParsesLikeTheJdk() { + String pattern = "0#zero|1#one|2#two|2 failures) { + if (failures.isEmpty()) { + return; + } + StringBuilder sb = new StringBuilder(); + sb.append(failures.size()).append(" differences from the JDK:\n"); + for (int i = 0; i < failures.size() && i < 60; i++) { + sb.append(failures.get(i)).append('\n'); + } + fail(sb.toString()); + } +} diff --git a/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/ResourceBundleDifferentialTest.java b/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/ResourceBundleDifferentialTest.java new file mode 100644 index 00000000000..2baca385de2 --- /dev/null +++ b/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/ResourceBundleDifferentialTest.java @@ -0,0 +1,496 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Random; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/// Holds the `.properties` parser against `java.util.Properties`, and the +/// bundle lookup against `java.util.ResourceBundle` reading the same files. +public class ResourceBundleDifferentialTest { + + private static final String BASE = "com.codename1.compat.jdk.bundles.Messages"; + + // A backslash, kept out of string literals next to a 'u': javac reads + // that pair as an escape wherever it appears. + private static final String BS = String.valueOf((char) 92); + + private Locale saved; + + @Before + public void useUsLocale() { + saved = Locale.getDefault(); + Locale.setDefault(Locale.US); + ResourceBundle.clearCache(); + java.util.ResourceBundle.clearCache(); + } + + @After + public void restoreLocale() { + Locale.setDefault(saved); + ResourceBundle.clearCache(); + } + + // ------------------------------------------------------------------ + // The .properties format + // ------------------------------------------------------------------ + + private static Map jdkParse(String text) throws IOException { + Properties properties = new Properties(); + properties.load(new StringReader(text)); + Map out = new TreeMap(); + for (String name : properties.stringPropertyNames()) { + out.put(name, properties.getProperty(name)); + } + return out; + } + + private static Map shimParse(String text) { + Map out = new HashMap(); + PropertyResourceBundle.parse(text, out); + return new TreeMap(out); + } + + private static String outcome(String text, boolean jdk) { + try { + return (jdk ? jdkParse(text) : shimParse(text)).toString(); + } catch (IllegalArgumentException e) { + return "IllegalArgumentException"; + } catch (IOException e) { + return "IOException"; + } + } + + @Test + public void parsesPropertiesLikeTheJdk() { + String[] texts = { + "", + "a=b", + "a=b\n", + "a = b", + "a : b", + "a:b", + "a b", + "a\tb", + "a = b ", + " a=b", + "\t a=b", + "a", + "a=", + "=b", + ":b", + " = b", + "# comment\na=b", + "! comment\na=b", + " # indented comment\na=b", + "a=b # not a comment", + "a=b\r\nc=d\re=f\ng=h", + "a=b\n\n\nc=d", + "a=first\na=second", + "long=one \\\n two \\\n three", + "long=one\\\r\n two", + "odd=ends with two \\\\\nnext=1", + "odd=ends with three \\\\\\\nstill the value", + "key\\ with\\ spaces=value", + "key\\=with\\:separators=value", + "key\\", + "a=b\\", + "tabs=\\ta\\nb\\rc\\fd", + "unknown=\\q\\z\\'\\\"", + "unicode=" + BS + "u0041" + BS + "u00e9" + BS + "u4E2D", + "unicodeKey" + BS + "u0020x=1", + "lower=" + BS + "u00ff" + BS + "u00FF", + "# a comment does not continue \\\na=b", + "a=b\n \n \t\nc=d", + "\\#notcomment=1", + "\\!notcomment=1", + "a==b", + "a=:b", + "a:=b", + "a = = b", + "a\\\n b=c", + "a=b\\\n", + "a=b\\\n\n", + "a=b\\\n# not a comment\n", + "a=\\\n\\\n\\\nb", + "path=c:\\\\dir\\\\file", + "url=http://example.com/a?b=c", + "sp\\ ace : v", + "a\fb=c", + "\fa=b", + "a=" + (char) 0xE9 + (char) 0x4E2D, + "trailing=value\t \f", + "k1=v1\nk2:v2\nk3 v3\n", + }; + List failures = new ArrayList(); + for (String text : texts) { + String expected = outcome(text, true); + String actual = outcome(text, false); + if (!expected.equals(actual)) { + failures.add(show(text) + ": jdk=" + show(expected) + " shim=" + show(actual)); + } + } + report(failures); + } + + @Test + public void rejectsMalformedEscapesLikeTheJdk() { + String[] texts = { + "a=" + BS + "u12", "a=" + BS + "uXYZW", "a=" + BS + "u", "a=" + BS + "u123", BS + "u00G0=1", + "a=" + BS + "u12\nb=2", + }; + for (String text : texts) { + assertEquals(show(text), outcome(text, true), outcome(text, false)); + } + } + + @Test + public void parsesRandomPropertiesLikeTheJdk() { + String[] pieces = { + "a", "b", "key", "value", " ", " ", "\t", "\f", "=", ":", "#", "!", "\\", "\\\\", "\n", "\r", "\r\n", + "\\n", "\\t", "\\ ", "\\=", "\\:", "\\#", BS + "u0041", BS + "u00e9", "x", "1", ".", "\\\n", "\\\r\n", + String.valueOf((char) 0xE9), + }; + Random random = new Random(7L); + List failures = new ArrayList(); + for (int i = 0; i < 20000; i++) { + StringBuilder text = new StringBuilder(); + int n = random.nextInt(14); + for (int j = 0; j < n; j++) { + text.append(pieces[random.nextInt(pieces.length)]); + } + if (commentEndsInBackslash(text.toString())) { + continue; + } + String expected = outcome(text.toString(), true); + String actual = outcome(text.toString(), false); + if (!expected.equals(actual)) { + failures.add(show(text.toString()) + ": jdk=" + show(expected) + " shim=" + show(actual)); + } + } + report(failures); + } + + /// Whether a line that may be a comment ends in a backslash. + /// + /// Intended difference. The JDK's line reader counts backslashes through + /// a comment and does not start again at the next line, so a comment + /// ending in one changes how the following line is read -- to the point + /// of reading characters left in its buffer by earlier lines. A comment + /// cannot be continued in this format, and the shim does not let it + /// reach past its own line. + private static boolean commentEndsInBackslash(String text) { + int lineStart = 0; + for (int i = 0; i <= text.length(); i++) { + if (i < text.length() && text.charAt(i) != '\n' && text.charAt(i) != '\r') { + continue; + } + int first = lineStart; + while (first < i && (text.charAt(first) == ' ' || text.charAt(first) == '\t' + || text.charAt(first) == '\f')) { + first++; + } + if (first < i && (text.charAt(first) == '#' || text.charAt(first) == '!') + && text.charAt(i - 1) == '\\') { + return true; + } + lineStart = i + 1; + } + return false; + } + + @Test + public void commentsDoNotReachTheNextLine() { + // Where the JDK and the shim part ways, pinned down. + assertEquals("{b=1}", shimParse("# comment \\\nb=1").toString()); + assertEquals("{b=1}", shimParse("! comment \\\r\n b=1").toString()); + } + + @Test + public void readsStreamsAsLatin1LikeTheJdk() throws IOException { + byte[] bytes = {'k', '=', (byte) 0xE9, (byte) 0xFF, '\n', 'u', '=', '\\', 'u', '4', 'E', '2', 'D', '\n'}; + Properties properties = new Properties(); + properties.load(new ByteArrayInputStream(bytes)); + PropertyResourceBundle bundle = new PropertyResourceBundle(new ByteArrayInputStream(bytes)); + assertEquals(properties.getProperty("k"), bundle.getString("k")); + assertEquals(properties.getProperty("u"), bundle.getString("u")); + assertEquals(new TreeSet(properties.stringPropertyNames()), new TreeSet(bundle.keySet())); + + PropertyResourceBundle fromReader = new PropertyResourceBundle(new StringReader("a=1\nb=2")); + assertEquals("1", fromReader.getString("a")); + assertEquals("2", fromReader.getObject("b")); + assertTrue(fromReader.containsKey("a")); + assertFalse(fromReader.containsKey("c")); + } + + // ------------------------------------------------------------------ + // Lookup + // ------------------------------------------------------------------ + + private static String describe(java.util.ResourceBundle bundle) { + StringBuilder sb = new StringBuilder(); + for (String key : new TreeSet(bundle.keySet())) { + sb.append(key).append('=').append(bundle.getString(key)).append(';'); + } + return sb.append(" locale=").append(bundle.getLocale()).toString(); + } + + private static String describe(ResourceBundle bundle) { + StringBuilder sb = new StringBuilder(); + for (String key : new TreeSet(bundle.keySet())) { + sb.append(key).append('=').append(bundle.getString(key)).append(';'); + } + Locale locale = bundle.getLocale(); + String name = locale.getLanguage(); + if (locale.getCountry().length() > 0) { + name = name + "_" + locale.getCountry(); + } + return sb.append(" locale=").append(name).toString(); + } + + @Test + public void resolvesLocalesLikeTheJdk() { + Locale[] locales = { + new Locale("", ""), new Locale("fr", ""), new Locale("fr", "CA"), new Locale("fr", "FR"), + new Locale("de", ""), new Locale("de", "DE"), new Locale("en", "US"), new Locale("en", "GB"), + new Locale("es", "MX"), + }; + Locale[] defaults = {Locale.US, new Locale("fr", "CA"), new Locale("fr", "BE"), new Locale("de", "DE")}; + for (Locale def : defaults) { + Locale.setDefault(def); + for (Locale locale : locales) { + ResourceBundle.clearCache(); + java.util.ResourceBundle.clearCache(); + assertEquals("default " + def + ", asked " + locale, + describe(java.util.ResourceBundle.getBundle(BASE, locale)), + describe(ResourceBundle.getBundle(BASE, locale))); + } + ResourceBundle.clearCache(); + java.util.ResourceBundle.clearCache(); + assertEquals(describe(java.util.ResourceBundle.getBundle(BASE)), describe(ResourceBundle.getBundle(BASE))); + } + } + + @Test + public void chainsToLessSpecificBundles() { + ResourceBundle bundle = ResourceBundle.getBundle(BASE, new Locale("fr", "CA")); + assertEquals("Allo", bundle.getString("greeting")); + assertEquals("Au revoir", bundle.getString("farewell")); + assertEquals("fr", bundle.getString("only.fr")); + assertEquals("base", bundle.getString("only.base")); + assertEquals("base", bundle.getObject("only.base")); + assertTrue(bundle.containsKey("only.base")); + assertFalse(bundle.containsKey("absent")); + assertEquals(new TreeSet(java.util.ResourceBundle.getBundle(BASE, new Locale("fr", "CA")).keySet()), + new TreeSet(bundle.keySet())); + List keys = new ArrayList(); + for (Enumeration e = bundle.getKeys(); e.hasMoreElements();) { + keys.add(e.nextElement()); + } + Collections.sort(keys); + assertEquals("[farewell, greeting, only.base, only.fr]", keys.toString()); + // Slashes name the same bundle as dots. + assertSame(bundle, ResourceBundle.getBundle(BASE.replace('.', '/'), new Locale("fr", "CA"))); + // And the lookup is cached until told otherwise. + assertSame(bundle, ResourceBundle.getBundle(BASE, new Locale("fr", "CA"), null)); + ResourceBundle.clearCache(); + assertNotSame(bundle, ResourceBundle.getBundle(BASE, new Locale("fr", "CA"))); + } + + @Test + public void reportsWhatIsMissingLikeTheJdk() { + ResourceBundle bundle = ResourceBundle.getBundle(BASE, new Locale("fr", "")); + try { + bundle.getString("absent"); + fail("absent key"); + } catch (MissingResourceException e) { + try { + java.util.ResourceBundle.getBundle(BASE, new Locale("fr", "")).getString("absent"); + fail("absent key in the JDK"); + } catch (java.util.MissingResourceException theirs) { + assertEquals(theirs.getKey(), e.getKey()); + assertEquals(theirs.getMessage().replace("java.util.PropertyResourceBundle", + PropertyResourceBundle.class.getName()), e.getMessage()); + } + } + try { + ResourceBundle.getBundle("com.codename1.compat.jdk.bundles.Nothing", new Locale("fr", "CA")); + fail("absent bundle"); + } catch (MissingResourceException e) { + try { + java.util.ResourceBundle.getBundle("com.codename1.compat.jdk.bundles.Nothing", new Locale("fr", "CA")); + fail("absent bundle in the JDK"); + } catch (java.util.MissingResourceException theirs) { + assertEquals(theirs.getMessage(), e.getMessage()); + assertEquals(theirs.getClassName(), e.getClassName()); + assertEquals(theirs.getKey(), e.getKey()); + } + } + try { + ResourceBundle.getBundle(null); + fail("null name"); + } catch (NullPointerException expected) { + // As the JDK. + } + // A bundle with no base file exists only for its own language. + assertEquals("nur deutsch", ResourceBundle + .getBundle("com.codename1.compat.jdk.bundles.NoBase", new Locale("de", "AT")).getString("key")); + try { + ResourceBundle.getBundle("com.codename1.compat.jdk.bundles.NoBase", new Locale("fr", "")); + fail("no French and no base"); + } catch (MissingResourceException expected) { + // As the JDK. + } + } + + @Test + public void registeredResourcesComeBeforeThePathLookup() { + String dir = "/com/codename1/compat/jdk/registered/"; + // A name with no file at the conventional path at all. + ResourceBundle.cn1Register("app.Texts", "", dir + "odd-name.txt"); + ResourceBundle.cn1Register("app/Texts", "_es", "com/codename1/compat/jdk/registered/odd-name-es.txt"); + assertEquals("Registered", ResourceBundle.getBundle("app.Texts", new Locale("it", "")).getString("greeting")); + assertEquals("Registrado", ResourceBundle.getBundle("app.Texts", new Locale("es", "MX")).getString("greeting")); + assertEquals("es", ResourceBundle.getBundle("app.Texts", new Locale("es", "MX")).getLocale().getLanguage()); + + // A registration wins over the file the path lookup would find, and + // takes effect although the bundle was already loaded. + assertEquals("Bonjour", ResourceBundle.getBundle(BASE, new Locale("fr", "")).getString("greeting")); + ResourceBundle.cn1Register(BASE, "_fr", dir + "odd-name.txt"); + try { + ResourceBundle french = ResourceBundle.getBundle(BASE, new Locale("fr", "")); + assertEquals("Registered", french.getString("greeting")); + // The parent chain is unaffected. + assertEquals("base", french.getString("only.base")); + } finally { + // Registrations outlive clearCache(); put the real file back. + ResourceBundle.cn1Register(BASE, "_fr", "/com/codename1/compat/jdk/bundles/Messages_fr.properties"); + } + assertEquals("Bonjour", ResourceBundle.getBundle(BASE, new Locale("fr", "")).getString("greeting")); + try { + ResourceBundle.cn1Register(null, "", "x"); + fail("null base name"); + } catch (NullPointerException expected) { + // Nothing to register under. + } + } + + /// A bundle that is a class, found by name like a properties file. + public static class ListBundle extends ListResourceBundle { + @Override + protected Object[][] getContents() { + return new Object[][] { + {"text", "from a class"}, + {"array", new String[] {"one", "two"}}, + {"number", Integer.valueOf(42)}, + }; + } + } + + /// The French one; its parent is the class above. + public static class ListBundle_fr extends ListResourceBundle { + @Override + protected Object[][] getContents() { + return new Object[][] {{"text", "d'une classe"}}; + } + } + + @Test + public void loadsBundleClassesByName() { + String name = ListBundle.class.getName(); + ResourceBundle base = ResourceBundle.getBundle(name, new Locale("it", "")); + assertTrue(base instanceof ListBundle); + assertEquals("from a class", base.getString("text")); + assertArrayEquals(new String[] {"one", "two"}, base.getStringArray("array")); + assertEquals(Integer.valueOf(42), base.getObject("number")); + try { + base.getString("number"); + fail("not a string"); + } catch (ClassCastException expected) { + // As the JDK. Thrown explicitly by the shim, after an instanceof + // test: a failed cast does not throw on every device. + } + ResourceBundle french = ResourceBundle.getBundle(name, new Locale("fr", "FR")); + assertTrue(french instanceof ListBundle_fr); + assertEquals("d'une classe", french.getString("text")); + assertEquals(Integer.valueOf(42), french.getObject("number")); + assertEquals("[array, number, text]", new TreeSet(french.keySet()).toString()); + } + + private static String show(String text) { + StringBuilder sb = new StringBuilder("\""); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '\n') { + sb.append(""); + } else if (c == '\r') { + sb.append(""); + } else if (c == '\t') { + sb.append(""); + } else if (c == '\f') { + sb.append(""); + } else if (c > 126) { + sb.append("<").append(Integer.toHexString(c)).append(">"); + } else { + sb.append(c); + } + } + return sb.append('"').toString(); + } + + private static void report(List failures) { + if (failures.isEmpty()) { + return; + } + StringBuilder sb = new StringBuilder(); + sb.append(failures.size()).append(" differences from the JDK:\n"); + for (int i = 0; i < failures.size() && i < 40; i++) { + sb.append(failures.get(i)).append('\n'); + } + fail(sb.toString()); + } +} diff --git a/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/URLDifferentialTest.java b/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/URLDifferentialTest.java new file mode 100644 index 00000000000..e1bbbeb3fa5 --- /dev/null +++ b/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/URLDifferentialTest.java @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/// Holds the `URL` shim against `java.net.URL`: the same text has to come +/// apart into the same pieces and go back together into the same text. +/// +/// The JDK's `equals` and `hashCode` are left out of the comparison on +/// purpose. They resolve the host name, so two names of one address are +/// equal there; the shim compares the names, as `java.net.URI` does. +public class URLDifferentialTest { + + private static final String[] SPECS = { + "http://example.com", + "http://example.com/", + "http://example.com/a/b/c.html", + "http://example.com:8080/a?x=1&y=2#frag", + "https://user:secret@example.com:8443/path/to?q#r", + "https://user@example.com/", + "HTTP://Example.COM/Path", + "http://example.com?query", + "http://example.com#ref", + "http://example.com/?", + "http://example.com/#", + "http://example.com/a?b#c?d#e", + "http://example.com/a%20b/c+d?e=%2F", + "http://example.com:80/", + "http://example.com:/", + "http://example.com:-1/", + "http://example.com:abc/", + "http://example.com:99999/", + "http://[::1]/", + "http://[::1]:8080/x", + "http://[2001:db8::1]/", + "http://[::1/", + "http://192.168.0.1:8080/", + "http:///path", + "http://", + "http:", + "http:/", + "http:relative/path", + "http:/absolute/path", + "http://example.com/a/../b/./c", + "http://example.com/..", + "http://example.com/a/b/../../../c", + "file:/tmp/a.txt", + "file:///tmp/a.txt", + "file://host/share/a.txt", + "file:a.txt", + "file:/C:/Users/x", + "file:///", + "file:", + "file:/a b", + "ftp://ftp.example.com/pub/file.zip", + "ftp://anonymous:pw@ftp.example.com:2121/pub", + "jar:file:/a/b.jar!/c/d.class", + "jar:http://example.com/a.jar!/", + " http://example.com/trimmed ", + "url:http://example.com/prefixed", + "URL:http://example.com/prefixed", + "", + "no-protocol", + "/just/a/path", + "://missing", + "1http://example.com", + "ht tp://example.com", + "unknown://example.com/", + "mailto:someone@example.com", + "http://example.com/a;params?q", + "http://example.com/a#", + "http://exa mple.com/", + "http://example.com/" + (char) 0xE9, + "http://@example.com/", + "http://user:@example.com/", + "http://:pw@example.com/", + "http://a@b@example.com/", + "http://example.com/a?b?c", + "http://example.com//double//slash", + "http:///", + "https://example.com:443", + "http://example.com:0/", + "http://example.com:+81/", + "http://example.com:-2/", + "http://example.com:-/", + "http://example.com:99999999999/", + "mailto:", + "mailto: ", + "mailto:a@b.c?subject=hi#frag", + "jar:file:/a/b.jar", + "jar:nosuch:/a/b.jar!/c", + "jar:file:/a/b.jar!/c/../d?q#r", + "jar:file:/a/b.jar!/x!/y", + }; + + private static final String[] CONTEXTS = { + "http://example.com/a/b/c?q=1#r", + "http://example.com", + "http://example.com/", + "http://user@example.com:8080/dir/", + "file:/tmp/dir/file.txt", + "jar:file:/a/b.jar!/c/d.class", + "https://example.com/a/b/", + "jar:file:/a/b.jar!/", + "mailto:x@example.com", + }; + + private static final String[] RELATIVE = { + "d", "d/e", "/d", "../d", "../../d", "../../../d", "./d", ".", "..", "", "?x=1", "#top", "d?x#y", + "//other.com/p", "http://absolute.com/x", "http:same/scheme", "http:/same/root", "https:other", "g;x", + "d/./e/../f", "/..", "/./x", " spaced ", "file:/elsewhere", "ftp://f.example.com/x", "?", "#", + "d/", "../", "./", "/", "a b", "x:y", + "jar:file:/z.jar!/q", "#only", "d#r", "mailto:a@b", + }; + + private static String describe(java.net.URL u) { + return "protocol=" + u.getProtocol() + " host=" + u.getHost() + " port=" + u.getPort() + " default=" + + u.getDefaultPort() + " authority=" + u.getAuthority() + " userInfo=" + u.getUserInfo() + + " path=" + u.getPath() + " query=" + u.getQuery() + " ref=" + u.getRef() + " file=" + + u.getFile() + " external=" + u.toExternalForm() + " string=" + u; + } + + private static String describe(URL u) { + return "protocol=" + u.getProtocol() + " host=" + u.getHost() + " port=" + u.getPort() + " default=" + + u.getDefaultPort() + " authority=" + u.getAuthority() + " userInfo=" + u.getUserInfo() + + " path=" + u.getPath() + " query=" + u.getQuery() + " ref=" + u.getRef() + " file=" + + u.getFile() + " external=" + u.toExternalForm() + " string=" + u; + } + + private static String jdk(String spec) { + try { + return describe(new java.net.URL(spec)); + } catch (java.net.MalformedURLException e) { + return "MalformedURLException"; + } + } + + private static String shim(String spec) { + try { + return describe(new URL(spec)); + } catch (MalformedURLException e) { + return "MalformedURLException"; + } + } + + @Test + public void parsesLikeTheJdk() { + List failures = new ArrayList(); + for (String spec : SPECS) { + String expected = jdk(spec); + String actual = shim(spec); + if (!expected.equals(actual)) { + failures.add("[" + spec + "]\n jdk =" + expected + "\n shim=" + actual); + } + } + report(failures); + } + + @Test + public void resolvesRelativeReferencesLikeTheJdk() throws Exception { + List failures = new ArrayList(); + for (String context : CONTEXTS) { + java.net.URL theirContext = new java.net.URL(context); + URL myContext = new URL(context); + for (String relative : RELATIVE) { + String expected; + String actual; + try { + expected = describe(new java.net.URL(theirContext, relative)); + } catch (java.net.MalformedURLException e) { + expected = "MalformedURLException"; + } + try { + actual = describe(new URL(myContext, relative)); + } catch (MalformedURLException e) { + actual = "MalformedURLException"; + } + if (!expected.equals(actual)) { + failures.add("[" + context + "] + [" + relative + "]\n jdk =" + expected + "\n shim=" + actual); + } + } + } + // No context is no context. + assertEquals(describe(new java.net.URL(null, "http://example.com/x")), + describe(new URL(null, "http://example.com/x"))); + report(failures); + } + + @Test + public void buildsFromPartsLikeTheJdk() throws Exception { + Object[][] parts = { + {"http", "example.com", Integer.valueOf(-1), "/a?b#c"}, + {"http", "example.com", Integer.valueOf(8080), "/a"}, + {"https", "example.com", Integer.valueOf(443), ""}, + {"file", "", Integer.valueOf(-1), "/tmp/x"}, + {"file", null, Integer.valueOf(-1), "/tmp/x"}, + {"http", "::1", Integer.valueOf(80), "/v6"}, + {"http", "[::1]", Integer.valueOf(80), "/v6"}, + {"HTTP", "Example.com", Integer.valueOf(-1), "no-slash"}, + {"ftp", "example.com", Integer.valueOf(21), "/a#ref"}, + {"http", "example.com", Integer.valueOf(-2), "/bad"}, + {"nosuch", "example.com", Integer.valueOf(-1), "/bad"}, + }; + for (Object[] p : parts) { + String protocol = (String) p[0]; + String host = (String) p[1]; + int port = ((Integer) p[2]).intValue(); + String file = (String) p[3]; + String expected; + String actual; + try { + expected = describe(new java.net.URL(protocol, host, port, file)); + } catch (java.net.MalformedURLException e) { + expected = "MalformedURLException"; + } + try { + actual = describe(new URL(protocol, host, port, file)); + } catch (MalformedURLException e) { + actual = "MalformedURLException"; + } + assertEquals(protocol + " " + host + " " + port + " " + file, expected, actual); + } + assertEquals(describe(new java.net.URL("http", "example.com", "/three")), + describe(new URL("http", "example.com", "/three"))); + } + + @Test + public void convertsToUriLikeTheJdk() throws Exception { + for (String spec : new String[] {"http://example.com/a?b#c", "file:/tmp/x", "https://u@h:1/p"}) { + assertEquals(new java.net.URL(spec).toURI(), new URL(spec).toURI()); + } + try { + new URL("http://example.com/a b").toURI(); + fail("a space is not legal in a URI"); + } catch (java.net.URISyntaxException expected) { + // As the JDK. + } + } + + @Test + public void comparesByText() throws Exception { + URL a = new URL("http://example.com/a?b#c"); + assertEquals(a, new URL("http://example.com/a?b#c")); + assertEquals(a.hashCode(), new URL("http://example.com/a?b#c").hashCode()); + // The default port is the port, and the host is not case sensitive. + assertEquals(new URL("http://example.com/"), new URL("http://EXAMPLE.com:80/")); + assertEquals(new URL("http://example.com/").hashCode(), new URL("http://EXAMPLE.com:80/").hashCode()); + assertNotEquals(a, new URL("http://example.com/a?b#d")); + assertNotEquals(a, new URL("https://example.com/a?b#c")); + assertNotEquals(a, new URL("http://example.com/A?b#c")); + assertNotEquals(a, "http://example.com/a?b#c"); + // sameFile ignores the fragment, as in the JDK. + assertTrue(a.sameFile(new URL("http://example.com/a?b#other"))); + assertFalse(a.sameFile(new URL("http://example.com/a?x#c"))); + } + + private static String read(InputStream in) throws IOException { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int b; + while ((b = in.read()) >= 0) { + out.write(b); + } + return new String(out.toByteArray(), "UTF-8"); + } finally { + in.close(); + } + } + + @Test + public void readsApplicationResourcesThroughCn1res() throws Exception { + // The form the build writes in place of Class.getResource(String). + URL url = new URL("cn1res:/com/codename1/compat/jdk/registered/plain.txt"); + assertEquals("cn1res", url.getProtocol()); + assertEquals("/com/codename1/compat/jdk/registered/plain.txt", url.getPath()); + assertEquals("", url.getHost()); + assertEquals(-1, url.getPort()); + assertEquals("cn1res:/com/codename1/compat/jdk/registered/plain.txt", url.toExternalForm()); + assertEquals("first line\nsecond line\n", read(url.openStream())); + + // A sibling resource, the way code resolves one file next to another. + URL sibling = new URL(url, "odd-name.txt"); + assertEquals("cn1res:/com/codename1/compat/jdk/registered/odd-name.txt", sibling.toExternalForm()); + assertEquals("greeting=Registered\n", read(sibling.openStream())); + + // Escapes are decoded on the way to the resource name. + assertEquals("first line\nsecond line\n", + read(new URL("cn1res:/com/codename1/compat/jdk/registered/plain%2Etxt").openStream())); + + try { + new URL("cn1res:/com/codename1/compat/jdk/registered/absent.txt").openStream(); + fail("no such resource"); + } catch (FileNotFoundException e) { + assertTrue(e.getMessage().indexOf("absent.txt") >= 0); + } + } + + @Test + public void refusesProtocolsItCannotRead() throws Exception { + try { + new URL("ftp://example.com/file").openStream(); + fail("ftp is parsed, not read"); + } catch (IOException e) { + assertTrue(e.getMessage(), e.getMessage().indexOf("ftp") >= 0); + } + } + + private static void report(List failures) { + if (failures.isEmpty()) { + return; + } + StringBuilder sb = new StringBuilder(); + sb.append(failures.size()).append(" differences from the JDK:\n"); + for (int i = 0; i < failures.size() && i < 40; i++) { + sb.append(failures.get(i)).append('\n'); + } + fail(sb.toString()); + } +} diff --git a/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/UtilShimsDifferentialTest.java b/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/UtilShimsDifferentialTest.java new file mode 100644 index 00000000000..968ae63886e --- /dev/null +++ b/maven/compat-jdk/src/test/java/com/codename1/compat/jdk/UtilShimsDifferentialTest.java @@ -0,0 +1,631 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.compat.jdk; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.TreeMap; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/// Holds the `java.util` and `java.util.concurrent` shims against the JDK +/// classes they are named after. +public class UtilShimsDifferentialTest { + + // ------------------------------------------------------------------ + // Optional + // ------------------------------------------------------------------ + + @Test + public void optionalMatchesTheJdk() { + String[] values = {null, "", "a", "hello"}; + for (String value : values) { + java.util.Optional theirs = java.util.Optional.ofNullable(value); + Optional mine = Optional.ofNullable(value); + assertEquals(theirs.isPresent(), mine.isPresent()); + assertEquals(theirs.toString(), mine.toString()); + assertEquals(theirs.hashCode(), mine.hashCode()); + assertEquals(theirs.orElse("other"), mine.orElse("other")); + assertEquals(theirs.orElseGet(() -> "made"), mine.orElseGet(() -> "made")); + assertEquals(theirs.map(String::length).toString(), mine.map(String::length).toString()); + assertEquals(theirs.map(s -> (String) null).isPresent(), mine.map(s -> (String) null).isPresent()); + assertEquals(theirs.filter(s -> s.length() > 1).toString(), mine.filter(s -> s.length() > 1).toString()); + assertEquals(theirs.flatMap(s -> java.util.Optional.of(s + "!")).toString(), + mine.flatMap(s -> Optional.of(s + "!")).toString()); + assertEquals(theirs.flatMap(s -> java.util.Optional.empty()).toString(), + mine.flatMap(s -> Optional.empty()).toString()); + List theirSeen = new ArrayList(); + List mySeen = new ArrayList(); + theirs.ifPresent(theirSeen::add); + mine.ifPresent(mySeen::add); + assertEquals(theirSeen, mySeen); + String theirGet; + String myGet; + try { + theirGet = theirs.get(); + } catch (NoSuchElementException e) { + theirGet = "NoSuchElementException:" + e.getMessage(); + } + try { + myGet = mine.get(); + } catch (NoSuchElementException e) { + myGet = "NoSuchElementException:" + e.getMessage(); + } + assertEquals(theirGet, myGet); + try { + assertEquals(value, mine.orElseThrow(() -> new IllegalStateException("absent"))); + assertTrue(theirs.isPresent()); + } catch (IllegalStateException e) { + assertFalse(theirs.isPresent()); + assertEquals("absent", e.getMessage()); + } + } + assertEquals(Optional.of("a"), Optional.of("a")); + assertNotEquals(Optional.of("a"), Optional.of("b")); + assertNotEquals(Optional.of("a"), Optional.empty()); + assertSame(Optional.empty(), Optional.empty()); + assertSame(Optional.empty(), Optional.ofNullable(null)); + } + + @Test + public void optionalRejectsNullLikeTheJdk() { + try { + Optional.of(null); + fail("of(null)"); + } catch (NullPointerException expected) { + // As the JDK. + } + try { + Optional.of("a").map(null); + fail("map(null)"); + } catch (NullPointerException expected) { + // As the JDK. + } + try { + Optional.of("a").flatMap(s -> null); + fail("flatMap answering null"); + } catch (NullPointerException expected) { + // As the JDK. + } + try { + Optional.empty().filter(null); + fail("filter(null)"); + } catch (NullPointerException expected) { + // As the JDK, which checks the predicate even when empty. + } + } + + // ------------------------------------------------------------------ + // StringJoiner, EventObject, TimeUnit + // ------------------------------------------------------------------ + + @Test + public void stringJoinerMatchesTheJdk() { + String[][] parts = {{}, {"a"}, {"a", "b"}, {"", ""}, {"x", "", "z"}, {"null", "1"}}; + for (String[] elements : parts) { + for (int variant = 0; variant < 4; variant++) { + java.util.StringJoiner theirs = variant < 2 + ? new java.util.StringJoiner(", ") : new java.util.StringJoiner("-", "[", "]"); + StringJoiner mine = variant < 2 ? new StringJoiner(", ") : new StringJoiner("-", "[", "]"); + if (variant % 2 == 1) { + theirs.setEmptyValue("EMPTY"); + mine.setEmptyValue("EMPTY"); + } + assertEquals(theirs.toString(), mine.toString()); + assertEquals(theirs.length(), mine.length()); + for (String element : elements) { + theirs.add(element); + mine.add(element); + assertEquals(theirs.toString(), mine.toString()); + assertEquals(theirs.length(), mine.length()); + } + java.util.StringJoiner theirOther = new java.util.StringJoiner("+", "<", ">"); + StringJoiner myOther = new StringJoiner("+", "<", ">"); + theirs.merge(theirOther); + mine.merge(myOther); + assertEquals(theirs.toString(), mine.toString()); + theirOther.add("p").add("q"); + myOther.add("p").add("q"); + theirs.merge(theirOther); + mine.merge(myOther); + assertEquals(theirs.toString(), mine.toString()); + theirs.merge(theirs); + mine.merge(mine); + assertEquals(theirs.toString(), mine.toString()); + theirs.add(null); + mine.add(null); + assertEquals(theirs.toString(), mine.toString()); + } + } + try { + new StringJoiner(null); + fail("null delimiter"); + } catch (NullPointerException expected) { + // As the JDK. + } + } + + @Test + public void eventObjectMatchesTheJdk() { + Object source = "the source"; + EventObject mine = new EventObject(source); + java.util.EventObject theirs = new java.util.EventObject(source); + assertSame(source, mine.getSource()); + assertEquals(theirs.toString().replace("java.util.EventObject", EventObject.class.getName()), + mine.toString()); + try { + new EventObject(null); + fail("null source"); + } catch (IllegalArgumentException e) { + assertEquals("null source", e.getMessage()); + } + } + + @Test + public void timeUnitMatchesTheJdk() { + long[] durations = { + 0, 1, -1, 59, 60, 999, 1000, 1001, 86399, 86400, 1234567890123L, -1234567890123L, + Long.MAX_VALUE, Long.MIN_VALUE, Long.MAX_VALUE / 1000, Long.MAX_VALUE / 1000 + 1, + Long.MIN_VALUE / 1000 - 1, 9223372036854L, 9223372036855L, 106751991167L, + }; + java.util.concurrent.TimeUnit[] theirs = java.util.concurrent.TimeUnit.values(); + TimeUnit[] mine = TimeUnit.values(); + assertEquals(theirs.length, mine.length); + for (int i = 0; i < theirs.length; i++) { + assertEquals(theirs[i].name(), mine[i].name()); + assertEquals(theirs[i].ordinal(), mine[i].ordinal()); + for (long duration : durations) { + String what = theirs[i] + " " + duration; + assertEquals(what, theirs[i].toNanos(duration), mine[i].toNanos(duration)); + assertEquals(what, theirs[i].toMicros(duration), mine[i].toMicros(duration)); + assertEquals(what, theirs[i].toMillis(duration), mine[i].toMillis(duration)); + assertEquals(what, theirs[i].toSeconds(duration), mine[i].toSeconds(duration)); + assertEquals(what, theirs[i].toMinutes(duration), mine[i].toMinutes(duration)); + assertEquals(what, theirs[i].toHours(duration), mine[i].toHours(duration)); + assertEquals(what, theirs[i].toDays(duration), mine[i].toDays(duration)); + for (int j = 0; j < theirs.length; j++) { + assertEquals(what + " from " + theirs[j], theirs[i].convert(duration, theirs[j]), + mine[i].convert(duration, mine[j])); + } + } + } + assertSame(TimeUnit.SECONDS, TimeUnit.valueOf("SECONDS")); + } + + @Test + public void exceptionsCarryWhatTheJdkOnesCarry() { + Throwable cause = new IllegalStateException("why"); + assertEquals(new java.util.concurrent.ExecutionException(cause).getMessage(), + new ExecutionException(cause).getMessage()); + assertSame(cause, new ExecutionException("m", cause).getCause()); + assertEquals("m", new CancellationException("m").getMessage()); + assertNull(new CancellationException().getMessage()); + assertEquals("m", new TimeoutException("m").getMessage()); + MissingResourceException missing = new MissingResourceException("text", "Class", "key"); + assertEquals("text", missing.getMessage()); + assertEquals("Class", missing.getClassName()); + assertEquals("key", missing.getKey()); + assertEquals("bad", new MalformedURLException("bad").getMessage()); + assertTrue(new MalformedURLException() instanceof java.io.IOException); + } + + // ------------------------------------------------------------------ + // ConcurrentHashMap + // ------------------------------------------------------------------ + + private static String sorted(Map map) { + return new TreeMap(map).toString(); + } + + @Test + public void concurrentHashMapFollowsTheJdkThroughRandomOperations() { + Random random = new Random(20260101L); + for (int round = 0; round < 200; round++) { + java.util.concurrent.ConcurrentHashMap theirs = + new java.util.concurrent.ConcurrentHashMap(); + ConcurrentHashMap mine = new ConcurrentHashMap(); + StringBuilder trace = new StringBuilder(); + for (int step = 0; step < 60; step++) { + String key = "k" + random.nextInt(8); + Integer value = Integer.valueOf(random.nextInt(5)); + int op = random.nextInt(16); + trace.append(op).append(':').append(key).append('=').append(value).append(' '); + Object expected; + Object actual; + switch (op) { + case 0: + expected = theirs.put(key, value); + actual = mine.put(key, value); + break; + case 1: + expected = theirs.remove(key); + actual = mine.remove(key); + break; + case 2: + expected = theirs.putIfAbsent(key, value); + actual = mine.putIfAbsent(key, value); + break; + case 3: + expected = Boolean.valueOf(theirs.remove(key, value)); + actual = Boolean.valueOf(mine.remove(key, value)); + break; + case 4: + expected = theirs.replace(key, value); + actual = mine.replace(key, value); + break; + case 5: + expected = Boolean.valueOf(theirs.replace(key, value, Integer.valueOf(9))); + actual = Boolean.valueOf(mine.replace(key, value, Integer.valueOf(9))); + break; + case 6: + expected = theirs.getOrDefault(key, Integer.valueOf(-1)); + actual = mine.getOrDefault(key, Integer.valueOf(-1)); + break; + case 7: + expected = theirs.computeIfAbsent(key, k -> value.intValue() == 0 ? null : value); + actual = mine.computeIfAbsent(key, k -> value.intValue() == 0 ? null : value); + break; + case 8: + expected = theirs.computeIfPresent(key, (k, v) -> v.intValue() == 0 ? null : v + 1); + actual = mine.computeIfPresent(key, (k, v) -> v.intValue() == 0 ? null : v + 1); + break; + case 9: + expected = theirs.compute(key, + (k, v) -> v == null ? value : (v.intValue() == 1 ? null : Integer.valueOf(v + 10))); + actual = mine.compute(key, + (k, v) -> v == null ? value : (v.intValue() == 1 ? null : Integer.valueOf(v + 10))); + break; + case 10: + expected = theirs.merge(key, value, (a, b) -> a + b == 4 ? null : Integer.valueOf(a + b)); + actual = mine.merge(key, value, (a, b) -> a + b == 4 ? null : Integer.valueOf(a + b)); + break; + case 11: + expected = theirs.containsKey(key) + "/" + theirs.containsValue(value) + "/" + + theirs.contains(value) + "/" + theirs.get(key); + actual = mine.containsKey(key) + "/" + mine.containsValue(value) + "/" + + mine.contains(value) + "/" + mine.get(key); + break; + case 12: + theirs.replaceAll((k, v) -> Integer.valueOf(v + 1)); + mine.replaceAll((k, v) -> Integer.valueOf(v + 1)); + expected = null; + actual = null; + break; + case 13: + expected = Boolean.valueOf(theirs.keySet().remove(key)); + actual = Boolean.valueOf(mine.keySet().remove(key)); + break; + case 14: + expected = Boolean.valueOf(theirs.values().remove(value)); + actual = Boolean.valueOf(mine.values().remove(value)); + break; + default: + expected = Boolean.valueOf(theirs.entrySet().removeIf(e -> e.getValue().equals(value))); + actual = Boolean.valueOf(mine.entrySet().removeIf(e -> e.getValue().equals(value))); + break; + } + assertEquals(trace.toString(), expected, actual); + assertEquals(trace.toString(), sorted(theirs), sorted(mine)); + assertEquals(trace.toString(), theirs.size(), mine.size()); + assertEquals(trace.toString(), theirs.mappingCount(), mine.mappingCount()); + assertEquals(trace.toString(), theirs.isEmpty(), mine.isEmpty()); + assertEquals(trace.toString(), theirs.hashCode(), mine.hashCode()); + assertTrue(trace.toString(), mine.equals(theirs)); + assertTrue(trace.toString(), theirs.equals(mine)); + } + } + } + + @Test + public void concurrentHashMapRejectsNullLikeTheJdk() { + ConcurrentHashMap map = new ConcurrentHashMap(); + map.put("a", "1"); + Runnable[] attempts = { + () -> map.put(null, "x"), + () -> map.put("x", null), + () -> map.get(null), + () -> map.containsKey(null), + () -> map.containsValue(null), + () -> map.remove(null), + () -> map.putIfAbsent("x", null), + () -> map.putIfAbsent(null, "x"), + () -> map.replace("a", null), + () -> map.replace("a", "1", null), + () -> map.merge("a", null, (x, y) -> x), + () -> map.computeIfAbsent(null, k -> "x"), + () -> map.getOrDefault(null, "x"), + () -> map.putAll(Collections.singletonMap("x", null)), + }; + for (int i = 0; i < attempts.length; i++) { + try { + attempts[i].run(); + fail("attempt " + i + " accepted a null"); + } catch (NullPointerException expected) { + // As the JDK. + } + } + assertEquals("{a=1}", map.toString()); + // A value that is not there is simply not removed, null or not. + assertFalse(map.remove("a", null)); + } + + @Test + public void concurrentHashMapIteratesWhileItChanges() { + ConcurrentHashMap map = new ConcurrentHashMap(); + for (int i = 0; i < 10; i++) { + map.put("k" + i, Integer.valueOf(i)); + } + // The JDK's iterators never throw ConcurrentModificationException; + // this one walks the keys present when it was created. + int seen = 0; + for (String key : map.keySet()) { + map.remove(key); + map.put("new" + key, Integer.valueOf(0)); + seen++; + } + assertEquals(10, seen); + assertEquals(10, map.size()); + for (Map.Entry entry : map.entrySet()) { + entry.setValue(Integer.valueOf(7)); + map.put("again" + entry.getKey(), Integer.valueOf(1)); + } + assertEquals(20, map.size()); + assertEquals(Integer.valueOf(7), map.get("newk3")); + Iterator values = map.values().iterator(); + while (values.hasNext()) { + if (values.next().intValue() == 1) { + values.remove(); + } + } + assertEquals(10, map.size()); + map.forEach((k, v) -> map.put(k + "!", v)); + assertEquals(20, map.size()); + + int count = 0; + for (Enumeration keys = map.keys(); keys.hasMoreElements();) { + assertTrue(map.containsKey(keys.nextElement())); + count++; + } + assertEquals(20, count); + count = 0; + for (Enumeration elements = map.elements(); elements.hasMoreElements();) { + assertEquals(Integer.valueOf(7), elements.nextElement()); + count++; + } + assertEquals(20, count); + } + + @Test + public void keySetViewsBehaveLikeTheJdk() { + ConcurrentHashMap.KeySetView mine = ConcurrentHashMap.newKeySet(); + java.util.concurrent.ConcurrentHashMap.KeySetView theirs = + java.util.concurrent.ConcurrentHashMap.newKeySet(); + for (String s : new String[] {"a", "b", "a", "c"}) { + assertEquals(Boolean.valueOf(theirs.add(s)), Boolean.valueOf(mine.add(s))); + } + assertEquals(theirs.size(), mine.size()); + assertEquals(Boolean.valueOf(theirs.remove("b")), Boolean.valueOf(mine.remove("b"))); + assertEquals(Boolean.valueOf(theirs.contains("b")), Boolean.valueOf(mine.contains("b"))); + assertTrue(mine.containsAll(Arrays.asList("a", "c"))); + assertEquals(theirs, mine); + assertEquals(theirs.hashCode(), mine.hashCode()); + + ConcurrentHashMap map = new ConcurrentHashMap(); + ConcurrentHashMap.KeySetView view = map.keySet(Integer.valueOf(5)); + assertTrue(view.add("x")); + assertFalse(view.add("x")); + assertEquals(Integer.valueOf(5), map.get("x")); + assertEquals(Integer.valueOf(5), view.getMappedValue()); + assertSame(map, view.getMap()); + try { + map.keySet().add("y"); + fail("a plain key set has no value to map new keys to"); + } catch (UnsupportedOperationException expected) { + // As the JDK. + } + } + + // ------------------------------------------------------------------ + // CopyOnWriteArrayList + // ------------------------------------------------------------------ + + @Test + public void copyOnWriteArrayListFollowsTheJdkThroughRandomOperations() { + Random random = new Random(42L); + for (int round = 0; round < 200; round++) { + java.util.concurrent.CopyOnWriteArrayList theirs = + new java.util.concurrent.CopyOnWriteArrayList(); + CopyOnWriteArrayList mine = new CopyOnWriteArrayList(); + StringBuilder trace = new StringBuilder(); + for (int step = 0; step < 50; step++) { + Integer value = Integer.valueOf(random.nextInt(6)); + int index = random.nextInt(theirs.size() + 2) - 1; + int op = random.nextInt(18); + trace.append(op).append(':').append(index).append('=').append(value).append(' '); + Object expected; + Object actual; + try { + expected = apply(theirs, theirs, op, index, value); + } catch (RuntimeException e) { + expected = e.getClass().getName(); + } + try { + actual = apply(mine, mine, op, index, value); + } catch (RuntimeException e) { + actual = e.getClass().getName(); + } + assertEquals(trace.toString(), expected, actual); + assertEquals(trace.toString(), theirs.toString(), mine.toString()); + assertEquals(trace.toString(), theirs.hashCode(), mine.hashCode()); + assertTrue(trace.toString(), mine.equals(theirs)); + assertTrue(trace.toString(), theirs.equals(mine)); + assertEquals(trace.toString(), Arrays.toString(theirs.toArray()), Arrays.toString(mine.toArray())); + assertEquals(trace.toString(), Arrays.toString(theirs.toArray(new Integer[0])), + Arrays.toString(mine.toArray(new Integer[0]))); + } + } + } + + /// One operation, applied the same way to either list. `list` and + /// `typed` are the same object: once as the interface both classes share, + /// once for the methods only the two concrete classes have. + private static Object apply(List list, Object typed, int op, int index, Integer value) { + switch (op) { + case 0: + return Boolean.valueOf(list.add(value)); + case 1: + list.add(index, value); + return null; + case 2: + return list.remove(index); + case 3: + return Boolean.valueOf(list.remove(value)); + case 4: + return list.set(index, value); + case 5: + return list.get(index); + case 6: + return list.indexOf(value) + "/" + list.lastIndexOf(value) + "/" + list.contains(value); + case 7: + return Boolean.valueOf(list.addAll(Arrays.asList(value, Integer.valueOf(index)))); + case 8: + return Boolean.valueOf(list.addAll(index, Arrays.asList(value, value))); + case 9: + return Boolean.valueOf(list.removeAll(Arrays.asList(value, Integer.valueOf(0)))); + case 10: + return Boolean.valueOf(list.retainAll(Arrays.asList(value, Integer.valueOf(1), Integer.valueOf(2)))); + case 11: + return Boolean.valueOf(list.removeIf(v -> v.equals(value))); + case 12: + list.replaceAll(v -> Integer.valueOf((v + 1) % 6)); + return null; + case 13: + list.sort(index < 0 ? null : Collections.reverseOrder()); + return null; + case 14: + if (typed instanceof CopyOnWriteArrayList) { + @SuppressWarnings("unchecked") + CopyOnWriteArrayList mine = (CopyOnWriteArrayList) typed; + return mine.addIfAbsent(value) + "/" + mine.addAllAbsent(Arrays.asList(value, Integer.valueOf(3))) + + "/" + mine.indexOf(value, Math.max(0, index)) + "/" + + mine.lastIndexOf(value, Math.min(mine.size() - 1, Math.max(0, index))); + } + @SuppressWarnings("unchecked") + java.util.concurrent.CopyOnWriteArrayList theirs = + (java.util.concurrent.CopyOnWriteArrayList) typed; + return theirs.addIfAbsent(value) + "/" + theirs.addAllAbsent(Arrays.asList(value, Integer.valueOf(3))) + + "/" + theirs.indexOf(value, Math.max(0, index)) + "/" + + theirs.lastIndexOf(value, Math.min(theirs.size() - 1, Math.max(0, index))); + case 15: + return list.subList(Math.max(0, index), list.size()).toString() + list.containsAll(Arrays.asList(value)); + case 16: { + StringBuilder sb = new StringBuilder(); + ListIterator it = list.listIterator(index); + while (it.hasPrevious()) { + sb.append(it.previousIndex()).append('=').append(it.previous()).append(' '); + } + while (it.hasNext()) { + sb.append(it.nextIndex()).append('=').append(it.next()).append(' '); + } + return sb.toString(); + } + default: + if (index < 0) { + list.clear(); + } + return list.size() + "/" + list.isEmpty(); + } + } + + @Test + public void copyOnWriteIteratorsSeeTheListAsItWas() { + CopyOnWriteArrayList list = new CopyOnWriteArrayList(Arrays.asList("a", "b", "c")); + StringBuilder seen = new StringBuilder(); + // The reason desktop listener lists use this class: a listener that + // removes itself, or adds another, during the walk. + for (String s : list) { + seen.append(s); + list.remove(s); + list.add(s + s); + } + assertEquals("abc", seen.toString()); + assertEquals("[aa, bb, cc]", list.toString()); + + Iterator it = list.iterator(); + it.next(); + try { + it.remove(); + fail("a snapshot iterator cannot remove"); + } catch (UnsupportedOperationException expected) { + // As the JDK. + } + ListIterator listIterator = list.listIterator(); + listIterator.next(); + try { + listIterator.set("x"); + fail("a snapshot iterator cannot set"); + } catch (UnsupportedOperationException expected) { + // As the JDK. + } + try { + listIterator.add("x"); + fail("a snapshot iterator cannot add"); + } catch (UnsupportedOperationException expected) { + // As the JDK. + } + list.clear(); + try { + list.iterator().next(); + fail("nothing left"); + } catch (NoSuchElementException expected) { + // As the JDK. + } + } + + @Test + public void copyOnWriteCloneIsIndependent() { + CopyOnWriteArrayList list = new CopyOnWriteArrayList(new String[] {"a", "b"}); + Object copy = list.clone(); + assertTrue(copy instanceof CopyOnWriteArrayList); + assertEquals(list, copy); + list.add("c"); + assertEquals("[a, b]", copy.toString()); + assertEquals("[a, b, c]", list.toString()); + } +} diff --git a/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages.properties b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages.properties new file mode 100644 index 00000000000..22d17c6df7c --- /dev/null +++ b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages.properties @@ -0,0 +1,4 @@ +# Base bundle +greeting=Hello +farewell = Goodbye +only.base=base diff --git a/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages_fr.properties b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages_fr.properties new file mode 100644 index 00000000000..e42677fb9c0 --- /dev/null +++ b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages_fr.properties @@ -0,0 +1,3 @@ +greeting=Bonjour +farewell=Au revoir +only.fr=fr diff --git a/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages_fr_CA.properties b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages_fr_CA.properties new file mode 100644 index 00000000000..a0b368bc7ce --- /dev/null +++ b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/Messages_fr_CA.properties @@ -0,0 +1 @@ +greeting=Allo diff --git a/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/NoBase_de.properties b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/NoBase_de.properties new file mode 100644 index 00000000000..36753206024 --- /dev/null +++ b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/bundles/NoBase_de.properties @@ -0,0 +1 @@ +key=nur deutsch diff --git a/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/odd-name-es.txt b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/odd-name-es.txt new file mode 100644 index 00000000000..14caaec2edb --- /dev/null +++ b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/odd-name-es.txt @@ -0,0 +1 @@ +greeting=Registrado diff --git a/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/odd-name.txt b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/odd-name.txt new file mode 100644 index 00000000000..14080ff121b --- /dev/null +++ b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/odd-name.txt @@ -0,0 +1 @@ +greeting=Registered diff --git a/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/plain.txt b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/plain.txt new file mode 100644 index 00000000000..06fcdd77c93 --- /dev/null +++ b/maven/compat-jdk/src/test/resources/com/codename1/compat/jdk/registered/plain.txt @@ -0,0 +1,2 @@ +first line +second line From 6f33fa1039efd845c8209e4bd42357df5b4b26d5 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:47:57 +0300 Subject: [PATCH 006/257] Swing layer kernel: components, peers, painting, layout, events and windows Every AWT component owns one lazily created Codename One peer. The peer is transparent to Codename One and hands its paint to the Swing object, so an overridden paintComponent decides what is drawn; the default one paints the backing Codename One widget. Containers bridge their LayoutManager into the peer tree, AWT coordinates are logical pixels of 1/96 inch, and the AWT event dispatch thread is the Codename One one. Adds java.awt geometry, layouts, events and windows, java.beans property support, the Swing borders and box layouts, and the first peer-backed widgets: JButton, JCheckBox, JRadioButton, JToggleButton, JLabel, JTextField. Co-Authored-By: Claude Opus 5.5 --- maven/swing-compat/spotbugs-exclude.xml | 149 ++ .../desktopcompat/java/awt/AWTEvent.java | 113 ++ .../desktopcompat/java/awt/ActiveEvent.java | 29 + .../desktopcompat/java/awt/Adjustable.java | 65 + .../java/awt/AlphaComposite.java | 110 ++ .../desktopcompat/java/awt/BasicStroke.java | 177 ++ .../desktopcompat/java/awt/BorderLayout.java | 311 ++++ .../desktopcompat/java/awt/Canvas.java | 47 + .../desktopcompat/java/awt/CardLayout.java | 248 +++ .../desktopcompat/java/awt/Color.java | 286 ++++ .../desktopcompat/java/awt/Component.java | 1029 ++++++++++++ .../desktopcompat/java/awt/Composite.java | 28 + .../desktopcompat/java/awt/Container.java | 472 ++++++ .../desktopcompat/java/awt/Cursor.java | 96 ++ .../desktopcompat/java/awt/Dialog.java | 109 ++ .../desktopcompat/java/awt/Dimension.java | 101 ++ .../desktopcompat/java/awt/EventQueue.java | 62 + .../desktopcompat/java/awt/FlowLayout.java | 220 +++ .../desktopcompat/java/awt/Font.java | 192 +++ .../desktopcompat/java/awt/FontMetrics.java | 112 ++ .../desktopcompat/java/awt/Frame.java | 100 ++ .../desktopcompat/java/awt/GradientPaint.java | 89 + .../desktopcompat/java/awt/Graphics.java | 204 +++ .../desktopcompat/java/awt/Graphics2D.java | 129 ++ .../java/awt/GridBagConstraints.java | 165 ++ .../desktopcompat/java/awt/GridBagLayout.java | 686 ++++++++ .../desktopcompat/java/awt/GridLayout.java | 171 ++ .../desktopcompat/java/awt/Image.java | 73 + .../desktopcompat/java/awt/Insets.java | 73 + .../java/awt/ItemSelectable.java | 36 + .../desktopcompat/java/awt/LayoutManager.java | 37 + .../java/awt/LayoutManager2.java | 38 + .../desktopcompat/java/awt/Paint.java | 32 + .../desktopcompat/java/awt/Panel.java | 35 + .../desktopcompat/java/awt/Point.java | 106 ++ .../desktopcompat/java/awt/Polygon.java | 182 +++ .../desktopcompat/java/awt/Rectangle.java | 348 ++++ .../java/awt/RenderingHints.java | 283 ++++ .../desktopcompat/java/awt/Shape.java | 53 + .../desktopcompat/java/awt/Stroke.java | 28 + .../desktopcompat/java/awt/Toolkit.java | 68 + .../desktopcompat/java/awt/Transparency.java | 32 + .../desktopcompat/java/awt/Window.java | 286 ++++ .../java/awt/event/ActionEvent.java | 91 ++ .../java/awt/event/ActionListener.java | 31 + .../java/awt/event/AdjustmentEvent.java | 114 ++ .../java/awt/event/AdjustmentListener.java | 31 + .../java/awt/event/ComponentAdapter.java | 40 + .../java/awt/event/ComponentEvent.java | 76 + .../java/awt/event/ComponentListener.java | 37 + .../java/awt/event/ContainerAdapter.java | 34 + .../java/awt/event/ContainerEvent.java | 70 + .../java/awt/event/ContainerListener.java | 34 + .../java/awt/event/FocusAdapter.java | 34 + .../java/awt/event/FocusEvent.java | 82 + .../java/awt/event/FocusListener.java | 33 + .../java/awt/event/HierarchyEvent.java | 111 ++ .../java/awt/event/HierarchyListener.java | 31 + .../java/awt/event/InputEvent.java | 249 +++ .../java/awt/event/InvocationEvent.java | 101 ++ .../java/awt/event/ItemEvent.java | 88 + .../java/awt/event/ItemListener.java | 31 + .../java/awt/event/KeyAdapter.java | 37 + .../java/awt/event/KeyEvent.java | 1013 ++++++++++++ .../java/awt/event/KeyListener.java | 35 + .../java/awt/event/MouseAdapter.java | 52 + .../java/awt/event/MouseEvent.java | 239 +++ .../java/awt/event/MouseListener.java | 39 + .../java/awt/event/MouseMotionAdapter.java | 34 + .../java/awt/event/MouseMotionListener.java | 33 + .../java/awt/event/MouseWheelEvent.java | 98 ++ .../java/awt/event/MouseWheelListener.java | 31 + .../java/awt/event/TextEvent.java | 44 + .../java/awt/event/TextListener.java | 31 + .../java/awt/event/WindowAdapter.java | 58 + .../java/awt/event/WindowEvent.java | 136 ++ .../java/awt/event/WindowFocusListener.java | 33 + .../java/awt/event/WindowListener.java | 44 + .../java/awt/event/WindowStateListener.java | 31 + .../java/awt/event/package-info.java | 28 + .../java/awt/geom/AffineTransform.java | 674 ++++++++ .../desktopcompat/java/awt/geom/Arc2D.java | 588 +++++++ .../java/awt/geom/CubicCurve2D.java | 477 ++++++ .../java/awt/geom/Dimension2D.java | 80 + .../java/awt/geom/Ellipse2D.java | 255 +++ .../java/awt/geom/FlatteningPathIterator.java | 212 +++ .../java/awt/geom/GeneralPath.java | 51 + .../awt/geom/IllegalPathStateException.java | 38 + .../desktopcompat/java/awt/geom/Line2D.java | 397 +++++ .../geom/NoninvertibleTransformException.java | 34 + .../desktopcompat/java/awt/geom/Outline.java | 189 +++ .../desktopcompat/java/awt/geom/Path2D.java | 479 ++++++ .../java/awt/geom/PathIterator.java | 64 + .../desktopcompat/java/awt/geom/Point2D.java | 178 ++ .../java/awt/geom/QuadCurve2D.java | 441 +++++ .../java/awt/geom/Rectangle2D.java | 415 +++++ .../java/awt/geom/RectangularShape.java | 144 ++ .../java/awt/geom/RoundRectangle2D.java | 353 ++++ .../java/awt/geom/SegmentIterator.java | 177 ++ .../java/awt/geom/package-info.java | 39 + .../java/awt/image/BufferedImage.java | 246 +++ .../java/awt/image/ImageObserver.java | 41 + .../java/awt/image/package-info.java | 25 + .../desktopcompat/java/awt/package-info.java | 29 + .../beans/IndexedPropertyChangeEvent.java | 44 + .../java/beans/PropertyChangeEvent.java | 83 + .../java/beans/PropertyChangeListener.java | 31 + .../beans/PropertyChangeListenerProxy.java | 50 + .../java/beans/PropertyChangeSupport.java | 202 +++ .../java/beans/PropertyVetoException.java | 38 + .../java/beans/VetoableChangeListener.java | 31 + .../java/beans/package-info.java | 29 + .../javax/swing/AbstractAction.java | 119 ++ .../javax/swing/AbstractButton.java | 426 +++++ .../desktopcompat/javax/swing/Action.java | 65 + .../javax/swing/BorderFactory.java | 157 ++ .../desktopcompat/javax/swing/Box.java | 118 ++ .../desktopcompat/javax/swing/BoxLayout.java | 189 +++ .../javax/swing/ButtonGroup.java | 103 ++ .../javax/swing/ButtonModel.java | 74 + .../javax/swing/DefaultButtonModel.java | 263 +++ .../desktopcompat/javax/swing/Icon.java | 36 + .../desktopcompat/javax/swing/ImageIcon.java | 143 ++ .../desktopcompat/javax/swing/JButton.java | 65 + .../desktopcompat/javax/swing/JCheckBox.java | 67 + .../desktopcompat/javax/swing/JComponent.java | 287 ++++ .../desktopcompat/javax/swing/JDialog.java | 166 ++ .../desktopcompat/javax/swing/JFrame.java | 171 ++ .../desktopcompat/javax/swing/JLabel.java | 217 +++ .../desktopcompat/javax/swing/JPanel.java | 48 + .../javax/swing/JRadioButton.java | 68 + .../desktopcompat/javax/swing/JRootPane.java | 127 ++ .../desktopcompat/javax/swing/JTextField.java | 145 ++ .../javax/swing/JToggleButton.java | 88 + .../javax/swing/OverlayLayout.java | 141 ++ .../javax/swing/SizeRequirements.java | 184 +++ .../javax/swing/SwingConstants.java | 65 + .../javax/swing/SwingUtilities.java | 170 ++ .../desktopcompat/javax/swing/Timer.java | 182 +++ .../desktopcompat/javax/swing/UIManager.java | 108 ++ .../UnsupportedLookAndFeelException.java | 31 + .../javax/swing/WindowConstants.java | 33 + .../javax/swing/border/AbstractBorder.java | 71 + .../javax/swing/border/BevelBorder.java | 151 ++ .../javax/swing/border/Border.java | 38 + .../javax/swing/border/CompoundBorder.java | 102 ++ .../javax/swing/border/EmptyBorder.java | 75 + .../javax/swing/border/EtchedBorder.java | 117 ++ .../javax/swing/border/LineBorder.java | 113 ++ .../javax/swing/border/MatteBorder.java | 139 ++ .../javax/swing/border/TitledBorder.java | 404 +++++ .../javax/swing/border/package-info.java | 33 + .../javax/swing/event/ChangeEvent.java | 33 + .../javax/swing/event/ChangeListener.java | 31 + .../javax/swing/event/EventListenerList.java | 122 ++ .../javax/swing/event/ListSelectionEvent.java | 60 + .../swing/event/ListSelectionListener.java | 31 + .../event/SwingPropertyChangeSupport.java | 47 + .../javax/swing/event/package-info.java | 25 + .../javax/swing/package-info.java | 30 + .../javax/swing/text/JTextComponent.java | 84 + .../javax/swing/text/package-info.java | 25 + .../desktopcompat/rt/ButtonPeer.java | 74 + .../desktopcompat/rt/CanvasPeer.java | 72 + .../desktopcompat/rt/CheckBoxPeer.java | 75 + .../desktopcompat/rt/ContainerPeer.java | 76 + .../desktopcompat/rt/DesktopLifecycle.java | 46 + .../desktopcompat/rt/DesktopToolkit.java | 74 + .../desktopcompat/rt/EventBridge.java | 376 +++++ .../com/codename1/desktopcompat/rt/Fonts.java | 144 ++ .../codename1/desktopcompat/rt/FrameForm.java | 131 ++ .../com/codename1/desktopcompat/rt/G2D.java | 1390 ++++++++++++++++ .../com/codename1/desktopcompat/rt/Icons.java | 77 + .../codename1/desktopcompat/rt/LabelPeer.java | 73 + .../desktopcompat/rt/LayoutBridge.java | 71 + .../desktopcompat/rt/NativeImage.java | 59 + .../com/codename1/desktopcompat/rt/Peer.java | 47 + .../desktopcompat/rt/PeerSupport.java | 222 +++ .../desktopcompat/rt/RadioButtonPeer.java | 75 + .../desktopcompat/rt/TextFieldPeer.java | 74 + .../com/codename1/desktopcompat/rt/Units.java | 115 ++ .../desktopcompat/rt/package-info.java | 31 + .../codename1/desktopcompat/ApiShapeTest.java | 268 +++ .../desktopcompat/ConstantParityTest.java | 130 ++ .../desktopcompat/EdtAndGraphicsTest.java | 254 +++ .../desktopcompat/EventDeliveryTest.java | 345 ++++ .../desktopcompat/FrameAndLayoutTest.java | 187 +++ .../desktopcompat/KernelTestBase.java | 119 ++ .../desktopcompat/LayoutParityTest.java | 955 +++++++++++ .../NoJdkDesktopReferenceTest.java | 153 ++ .../desktopcompat/PaintRoutingTest.java | 163 ++ .../java/awt/geom/GeomParityTest.java | 1444 +++++++++++++++++ .../java/beans/PropertyChangeSupportTest.java | 245 +++ .../javax/swing/DefaultButtonModelTest.java | 261 +++ .../codename1/desktopcompat/rt/UnitsTest.java | 91 ++ 195 files changed, 30382 insertions(+) create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/AWTEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/ActiveEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Adjustable.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/AlphaComposite.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/BasicStroke.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/BorderLayout.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Canvas.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/CardLayout.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Color.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Component.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Composite.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Container.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Cursor.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Dialog.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Dimension.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/EventQueue.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/FlowLayout.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Font.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/FontMetrics.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Frame.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GradientPaint.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Graphics.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Graphics2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridBagConstraints.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridBagLayout.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridLayout.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Image.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Insets.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/ItemSelectable.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/LayoutManager.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/LayoutManager2.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Paint.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Panel.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Point.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Polygon.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Rectangle.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/RenderingHints.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Shape.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Stroke.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Toolkit.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Transparency.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Window.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ActionEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ActionListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/AdjustmentEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/AdjustmentListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentAdapter.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerAdapter.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusAdapter.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/HierarchyEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/HierarchyListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/InputEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/InvocationEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ItemEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ItemListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyAdapter.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseAdapter.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseMotionAdapter.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseMotionListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseWheelEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseWheelListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/TextEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/TextListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowAdapter.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowFocusListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowStateListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/package-info.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/AffineTransform.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Arc2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/CubicCurve2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Dimension2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Ellipse2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/FlatteningPathIterator.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/GeneralPath.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/IllegalPathStateException.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Line2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/NoninvertibleTransformException.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Outline.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Path2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/PathIterator.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Point2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/QuadCurve2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Rectangle2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/RectangularShape.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/RoundRectangle2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/SegmentIterator.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/package-info.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/BufferedImage.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/ImageObserver.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/package-info.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/package-info.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/IndexedPropertyChangeEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeListenerProxy.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeSupport.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyVetoException.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/VetoableChangeListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/package-info.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/AbstractAction.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/AbstractButton.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Action.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/BorderFactory.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Box.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/BoxLayout.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ButtonGroup.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ButtonModel.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/DefaultButtonModel.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Icon.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ImageIcon.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JButton.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JCheckBox.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JComponent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JDialog.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JFrame.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JLabel.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JPanel.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JRadioButton.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JRootPane.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JTextField.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JToggleButton.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/OverlayLayout.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SizeRequirements.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SwingConstants.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SwingUtilities.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Timer.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/UIManager.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/UnsupportedLookAndFeelException.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/WindowConstants.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/AbstractBorder.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/BevelBorder.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/Border.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/CompoundBorder.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/EmptyBorder.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/EtchedBorder.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/LineBorder.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/MatteBorder.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/TitledBorder.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/package-info.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ChangeEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ChangeListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/EventListenerList.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ListSelectionEvent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ListSelectionListener.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/SwingPropertyChangeSupport.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/package-info.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/package-info.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/text/JTextComponent.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/text/package-info.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/ButtonPeer.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/CanvasPeer.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/CheckBoxPeer.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/ContainerPeer.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/DesktopLifecycle.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/DesktopToolkit.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/EventBridge.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Fonts.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/FrameForm.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/G2D.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Icons.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/LabelPeer.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/LayoutBridge.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/NativeImage.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Peer.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/PeerSupport.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/RadioButtonPeer.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/TextFieldPeer.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Units.java create mode 100644 maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/package-info.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/ApiShapeTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/ConstantParityTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/EdtAndGraphicsTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/EventDeliveryTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/FrameAndLayoutTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/KernelTestBase.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/LayoutParityTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/NoJdkDesktopReferenceTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/PaintRoutingTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/java/awt/geom/GeomParityTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/java/beans/PropertyChangeSupportTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/javax/swing/DefaultButtonModelTest.java create mode 100644 maven/swing-compat/src/test/java/com/codename1/desktopcompat/rt/UnitsTest.java diff --git a/maven/swing-compat/spotbugs-exclude.xml b/maven/swing-compat/spotbugs-exclude.xml index f4c69d2e2a6..184099325a8 100644 --- a/maven/swing-compat/spotbugs-exclude.xml +++ b/maven/swing-compat/spotbugs-exclude.xml @@ -2,4 +2,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/AWTEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/AWTEvent.java new file mode 100644 index 00000000000..be1c6d41a38 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/AWTEvent.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import java.util.EventObject; + +/// The root of every event the AWT and Swing layers deliver. +/// +/// An event carries the object it came from, an integer id that tells the +/// event kinds of one class apart, and a consumed flag that lets a listener +/// stop the default handling. The legacy `java.awt.Event` based constructor +/// and the serialization support of the JDK class are not provided. +public abstract class AWTEvent extends EventObject { + + public static final long COMPONENT_EVENT_MASK = 0x01; + + public static final long CONTAINER_EVENT_MASK = 0x02; + + public static final long FOCUS_EVENT_MASK = 0x04; + + public static final long KEY_EVENT_MASK = 0x08; + + public static final long MOUSE_EVENT_MASK = 0x10; + + public static final long MOUSE_MOTION_EVENT_MASK = 0x20; + + public static final long WINDOW_EVENT_MASK = 0x40; + + public static final long ACTION_EVENT_MASK = 0x80; + + public static final long ADJUSTMENT_EVENT_MASK = 0x100; + + public static final long ITEM_EVENT_MASK = 0x200; + + public static final long TEXT_EVENT_MASK = 0x400; + + public static final long INPUT_METHOD_EVENT_MASK = 0x800; + + public static final long PAINT_EVENT_MASK = 0x2000; + + public static final long INVOCATION_EVENT_MASK = 0x4000; + + public static final long HIERARCHY_EVENT_MASK = 0x8000; + + public static final long HIERARCHY_BOUNDS_EVENT_MASK = 0x10000; + + public static final long MOUSE_WHEEL_EVENT_MASK = 0x20000; + + public static final long WINDOW_STATE_EVENT_MASK = 0x40000; + + public static final long WINDOW_FOCUS_EVENT_MASK = 0x80000; + + public static final int RESERVED_ID_MAX = 1999; + + /// The event kind within the event class. + protected int id; + + /// Whether a listener consumed the event. + protected boolean consumed; + + public AWTEvent(Object source, int id) { + super(source); + this.id = id; + } + + public void setSource(Object newSource) { + if (source == newSource) { + return; + } + source = newSource; + } + + public int getID() { + return id; + } + + public String paramString() { + return ""; + } + + @Override + public String toString() { + return getClass().getName() + "[" + paramString() + "] on " + source; + } + + protected void consume() { + consumed = true; + } + + protected boolean isConsumed() { + return consumed; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/ActiveEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/ActiveEvent.java new file mode 100644 index 00000000000..f116a50650a --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/ActiveEvent.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// An event that knows how to dispatch itself. +public interface ActiveEvent { + + void dispatch(); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Adjustable.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Adjustable.java new file mode 100644 index 00000000000..1fa84cc1807 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Adjustable.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.event.AdjustmentListener; + +/// A numeric value that can be adjusted within a range, such as a scroll bar. +public interface Adjustable { + + int HORIZONTAL = 0; + + int VERTICAL = 1; + + int NO_ORIENTATION = 2; + + int getOrientation(); + + void setMinimum(int min); + + int getMinimum(); + + void setMaximum(int max); + + int getMaximum(); + + void setUnitIncrement(int u); + + int getUnitIncrement(); + + void setBlockIncrement(int b); + + int getBlockIncrement(); + + void setVisibleAmount(int v); + + int getVisibleAmount(); + + void setValue(int v); + + int getValue(); + + void addAdjustmentListener(AdjustmentListener l); + + void removeAdjustmentListener(AdjustmentListener l); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/AlphaComposite.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/AlphaComposite.java new file mode 100644 index 00000000000..29b3a393783 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/AlphaComposite.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// A Porter-Duff rule with an extra alpha. +/// +/// Every rule can be named and carried around, but a `Graphics2D` only +/// draws with `SRC_OVER` (and `SRC` at full alpha onto an opaque surface, +/// which is the same thing); `setComposite` throws +/// `UnsupportedOperationException` for the others. +public final class AlphaComposite implements Composite { + + public static final int CLEAR = 1; + public static final int SRC = 2; + public static final int DST = 9; + public static final int SRC_OVER = 3; + public static final int DST_OVER = 4; + public static final int SRC_IN = 5; + public static final int DST_IN = 6; + public static final int SRC_OUT = 7; + public static final int DST_OUT = 8; + public static final int SRC_ATOP = 10; + public static final int DST_ATOP = 11; + public static final int XOR = 12; + + public static final AlphaComposite Clear = new AlphaComposite(CLEAR, 1f); + public static final AlphaComposite Src = new AlphaComposite(SRC, 1f); + public static final AlphaComposite Dst = new AlphaComposite(DST, 1f); + public static final AlphaComposite SrcOver = new AlphaComposite(SRC_OVER, 1f); + public static final AlphaComposite DstOver = new AlphaComposite(DST_OVER, 1f); + public static final AlphaComposite SrcIn = new AlphaComposite(SRC_IN, 1f); + public static final AlphaComposite DstIn = new AlphaComposite(DST_IN, 1f); + public static final AlphaComposite SrcOut = new AlphaComposite(SRC_OUT, 1f); + public static final AlphaComposite DstOut = new AlphaComposite(DST_OUT, 1f); + public static final AlphaComposite SrcAtop = new AlphaComposite(SRC_ATOP, 1f); + public static final AlphaComposite DstAtop = new AlphaComposite(DST_ATOP, 1f); + public static final AlphaComposite Xor = new AlphaComposite(XOR, 1f); + + private final int rule; + private final float extraAlpha; + + private AlphaComposite(int rule, float alpha) { + if (rule < CLEAR || rule > XOR) { + throw new IllegalArgumentException("unknown composite rule"); + } + if (!(alpha >= 0.0f && alpha <= 1.0f)) { + throw new IllegalArgumentException("alpha value out of range"); + } + this.rule = rule; + this.extraAlpha = alpha; + } + + public static AlphaComposite getInstance(int rule) { + return new AlphaComposite(rule, 1f); + } + + public static AlphaComposite getInstance(int rule, float alpha) { + return new AlphaComposite(rule, alpha); + } + + public float getAlpha() { + return extraAlpha; + } + + public int getRule() { + return rule; + } + + public AlphaComposite derive(int rule) { + return new AlphaComposite(rule, extraAlpha); + } + + public AlphaComposite derive(float alpha) { + return new AlphaComposite(rule, alpha); + } + + @Override + public int hashCode() { + return Float.floatToIntBits(extraAlpha) * 31 + rule; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AlphaComposite)) { + return false; + } + AlphaComposite ac = (AlphaComposite) obj; + return rule == ac.rule && Float.floatToIntBits(extraAlpha) == Float.floatToIntBits(ac.extraAlpha); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/BasicStroke.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/BasicStroke.java new file mode 100644 index 00000000000..58e07327fbb --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/BasicStroke.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// A pen of a given width, with caps, joins and an optional dash pattern. +/// +/// A `Graphics2D` strokes with it directly: width, cap and join map onto +/// the Codename One stroke and a dash pattern is laid out along the +/// flattened path. [#createStrokedShape(Shape)] -- turning the stroke into +/// an outline to fill -- is not implemented and throws. +public class BasicStroke implements Stroke { + + public static final int JOIN_MITER = 0; + public static final int JOIN_ROUND = 1; + public static final int JOIN_BEVEL = 2; + public static final int CAP_BUTT = 0; + public static final int CAP_ROUND = 1; + public static final int CAP_SQUARE = 2; + + private final float width; + private final int join; + private final int cap; + private final float miterlimit; + private final float[] dash; + private final float dashPhase; + + public BasicStroke(float width, int cap, int join, float miterlimit, float[] dash, float dashPhase) { + if (width < 0.0f) { + throw new IllegalArgumentException("negative width"); + } + if (cap != CAP_BUTT && cap != CAP_ROUND && cap != CAP_SQUARE) { + throw new IllegalArgumentException("illegal end cap value"); + } + if (join == JOIN_MITER) { + if (miterlimit < 1.0f) { + throw new IllegalArgumentException("miter limit < 1"); + } + } else if (join != JOIN_ROUND && join != JOIN_BEVEL) { + throw new IllegalArgumentException("illegal line join value"); + } + if (dash != null) { + if (dashPhase < 0.0f) { + throw new IllegalArgumentException("negative dash phase"); + } + boolean allzero = true; + for (float d : dash) { + if (d > 0.0) { + allzero = false; + } else if (d < 0.0) { + throw new IllegalArgumentException("negative dash length"); + } + } + if (allzero) { + throw new IllegalArgumentException("dash lengths all zero"); + } + } + this.width = width; + this.cap = cap; + this.join = join; + this.miterlimit = miterlimit; + this.dash = dash == null ? null : copy(dash); + this.dashPhase = dashPhase; + } + + public BasicStroke(float width, int cap, int join, float miterlimit) { + this(width, cap, join, miterlimit, null, 0.0f); + } + + public BasicStroke(float width, int cap, int join) { + this(width, cap, join, 10.0f, null, 0.0f); + } + + public BasicStroke(float width) { + this(width, CAP_SQUARE, JOIN_MITER, 10.0f, null, 0.0f); + } + + public BasicStroke() { + this(1.0f, CAP_SQUARE, JOIN_MITER, 10.0f, null, 0.0f); + } + + private static float[] copy(float[] in) { + float[] out = new float[in.length]; + System.arraycopy(in, 0, out, 0, in.length); + return out; + } + + /// Not implemented: the layer strokes shapes itself and has no stroker + /// that returns the outline. + @Override + public Shape createStrokedShape(Shape s) { + throw new UnsupportedOperationException( + "BasicStroke.createStrokedShape is not supported; draw the shape with Graphics2D.draw instead"); + } + + public float getLineWidth() { + return width; + } + + public int getEndCap() { + return cap; + } + + public int getLineJoin() { + return join; + } + + public float getMiterLimit() { + return miterlimit; + } + + public float[] getDashArray() { + return dash == null ? null : copy(dash); + } + + public float getDashPhase() { + return dashPhase; + } + + @Override + public int hashCode() { + int hash = Float.floatToIntBits(width); + hash = hash * 31 + join; + hash = hash * 31 + cap; + hash = hash * 31 + Float.floatToIntBits(miterlimit); + if (dash != null) { + hash = hash * 31 + Float.floatToIntBits(dashPhase); + for (float d : dash) { + hash = hash * 31 + Float.floatToIntBits(d); + } + } + return hash; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BasicStroke)) { + return false; + } + BasicStroke bs = (BasicStroke) obj; + if (Float.floatToIntBits(width) != Float.floatToIntBits(bs.width) || join != bs.join || cap != bs.cap + || Float.floatToIntBits(miterlimit) != Float.floatToIntBits(bs.miterlimit)) { + return false; + } + if (dash == null || bs.dash == null) { + return dash == null && bs.dash == null; + } + if (Float.floatToIntBits(dashPhase) != Float.floatToIntBits(bs.dashPhase) || dash.length != bs.dash.length) { + return false; + } + for (int i = 0; i < dash.length; i++) { + if (Float.floatToIntBits(dash[i]) != Float.floatToIntBits(bs.dash[i])) { + return false; + } + } + return true; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/BorderLayout.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/BorderLayout.java new file mode 100644 index 00000000000..977a983bcf1 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/BorderLayout.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// Lays a container out as a centre surrounded by up to four edge +/// components. +/// +/// There is no component orientation in this layer, so the relative +/// positions resolve as they do left to right: `PAGE_START` is the north +/// edge, `PAGE_END` the south, `LINE_START` the west and `LINE_END` the +/// east. A component added under a relative name takes precedence over one +/// added under the absolute name of the same edge. +public class BorderLayout implements LayoutManager2 { + + public static final String NORTH = "North"; + + public static final String SOUTH = "South"; + + public static final String EAST = "East"; + + public static final String WEST = "West"; + + public static final String CENTER = "Center"; + + public static final String BEFORE_FIRST_LINE = "First"; + + public static final String AFTER_LAST_LINE = "Last"; + + public static final String BEFORE_LINE_BEGINS = "Before"; + + public static final String AFTER_LINE_ENDS = "After"; + + public static final String PAGE_START = BEFORE_FIRST_LINE; + + public static final String PAGE_END = AFTER_LAST_LINE; + + public static final String LINE_START = BEFORE_LINE_BEGINS; + + public static final String LINE_END = AFTER_LINE_ENDS; + + private static final int N = 0; + private static final int S = 1; + private static final int E = 2; + private static final int W = 3; + private static final int C = 4; + private static final int FIRST_LINE = 5; + private static final int LAST_LINE = 6; + private static final int FIRST_ITEM = 7; + private static final int LAST_ITEM = 8; + private static final String[] NAMES = { + NORTH, SOUTH, EAST, WEST, CENTER, BEFORE_FIRST_LINE, AFTER_LAST_LINE, BEFORE_LINE_BEGINS, AFTER_LINE_ENDS + }; + + private int hgap; + private int vgap; + private final Component[] slots = new Component[NAMES.length]; + + public BorderLayout() { + this(0, 0); + } + + public BorderLayout(int hgap, int vgap) { + this.hgap = hgap; + this.vgap = vgap; + } + + public int getHgap() { + return hgap; + } + + public void setHgap(int hgap) { + this.hgap = hgap; + } + + public int getVgap() { + return vgap; + } + + public void setVgap(int vgap) { + this.vgap = vgap; + } + + private static int slotOf(Object name) { + for (int i = 0; i < NAMES.length; i++) { + if (NAMES[i].equals(name)) { + return i; + } + } + return -1; + } + + @Override + public void addLayoutComponent(Component comp, Object constraints) { + if (constraints == null || constraints instanceof String) { + addLayoutComponent((String) constraints, comp); + } else { + throw new IllegalArgumentException("cannot add to layout: constraint must be a string (or null)"); + } + } + + @Override + public void addLayoutComponent(String name, Component comp) { + int slot = name == null ? C : slotOf(name); + if (slot < 0) { + throw new IllegalArgumentException("cannot add to layout: unknown constraint: " + name); + } + slots[slot] = comp; + } + + @Override + public void removeLayoutComponent(Component comp) { + for (int i = 0; i < slots.length; i++) { + if (slots[i] == comp) { + slots[i] = null; + } + } + } + + public Component getLayoutComponent(Object constraints) { + int slot = slotOf(constraints); + if (slot < 0) { + throw new IllegalArgumentException("cannot get component: unknown constraint: " + constraints); + } + return slots[slot]; + } + + public Component getLayoutComponent(Container target, Object constraints) { + int slot = slotOf(constraints); + if (slot < 0 || slot > C) { + throw new IllegalArgumentException("cannot get component: invalid constraint: " + constraints); + } + return resolve(slot); + } + + public Object getConstraints(Component comp) { + if (comp == null) { + return null; + } + for (int i = 0; i < slots.length; i++) { + if (slots[i] == comp) { + return NAMES[i]; + } + } + return null; + } + + /// The component occupying an absolute edge, a relative name winning + /// over the absolute one. + private Component resolve(int edge) { + Component c = null; + switch (edge) { + case N: + c = slots[FIRST_LINE]; + break; + case S: + c = slots[LAST_LINE]; + break; + case W: + c = slots[FIRST_ITEM]; + break; + case E: + c = slots[LAST_ITEM]; + break; + default: + break; + } + return c != null ? c : slots[edge]; + } + + private Component visible(int edge) { + Component c = resolve(edge); + return c != null && c.isVisible() ? c : null; + } + + @Override + public Dimension minimumLayoutSize(Container target) { + return measure(target, false); + } + + @Override + public Dimension preferredLayoutSize(Container target) { + return measure(target, true); + } + + private static Dimension sizeOf(Component c, boolean preferred) { + return preferred ? c.getPreferredSize() : c.getMinimumSize(); + } + + private Dimension measure(Container target, boolean preferred) { + int w = 0; + int h = 0; + Component c = visible(E); + if (c != null) { + Dimension d = sizeOf(c, preferred); + w += d.width + hgap; + h = Math.max(d.height, h); + } + c = visible(W); + if (c != null) { + Dimension d = sizeOf(c, preferred); + w += d.width + hgap; + h = Math.max(d.height, h); + } + c = visible(C); + if (c != null) { + Dimension d = sizeOf(c, preferred); + w += d.width; + h = Math.max(d.height, h); + } + c = visible(N); + if (c != null) { + Dimension d = sizeOf(c, preferred); + w = Math.max(d.width, w); + h += d.height + vgap; + } + c = visible(S); + if (c != null) { + Dimension d = sizeOf(c, preferred); + w = Math.max(d.width, w); + h += d.height + vgap; + } + Insets in = target.getInsets(); + return new Dimension(w + in.left + in.right, h + in.top + in.bottom); + } + + @Override + public Dimension maximumLayoutSize(Container target) { + return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + @Override + public float getLayoutAlignmentX(Container parent) { + return 0.5f; + } + + @Override + public float getLayoutAlignmentY(Container parent) { + return 0.5f; + } + + @Override + public void invalidateLayout(Container target) { + } + + @Override + public void layoutContainer(Container target) { + Insets in = target.getInsets(); + int top = in.top; + int bottom = target.getHeight() - in.bottom; + int left = in.left; + int right = target.getWidth() - in.right; + + Component c = visible(N); + if (c != null) { + c.setSize(right - left, c.getHeight()); + Dimension d = c.getPreferredSize(); + c.setBounds(left, top, right - left, d.height); + top += d.height + vgap; + } + c = visible(S); + if (c != null) { + c.setSize(right - left, c.getHeight()); + Dimension d = c.getPreferredSize(); + c.setBounds(left, bottom - d.height, right - left, d.height); + bottom -= d.height + vgap; + } + c = visible(E); + if (c != null) { + c.setSize(c.getWidth(), bottom - top); + Dimension d = c.getPreferredSize(); + c.setBounds(right - d.width, top, d.width, bottom - top); + right -= d.width + hgap; + } + c = visible(W); + if (c != null) { + c.setSize(c.getWidth(), bottom - top); + Dimension d = c.getPreferredSize(); + c.setBounds(left, top, d.width, bottom - top); + left += d.width + hgap; + } + c = visible(C); + if (c != null) { + c.setBounds(left, top, right - left, bottom - top); + } + } + + @Override + public String toString() { + return getClass().getName() + "[hgap=" + hgap + ",vgap=" + vgap + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Canvas.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Canvas.java new file mode 100644 index 00000000000..9e280668e36 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Canvas.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// A blank rectangle an application paints by overriding `paint`. The +/// background is cleared before each paint, as a desktop canvas is. +public class Canvas extends Component { + + public Canvas() { + } + + @Override + public boolean isOpaque() { + return true; + } + + @Override + public void paint(Graphics g) { + g.clearRect(0, 0, getWidth(), getHeight()); + } + + @Override + public void update(Graphics g) { + g.clearRect(0, 0, getWidth(), getHeight()); + paint(g); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/CardLayout.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/CardLayout.java new file mode 100644 index 00000000000..d16823947fe --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/CardLayout.java @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import java.util.ArrayList; + +/// Shows one child of a container at a time, each child filling the +/// container like a card in a stack. +public class CardLayout implements LayoutManager2 { + + private final ArrayList names = new ArrayList(); + private final ArrayList cards = new ArrayList(); + private int hgap; + private int vgap; + + public CardLayout() { + this(0, 0); + } + + public CardLayout(int hgap, int vgap) { + this.hgap = hgap; + this.vgap = vgap; + } + + public int getHgap() { + return hgap; + } + + public void setHgap(int hgap) { + this.hgap = hgap; + } + + public int getVgap() { + return vgap; + } + + public void setVgap(int vgap) { + this.vgap = vgap; + } + + @Override + public void addLayoutComponent(Component comp, Object constraints) { + if (constraints == null) { + constraints = ""; + } + if (constraints instanceof String) { + addLayoutComponent((String) constraints, comp); + } else { + throw new IllegalArgumentException("cannot add to layout: constraint must be a string"); + } + } + + @Override + public void addLayoutComponent(String name, Component comp) { + if (!cards.isEmpty()) { + comp.setVisible(false); + } + for (int i = 0; i < names.size(); i++) { + if (names.get(i).equals(name)) { + cards.set(i, comp); + return; + } + } + names.add(name); + cards.add(comp); + } + + @Override + public void removeLayoutComponent(Component comp) { + for (int i = 0; i < cards.size(); i++) { + if (cards.get(i) == comp) { + if (comp.isVisible() && comp.getParent() != null) { + next(comp.getParent()); + } + cards.remove(i); + names.remove(i); + break; + } + } + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + return measure(parent, true); + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return measure(parent, false); + } + + private Dimension measure(Container parent, boolean preferred) { + Insets in = parent.getInsets(); + int n = parent.getComponentCount(); + int w = 0; + int h = 0; + for (int i = 0; i < n; i++) { + Component c = parent.getComponent(i); + Dimension d = preferred ? c.getPreferredSize() : c.getMinimumSize(); + if (d.width > w) { + w = d.width; + } + if (d.height > h) { + h = d.height; + } + } + return new Dimension(in.left + in.right + w + hgap * 2, in.top + in.bottom + h + vgap * 2); + } + + @Override + public Dimension maximumLayoutSize(Container target) { + return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + @Override + public float getLayoutAlignmentX(Container parent) { + return 0.5f; + } + + @Override + public float getLayoutAlignmentY(Container parent) { + return 0.5f; + } + + @Override + public void invalidateLayout(Container target) { + } + + @Override + public void layoutContainer(Container parent) { + Insets in = parent.getInsets(); + int n = parent.getComponentCount(); + boolean shown = false; + for (int i = 0; i < n; i++) { + Component c = parent.getComponent(i); + c.setBounds(hgap + in.left, vgap + in.top, parent.getWidth() - (hgap * 2 + in.left + in.right), + parent.getHeight() - (vgap * 2 + in.top + in.bottom)); + if (c.isVisible()) { + shown = true; + } + } + if (!shown && n > 0) { + parent.getComponent(0).setVisible(true); + } + } + + private void check(Container parent) { + if (parent.getLayout() != this) { + throw new IllegalArgumentException("wrong parent for CardLayout"); + } + } + + /// Hides whichever child is visible and shows the one at `index`, + /// counted from the visible child when `relative` is set. + private void flip(Container parent, int index, boolean relative) { + check(parent); + int n = parent.getComponentCount(); + if (n == 0) { + return; + } + int target = index; + boolean found = false; + for (int i = 0; i < n; i++) { + Component c = parent.getComponent(i); + if (c.isVisible()) { + if (relative) { + target = (i + index + n) % n; + } + c.setVisible(false); + found = true; + break; + } + } + if (relative && !found) { + target = index > 0 ? 0 : n - 1; + } + if (target < 0) { + target = n - 1; + } + parent.getComponent(target).setVisible(true); + parent.validate(); + } + + public void first(Container parent) { + flip(parent, 0, false); + } + + public void next(Container parent) { + flip(parent, 1, true); + } + + public void previous(Container parent) { + flip(parent, -1, true); + } + + public void last(Container parent) { + flip(parent, -1, false); + } + + public void show(Container parent, String name) { + check(parent); + Component next = null; + for (int i = 0; i < names.size(); i++) { + if (names.get(i).equals(name)) { + next = cards.get(i); + break; + } + } + if (next == null || next.isVisible()) { + return; + } + int n = parent.getComponentCount(); + for (int i = 0; i < n; i++) { + Component c = parent.getComponent(i); + if (c.isVisible()) { + c.setVisible(false); + break; + } + } + next.setVisible(true); + parent.validate(); + } + + @Override + public String toString() { + return getClass().getName() + "[hgap=" + hgap + ",vgap=" + vgap + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Color.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Color.java new file mode 100644 index 00000000000..3be6018f504 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Color.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// An sRGB color with an alpha channel; immutable. +public class Color implements Paint { + + public static final Color white = new Color(255, 255, 255); + public static final Color WHITE = white; + public static final Color lightGray = new Color(192, 192, 192); + public static final Color LIGHT_GRAY = lightGray; + public static final Color gray = new Color(128, 128, 128); + public static final Color GRAY = gray; + public static final Color darkGray = new Color(64, 64, 64); + public static final Color DARK_GRAY = darkGray; + public static final Color black = new Color(0, 0, 0); + public static final Color BLACK = black; + public static final Color red = new Color(255, 0, 0); + public static final Color RED = red; + public static final Color pink = new Color(255, 175, 175); + public static final Color PINK = pink; + public static final Color orange = new Color(255, 200, 0); + public static final Color ORANGE = orange; + public static final Color yellow = new Color(255, 255, 0); + public static final Color YELLOW = yellow; + public static final Color green = new Color(0, 255, 0); + public static final Color GREEN = green; + public static final Color magenta = new Color(255, 0, 255); + public static final Color MAGENTA = magenta; + public static final Color cyan = new Color(0, 255, 255); + public static final Color CYAN = cyan; + public static final Color blue = new Color(0, 0, 255); + public static final Color BLUE = blue; + + private static final double FACTOR = 0.7; + + private final int value; + + public Color(int r, int g, int b) { + this(r, g, b, 255); + } + + public Color(int r, int g, int b, int a) { + check(r); + check(g); + check(b); + check(a); + value = (a << 24) | (r << 16) | (g << 8) | b; + } + + public Color(int rgb) { + value = 0xff000000 | rgb; + } + + public Color(int rgba, boolean hasalpha) { + value = hasalpha ? rgba : 0xff000000 | rgba; + } + + public Color(float r, float g, float b) { + this((int) (r * 255 + 0.5), (int) (g * 255 + 0.5), (int) (b * 255 + 0.5)); + } + + public Color(float r, float g, float b, float a) { + this((int) (r * 255 + 0.5), (int) (g * 255 + 0.5), (int) (b * 255 + 0.5), (int) (a * 255 + 0.5)); + } + + private static void check(int component) { + if (component < 0 || component > 255) { + throw new IllegalArgumentException("Color parameter outside of expected range"); + } + } + + public int getRed() { + return (value >> 16) & 0xff; + } + + public int getGreen() { + return (value >> 8) & 0xff; + } + + public int getBlue() { + return value & 0xff; + } + + public int getAlpha() { + return (value >> 24) & 0xff; + } + + public int getRGB() { + return value; + } + + public Color brighter() { + int r = getRed(); + int g = getGreen(); + int b = getBlue(); + int alpha = getAlpha(); + int i = (int) (1.0 / (1.0 - FACTOR)); + if (r == 0 && g == 0 && b == 0) { + return new Color(i, i, i, alpha); + } + if (r > 0 && r < i) { + r = i; + } + if (g > 0 && g < i) { + g = i; + } + if (b > 0 && b < i) { + b = i; + } + return new Color(Math.min((int) (r / FACTOR), 255), Math.min((int) (g / FACTOR), 255), + Math.min((int) (b / FACTOR), 255), alpha); + } + + public Color darker() { + return new Color(Math.max((int) (getRed() * FACTOR), 0), Math.max((int) (getGreen() * FACTOR), 0), + Math.max((int) (getBlue() * FACTOR), 0), getAlpha()); + } + + @Override + public int hashCode() { + return value; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Color && ((Color) obj).value == value; + } + + @Override + public String toString() { + return "java.awt.Color[r=" + getRed() + ",g=" + getGreen() + ",b=" + getBlue() + "]"; + } + + /// Parses a decimal, `0x`/`#` hexadecimal or `0` octal number as an + /// opaque color. + public static Color decode(String nm) throws NumberFormatException { + String s = nm.trim(); + int radix = 10; + if (s.startsWith("0x") || s.startsWith("0X")) { + s = s.substring(2); + radix = 16; + } else if (s.startsWith("#")) { + s = s.substring(1); + radix = 16; + } else if (s.startsWith("0") && s.length() > 1) { + s = s.substring(1); + radix = 8; + } + int i = (int) Long.parseLong(s, radix); + return new Color((i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff); + } + + public static int HSBtoRGB(float hue, float saturation, float brightness) { + int r = 0; + int g = 0; + int b = 0; + if (saturation == 0) { + r = (int) (brightness * 255.0f + 0.5f); + g = r; + b = r; + } else { + float h = (hue - (float) Math.floor(hue)) * 6.0f; + float f = h - (float) Math.floor(h); + float p = brightness * (1.0f - saturation); + float q = brightness * (1.0f - saturation * f); + float t = brightness * (1.0f - (saturation * (1.0f - f))); + float fr; + float fg; + float fb; + switch ((int) h) { + case 0: + fr = brightness; + fg = t; + fb = p; + break; + case 1: + fr = q; + fg = brightness; + fb = p; + break; + case 2: + fr = p; + fg = brightness; + fb = t; + break; + case 3: + fr = p; + fg = q; + fb = brightness; + break; + case 4: + fr = t; + fg = p; + fb = brightness; + break; + default: + fr = brightness; + fg = p; + fb = q; + break; + } + r = (int) (fr * 255.0f + 0.5f); + g = (int) (fg * 255.0f + 0.5f); + b = (int) (fb * 255.0f + 0.5f); + } + return 0xff000000 | (r << 16) | (g << 8) | b; + } + + public static float[] RGBtoHSB(int r, int g, int b, float[] hsbvals) { + float[] out = hsbvals == null ? new float[3] : hsbvals; + int cmax = Math.max(r, Math.max(g, b)); + int cmin = Math.min(r, Math.min(g, b)); + float brightness = cmax / 255.0f; + float saturation = cmax != 0 ? (cmax - cmin) / (float) cmax : 0; + float hue = 0; + if (saturation != 0) { + float span = cmax - cmin; + float redc = (cmax - r) / span; + float greenc = (cmax - g) / span; + float bluec = (cmax - b) / span; + if (r == cmax) { + hue = bluec - greenc; + } else if (g == cmax) { + hue = 2.0f + redc - bluec; + } else { + hue = 4.0f + greenc - redc; + } + hue = hue / 6.0f; + if (hue < 0) { + hue = hue + 1.0f; + } + } + out[0] = hue; + out[1] = saturation; + out[2] = brightness; + return out; + } + + public static Color getHSBColor(float h, float s, float b) { + return new Color(HSBtoRGB(h, s, b)); + } + + public float[] getRGBComponents(float[] compArray) { + float[] f = compArray == null ? new float[4] : compArray; + f[0] = getRed() / 255f; + f[1] = getGreen() / 255f; + f[2] = getBlue() / 255f; + f[3] = getAlpha() / 255f; + return f; + } + + public float[] getRGBColorComponents(float[] compArray) { + float[] f = compArray == null ? new float[3] : compArray; + f[0] = getRed() / 255f; + f[1] = getGreen() / 255f; + f[2] = getBlue() / 255f; + return f; + } + + @Override + public int getTransparency() { + int alpha = getAlpha(); + return alpha == 0xff ? OPAQUE : alpha == 0 ? BITMASK : TRANSLUCENT; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Component.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Component.java new file mode 100644 index 00000000000..140ac26a00b --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Component.java @@ -0,0 +1,1029 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.event.ComponentEvent; +import com.codename1.desktopcompat.java.awt.event.ComponentListener; +import com.codename1.desktopcompat.java.awt.event.FocusEvent; +import com.codename1.desktopcompat.java.awt.event.FocusListener; +import com.codename1.desktopcompat.java.awt.event.KeyEvent; +import com.codename1.desktopcompat.java.awt.event.KeyListener; +import com.codename1.desktopcompat.java.awt.event.MouseEvent; +import com.codename1.desktopcompat.java.awt.event.MouseListener; +import com.codename1.desktopcompat.java.awt.event.MouseMotionListener; +import com.codename1.desktopcompat.java.awt.event.MouseWheelEvent; +import com.codename1.desktopcompat.java.awt.event.MouseWheelListener; +import com.codename1.desktopcompat.java.awt.image.BufferedImage; +import com.codename1.desktopcompat.java.awt.image.ImageObserver; +import com.codename1.desktopcompat.java.beans.PropertyChangeListener; +import com.codename1.desktopcompat.java.beans.PropertyChangeSupport; +import com.codename1.desktopcompat.rt.CanvasPeer; +import com.codename1.desktopcompat.rt.EventBridge; +import com.codename1.desktopcompat.rt.Fonts; +import com.codename1.desktopcompat.rt.G2D; +import com.codename1.desktopcompat.rt.Peer; +import com.codename1.desktopcompat.rt.PeerSupport; +import com.codename1.desktopcompat.rt.Units; +import java.util.ArrayList; + +/// The base of every AWT and Swing component. +/// +/// A component keeps its own state -- bounds in logical pixels, colors, +/// font, listeners -- and is shown through one Codename One component, its +/// peer, which is made the first time something asks for it. The members +/// whose names start with `cn1` are the contract between a component and +/// its peer; they are not part of the desktop API. +/// +/// Not supported: `getGraphics()` answers `null` (paint from `paint` or +/// `paintComponent` and ask for it with `repaint`), there is no component +/// orientation, no drag and drop, no input methods and no focus traversal +/// policy. +public abstract class Component implements ImageObserver { + + public static final float TOP_ALIGNMENT = 0.0f; + public static final float CENTER_ALIGNMENT = 0.5f; + public static final float BOTTOM_ALIGNMENT = 1.0f; + public static final float LEFT_ALIGNMENT = 0.0f; + public static final float RIGHT_ALIGNMENT = 1.0f; + + private static final Object TREE_LOCK = new Object(); + + private int x; + private int y; + private int width; + private int height; + private Container parent; + private String name; + private boolean visible = true; + private boolean enabled = true; + private boolean valid; + private boolean displayable; + private boolean focusable = true; + private boolean ignoreRepaint; + private Color foreground; + private Color background; + private Font font; + private Cursor cursor; + private Dimension prefSize; + private Dimension minSize; + private Dimension maxSize; + private long eventMask; + private com.codename1.ui.Component peer; + private PropertyChangeSupport changeSupport; + private ArrayList componentListeners; + private ArrayList focusListeners; + private ArrayList keyListeners; + private ArrayList mouseListeners; + private ArrayList mouseMotionListeners; + private ArrayList mouseWheelListeners; + + protected Component() { + } + + // ------------------------------------------------------------ peer + + /// The Codename One component that shows this one, made on first use. + public final com.codename1.ui.Component cn1Peer() { + if (peer == null) { + peer = cn1CreatePeer(); + cn1SyncPeerBounds(); + peer.setVisible(visible); + peer.setEnabled(enabled); + cn1PeerCreated(); + } + return peer; + } + + /// The peer if it was made already, else `null`. + public final com.codename1.ui.Component cn1PeerOrNull() { + return peer; + } + + /// Makes the peer. A widget overrides this to answer the Codename One + /// widget that backs it; the answer must implement + /// [com.codename1.desktopcompat.rt.Peer]. The default is a blank canvas. + protected com.codename1.ui.Component cn1CreatePeer() { + return new CanvasPeer(this); + } + + /// Called once, right after the peer was made and given this + /// component's bounds, visibility and enabled state. + protected void cn1PeerCreated() { + cn1PeerSupportApply(); + } + + private void cn1PeerSupportApply() { + if (peer instanceof Peer) { + ((Peer) peer).support().applyStyle(); + } + } + + /// Pushes this component's bounds to its peer, snapped to device pixels. + /// A window overrides it with nothing: its host places it. + public void cn1SyncPeerBounds() { + if (peer != null) { + peer.setX(Units.toDevice(x)); + peer.setY(Units.toDevice(y)); + peer.setWidth(Units.toDeviceSize(x, width)); + peer.setHeight(Units.toDeviceSize(y, height)); + } + } + + /// Tells a component whose peer is placed by Codename One -- the root + /// of a peer tree -- the size its peer was given, in logical pixels. + public void cn1PeerResized(int w, int h) { + if (w != width || h != height) { + width = w; + height = h; + invalidate(); + fireComponentEvent(ComponentEvent.COMPONENT_RESIZED); + } + } + + /// The preferred size of the Codename One widget behind a component + /// that has a native look, in logical pixels; `null` for the others. + protected Dimension cn1NativePreferredSize() { + if (!com.codename1.ui.Display.isInitialized()) { + return null; + } + com.codename1.ui.Component p = cn1Peer(); + if (p instanceof Peer && ((Peer) p).nativeLook()) { + return new Dimension(Units.toLogicalCeil(p.getPreferredW()), Units.toLogicalCeil(p.getPreferredH())); + } + return null; + } + + /// Paints the Codename One widget behind this component into `g`, at + /// the origin, and answers `true`; answers `false` and paints nothing + /// when the component has no native look, there is no display, or `g` + /// is not a graphics of this layer. + public boolean cn1PaintNative(Graphics g) { + if (!(g instanceof G2D) || !com.codename1.ui.Display.isInitialized()) { + return false; + } + com.codename1.ui.Component p = cn1Peer(); + if (p instanceof Peer && ((Peer) p).nativeLook()) { + ((Peer) p).support().paintNative((G2D) g); + return true; + } + return false; + } + + /// Whether mouse events over this component are delivered to it rather + /// than to an ancestor: it has a mouse, mouse motion or mouse wheel + /// listener, or enabled those events. + public boolean cn1WantsMouse() { + return notEmpty(mouseListeners) || notEmpty(mouseMotionListeners) || notEmpty(mouseWheelListeners) + || (eventMask & (AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK + | AWTEvent.MOUSE_WHEEL_EVENT_MASK)) != 0; + } + + private static boolean notEmpty(ArrayList l) { + return l != null && !l.isEmpty(); + } + + void cn1SetParent(Container p) { + parent = p; + } + + // ------------------------------------------------------------ basics + + public String getName() { + return name; + } + + public void setName(String name) { + String old = this.name; + this.name = name; + firePropertyChange("name", old, name); + } + + public Container getParent() { + return parent; + } + + public Toolkit getToolkit() { + return Toolkit.getDefaultToolkit(); + } + + /// A single object shared by every component. Nothing in this layer + /// locks it: the component tree belongs to the event dispatch thread. + public final Object getTreeLock() { + return TREE_LOCK; + } + + public boolean isValid() { + return valid && displayable; + } + + /// Whether the component is part of a window that was shown or packed. + public boolean isDisplayable() { + return displayable; + } + + public boolean isVisible() { + return visible; + } + + public boolean isShowing() { + return visible && displayable && parent != null && parent.isShowing(); + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean b) { + if (enabled != b) { + enabled = b; + if (peer != null) { + peer.setEnabled(b); + } + firePropertyChange("enabled", !b, b); + repaint(); + } + } + + public void setVisible(boolean b) { + if (visible != b) { + visible = b; + if (peer != null) { + peer.setVisible(b); + } + fireComponentEvent(b ? ComponentEvent.COMPONENT_SHOWN : ComponentEvent.COMPONENT_HIDDEN); + if (parent != null) { + parent.invalidate(); + PeerSupport.scheduleLayout(this); + } + } + } + + /// Always `true`: every component of this layer is drawn by its parent. + public boolean isLightweight() { + return true; + } + + public boolean isOpaque() { + return false; + } + + public boolean isDoubleBuffered() { + return false; + } + + // ------------------------------------------------------------ look + + public Color getForeground() { + if (foreground != null) { + return foreground; + } + return parent != null ? parent.getForeground() : null; + } + + public void setForeground(Color c) { + Color old = foreground; + foreground = c; + cn1PeerSupportApply(); + firePropertyChange("foreground", old, c); + repaint(); + } + + public boolean isForegroundSet() { + return foreground != null; + } + + public Color getBackground() { + if (background != null) { + return background; + } + return parent != null ? parent.getBackground() : null; + } + + public void setBackground(Color c) { + Color old = background; + background = c; + cn1PeerSupportApply(); + firePropertyChange("background", old, c); + repaint(); + } + + public boolean isBackgroundSet() { + return background != null; + } + + public Font getFont() { + if (font != null) { + return font; + } + return parent != null ? parent.getFont() : null; + } + + public void setFont(Font f) { + Font old = font; + font = f; + cn1PeerSupportApply(); + firePropertyChange("font", old, f); + if (f != old && (old == null || !old.equals(f))) { + invalidate(); + } + } + + public boolean isFontSet() { + return font != null; + } + + /// Recorded only; the pointer's shape does not change. + public void setCursor(Cursor cursor) { + this.cursor = cursor; + } + + public Cursor getCursor() { + if (cursor != null) { + return cursor; + } + return parent != null ? parent.getCursor() : Cursor.getDefaultCursor(); + } + + public boolean isCursorSet() { + return cursor != null; + } + + public FontMetrics getFontMetrics(Font font) { + return Fonts.metrics(font); + } + + // ------------------------------------------------------------ bounds + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public Point getLocation() { + return new Point(x, y); + } + + public Point getLocation(Point rv) { + if (rv == null) { + return new Point(x, y); + } + rv.setLocation(x, y); + return rv; + } + + public void setLocation(int x, int y) { + setBounds(x, y, width, height); + } + + public void setLocation(Point p) { + setLocation(p.x, p.y); + } + + public Dimension getSize() { + return new Dimension(width, height); + } + + public Dimension getSize(Dimension rv) { + if (rv == null) { + return new Dimension(width, height); + } + rv.setSize(width, height); + return rv; + } + + public void setSize(int width, int height) { + setBounds(x, y, width, height); + } + + public void setSize(Dimension d) { + setSize(d.width, d.height); + } + + public Rectangle getBounds() { + return new Rectangle(x, y, width, height); + } + + public Rectangle getBounds(Rectangle rv) { + if (rv == null) { + return new Rectangle(x, y, width, height); + } + rv.setBounds(x, y, width, height); + return rv; + } + + public void setBounds(Rectangle r) { + setBounds(r.x, r.y, r.width, r.height); + } + + public void setBounds(int x, int y, int width, int height) { + boolean resized = this.width != width || this.height != height; + boolean moved = this.x != x || this.y != y; + if (!resized && !moved) { + return; + } + this.x = x; + this.y = y; + this.width = width; + this.height = height; + cn1SyncPeerBounds(); + if (resized) { + invalidate(); + fireComponentEvent(ComponentEvent.COMPONENT_RESIZED); + } + if (moved) { + fireComponentEvent(ComponentEvent.COMPONENT_MOVED); + } + } + + public Point getLocationOnScreen() { + Point p = parent != null ? parent.getLocationOnScreen() : new Point(0, 0); + p.translate(x, y); + return p; + } + + public boolean contains(int x, int y) { + return x >= 0 && x < width && y >= 0 && y < height; + } + + public boolean contains(Point p) { + return contains(p.x, p.y); + } + + public Component getComponentAt(int x, int y) { + return contains(x, y) ? this : null; + } + + public Component getComponentAt(Point p) { + return getComponentAt(p.x, p.y); + } + + // ------------------------------------------------------------ sizes + + public void setPreferredSize(Dimension preferredSize) { + Dimension old = prefSize; + prefSize = preferredSize == null ? null : new Dimension(preferredSize); + firePropertyChange("preferredSize", old, preferredSize); + } + + public boolean isPreferredSizeSet() { + return prefSize != null; + } + + /// The size set with `setPreferredSize`; else the preferred size of + /// the Codename One widget behind a component with a native look; else + /// the minimum size. + public Dimension getPreferredSize() { + if (prefSize != null) { + return new Dimension(prefSize); + } + Dimension d = cn1NativePreferredSize(); + return d != null ? d : getMinimumSize(); + } + + public void setMinimumSize(Dimension minimumSize) { + Dimension old = minSize; + minSize = minimumSize == null ? null : new Dimension(minimumSize); + firePropertyChange("minimumSize", old, minimumSize); + } + + public boolean isMinimumSizeSet() { + return minSize != null; + } + + public Dimension getMinimumSize() { + if (minSize != null) { + return new Dimension(minSize); + } + Dimension d = cn1NativePreferredSize(); + return d != null ? d : new Dimension(width, height); + } + + public void setMaximumSize(Dimension maximumSize) { + Dimension old = maxSize; + maxSize = maximumSize == null ? null : new Dimension(maximumSize); + firePropertyChange("maximumSize", old, maximumSize); + } + + public boolean isMaximumSizeSet() { + return maxSize != null; + } + + public Dimension getMaximumSize() { + if (maxSize != null) { + return new Dimension(maxSize); + } + return new Dimension(Short.MAX_VALUE, Short.MAX_VALUE); + } + + public float getAlignmentX() { + return CENTER_ALIGNMENT; + } + + public float getAlignmentY() { + return CENTER_ALIGNMENT; + } + + public int getBaseline(int width, int height) { + if (width < 0 || height < 0) { + throw new IllegalArgumentException("Width and height must be >= 0"); + } + return -1; + } + + // ------------------------------------------------------------ layout + + public void doLayout() { + } + + public void validate() { + valid = true; + } + + public void invalidate() { + valid = false; + if (peer != null) { + peer.setShouldCalcPreferredSize(true); + } + if (parent != null && parent.cn1Valid()) { + parent.invalidate(); + } + } + + /// Invalidates the component and has its window laid out again on the + /// next turn of the event dispatch thread. + public void revalidate() { + invalidate(); + PeerSupport.scheduleLayout(this); + } + + boolean cn1Valid() { + return valid; + } + + void cn1SetValid(boolean v) { + valid = v; + } + + /// Called when the component joins a tree that is shown. + public void addNotify() { + displayable = true; + } + + public void removeNotify() { + displayable = false; + EventBridge.forget(this); + } + + // ------------------------------------------------------------ paint + + /// Always `null`: a component is painted only from `paint`, with the + /// graphics it is handed. + public Graphics getGraphics() { + return null; + } + + public void paint(Graphics g) { + } + + public void update(Graphics g) { + paint(g); + } + + public void paintAll(Graphics g) { + paint(g); + } + + public void print(Graphics g) { + paint(g); + } + + public void printAll(Graphics g) { + paintAll(g); + } + + public void repaint() { + repaint(0, 0, 0, width, height); + } + + public void repaint(long tm) { + repaint(tm, 0, 0, width, height); + } + + public void repaint(int x, int y, int width, int height) { + repaint(0, x, y, width, height); + } + + /// Asks for the rectangle to be painted on the next paint cycle. The + /// delay is ignored. + public void repaint(long tm, int x, int y, int width, int height) { + if (peer != null && !ignoreRepaint && width > 0 && height > 0) { + PeerSupport.repaint(this, x, y, width, height); + } + } + + public void setIgnoreRepaint(boolean ignoreRepaint) { + this.ignoreRepaint = ignoreRepaint; + } + + public boolean getIgnoreRepaint() { + return ignoreRepaint; + } + + @Override + public boolean imageUpdate(Image img, int infoflags, int x, int y, int w, int h) { + return false; + } + + /// An off screen image of the given size, in image pixels. + public Image createImage(int width, int height) { + return new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_ARGB); + } + + // ------------------------------------------------------------ focus + + public boolean isFocusable() { + return focusable; + } + + public void setFocusable(boolean focusable) { + boolean old = this.focusable; + this.focusable = focusable; + firePropertyChange("focusable", old, focusable); + } + + /// Makes this component the focus owner: the component key events go + /// to. Focus is granted at once. + public void requestFocus() { + requestFocusInWindow(); + } + + public boolean requestFocusInWindow() { + if (!focusable || !visible || !enabled) { + return false; + } + EventBridge.setFocusOwner(this, true); + return true; + } + + public boolean isFocusOwner() { + return EventBridge.focusOwner() == this; + } + + public boolean hasFocus() { + return isFocusOwner(); + } + + // ------------------------------------------------------------ listeners + + public void addComponentListener(ComponentListener l) { + if (l != null) { + if (componentListeners == null) { + componentListeners = new ArrayList(); + } + componentListeners.add(l); + } + } + + public void removeComponentListener(ComponentListener l) { + if (componentListeners != null) { + componentListeners.remove(l); + } + } + + public ComponentListener[] getComponentListeners() { + return componentListeners == null ? new ComponentListener[0] + : componentListeners.toArray(new ComponentListener[componentListeners.size()]); + } + + public void addFocusListener(FocusListener l) { + if (l != null) { + if (focusListeners == null) { + focusListeners = new ArrayList(); + } + focusListeners.add(l); + } + } + + public void removeFocusListener(FocusListener l) { + if (focusListeners != null) { + focusListeners.remove(l); + } + } + + public FocusListener[] getFocusListeners() { + return focusListeners == null ? new FocusListener[0] + : focusListeners.toArray(new FocusListener[focusListeners.size()]); + } + + public void addKeyListener(KeyListener l) { + if (l != null) { + if (keyListeners == null) { + keyListeners = new ArrayList(); + } + keyListeners.add(l); + } + } + + public void removeKeyListener(KeyListener l) { + if (keyListeners != null) { + keyListeners.remove(l); + } + } + + public KeyListener[] getKeyListeners() { + return keyListeners == null ? new KeyListener[0] : keyListeners.toArray(new KeyListener[keyListeners.size()]); + } + + public void addMouseListener(MouseListener l) { + if (l != null) { + if (mouseListeners == null) { + mouseListeners = new ArrayList(); + } + mouseListeners.add(l); + } + } + + public void removeMouseListener(MouseListener l) { + if (mouseListeners != null) { + mouseListeners.remove(l); + } + } + + public MouseListener[] getMouseListeners() { + return mouseListeners == null ? new MouseListener[0] + : mouseListeners.toArray(new MouseListener[mouseListeners.size()]); + } + + public void addMouseMotionListener(MouseMotionListener l) { + if (l != null) { + if (mouseMotionListeners == null) { + mouseMotionListeners = new ArrayList(); + } + mouseMotionListeners.add(l); + } + } + + public void removeMouseMotionListener(MouseMotionListener l) { + if (mouseMotionListeners != null) { + mouseMotionListeners.remove(l); + } + } + + public MouseMotionListener[] getMouseMotionListeners() { + return mouseMotionListeners == null ? new MouseMotionListener[0] + : mouseMotionListeners.toArray(new MouseMotionListener[mouseMotionListeners.size()]); + } + + public void addMouseWheelListener(MouseWheelListener l) { + if (l != null) { + if (mouseWheelListeners == null) { + mouseWheelListeners = new ArrayList(); + } + mouseWheelListeners.add(l); + } + } + + public void removeMouseWheelListener(MouseWheelListener l) { + if (mouseWheelListeners != null) { + mouseWheelListeners.remove(l); + } + } + + public MouseWheelListener[] getMouseWheelListeners() { + return mouseWheelListeners == null ? new MouseWheelListener[0] + : mouseWheelListeners.toArray(new MouseWheelListener[mouseWheelListeners.size()]); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + if (listener != null) { + if (changeSupport == null) { + changeSupport = new PropertyChangeSupport(this); + } + changeSupport.addPropertyChangeListener(listener); + } + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + if (changeSupport != null) { + changeSupport.removePropertyChangeListener(listener); + } + } + + public PropertyChangeListener[] getPropertyChangeListeners() { + return changeSupport == null ? new PropertyChangeListener[0] : changeSupport.getPropertyChangeListeners(); + } + + public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { + if (listener != null) { + if (changeSupport == null) { + changeSupport = new PropertyChangeSupport(this); + } + changeSupport.addPropertyChangeListener(propertyName, listener); + } + } + + public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { + if (changeSupport != null) { + changeSupport.removePropertyChangeListener(propertyName, listener); + } + } + + public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) { + return changeSupport == null ? new PropertyChangeListener[0] + : changeSupport.getPropertyChangeListeners(propertyName); + } + + protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + if (changeSupport != null && (oldValue == null || newValue == null || !oldValue.equals(newValue))) { + changeSupport.firePropertyChange(propertyName, oldValue, newValue); + } + } + + protected void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { + if (changeSupport != null && oldValue != newValue) { + changeSupport.firePropertyChange(propertyName, oldValue, newValue); + } + } + + protected void firePropertyChange(String propertyName, int oldValue, int newValue) { + if (changeSupport != null && oldValue != newValue) { + changeSupport.firePropertyChange(propertyName, oldValue, newValue); + } + } + + // ------------------------------------------------------------ events + + protected final void enableEvents(long eventsToEnable) { + eventMask |= eventsToEnable; + } + + protected final void disableEvents(long eventsToDisable) { + eventMask &= ~eventsToDisable; + } + + private void fireComponentEvent(int id) { + if (notEmpty(componentListeners) || (eventMask & AWTEvent.COMPONENT_EVENT_MASK) != 0) { + dispatchEvent(new ComponentEvent(this, id)); + } + } + + /// Delivers the event to this component, at once. + public final void dispatchEvent(AWTEvent e) { + processEvent(e); + } + + protected void processEvent(AWTEvent e) { + if (e instanceof FocusEvent) { + processFocusEvent((FocusEvent) e); + } else if (e instanceof MouseWheelEvent) { + processMouseWheelEvent((MouseWheelEvent) e); + } else if (e instanceof MouseEvent) { + int id = e.getID(); + if (id == MouseEvent.MOUSE_MOVED || id == MouseEvent.MOUSE_DRAGGED) { + processMouseMotionEvent((MouseEvent) e); + } else { + processMouseEvent((MouseEvent) e); + } + } else if (e instanceof KeyEvent) { + processKeyEvent((KeyEvent) e); + } else if (e instanceof ComponentEvent) { + processComponentEvent((ComponentEvent) e); + } + } + + protected void processComponentEvent(ComponentEvent e) { + ComponentListener[] ls = getComponentListeners(); + for (int i = 0; i < ls.length; i++) { + switch (e.getID()) { + case ComponentEvent.COMPONENT_RESIZED: + ls[i].componentResized(e); + break; + case ComponentEvent.COMPONENT_MOVED: + ls[i].componentMoved(e); + break; + case ComponentEvent.COMPONENT_SHOWN: + ls[i].componentShown(e); + break; + case ComponentEvent.COMPONENT_HIDDEN: + ls[i].componentHidden(e); + break; + default: + break; + } + } + } + + protected void processFocusEvent(FocusEvent e) { + FocusListener[] ls = getFocusListeners(); + for (int i = 0; i < ls.length; i++) { + if (e.getID() == FocusEvent.FOCUS_GAINED) { + ls[i].focusGained(e); + } else { + ls[i].focusLost(e); + } + } + } + + protected void processKeyEvent(KeyEvent e) { + KeyListener[] ls = getKeyListeners(); + for (int i = 0; i < ls.length; i++) { + switch (e.getID()) { + case KeyEvent.KEY_TYPED: + ls[i].keyTyped(e); + break; + case KeyEvent.KEY_PRESSED: + ls[i].keyPressed(e); + break; + case KeyEvent.KEY_RELEASED: + ls[i].keyReleased(e); + break; + default: + break; + } + } + } + + protected void processMouseEvent(MouseEvent e) { + MouseListener[] ls = getMouseListeners(); + for (int i = 0; i < ls.length; i++) { + switch (e.getID()) { + case MouseEvent.MOUSE_PRESSED: + ls[i].mousePressed(e); + break; + case MouseEvent.MOUSE_RELEASED: + ls[i].mouseReleased(e); + break; + case MouseEvent.MOUSE_CLICKED: + ls[i].mouseClicked(e); + break; + case MouseEvent.MOUSE_ENTERED: + ls[i].mouseEntered(e); + break; + case MouseEvent.MOUSE_EXITED: + ls[i].mouseExited(e); + break; + default: + break; + } + } + } + + protected void processMouseMotionEvent(MouseEvent e) { + MouseMotionListener[] ls = getMouseMotionListeners(); + for (int i = 0; i < ls.length; i++) { + if (e.getID() == MouseEvent.MOUSE_MOVED) { + ls[i].mouseMoved(e); + } else { + ls[i].mouseDragged(e); + } + } + } + + protected void processMouseWheelEvent(MouseWheelEvent e) { + MouseWheelListener[] ls = getMouseWheelListeners(); + for (int i = 0; i < ls.length; i++) { + ls[i].mouseWheelMoved(e); + } + } + + protected String paramString() { + return (name == null ? "" : name) + "," + x + "," + y + "," + width + "x" + height + + (valid ? "" : ",invalid") + (visible ? "" : ",hidden") + (enabled ? "" : ",disabled"); + } + + @Override + public String toString() { + return getClass().getName() + "[" + paramString() + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Composite.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Composite.java new file mode 100644 index 00000000000..d3b3db38cf8 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Composite.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// How drawing is combined with what is already there. The only composite +/// the layer draws with is [AlphaComposite] in its `SRC_OVER` rule. +public interface Composite { +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Container.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Container.java new file mode 100644 index 00000000000..32f6541fc63 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Container.java @@ -0,0 +1,472 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.event.ContainerEvent; +import com.codename1.desktopcompat.java.awt.event.ContainerListener; +import com.codename1.desktopcompat.rt.ContainerPeer; +import com.codename1.desktopcompat.rt.G2D; +import com.codename1.desktopcompat.rt.Peer; +import java.util.ArrayList; + +/// A component that holds other components and lays them out with a +/// layout manager. +/// +/// The peers of the children are children of this container's peer, in +/// reverse order, so that the first child is painted on top as it is on the +/// desktop. +public class Container extends Component { + + private final ArrayList children = new ArrayList(); + private LayoutManager layoutMgr; + private ArrayList containerListeners; + + public Container() { + } + + @Override + protected com.codename1.ui.Component cn1CreatePeer() { + return new ContainerPeer(this); + } + + @Override + protected void cn1PeerCreated() { + super.cn1PeerCreated(); + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof ContainerPeer) { + ContainerPeer cp = (ContainerPeer) p; + for (int i = children.size() - 1; i >= 0; i--) { + cp.addComponent(children.get(i).cn1Peer()); + } + } + } + + /// Paints the children: through the peers when `g` is the graphics + /// this container's peer is being painted with, else one by one into + /// `g`, the last child first. + protected void cn1PaintChildren(Graphics g) { + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof Peer && g instanceof G2D && ((Peer) p).support().isPainting((G2D) g)) { + ((Peer) p).support().paintChildren((G2D) g); + return; + } + for (int i = children.size() - 1; i >= 0; i--) { + Component c = children.get(i); + if (c.isVisible()) { + Graphics cg = g.create(c.getX(), c.getY(), c.getWidth(), c.getHeight()); + try { + c.paint(cg); + } finally { + cg.dispose(); + } + } + } + } + + public int getComponentCount() { + return children.size(); + } + + public Component getComponent(int n) { + if (n < 0 || n >= children.size()) { + throw new ArrayIndexOutOfBoundsException("No such child: " + n); + } + return children.get(n); + } + + public Component[] getComponents() { + return children.toArray(new Component[children.size()]); + } + + public Insets getInsets() { + return new Insets(0, 0, 0, 0); + } + + public Component add(Component comp) { + addImpl(comp, null, -1); + return comp; + } + + public Component add(String name, Component comp) { + addImpl(comp, name, -1); + return comp; + } + + public Component add(Component comp, int index) { + addImpl(comp, null, index); + return comp; + } + + public void add(Component comp, Object constraints) { + addImpl(comp, constraints, -1); + } + + public void add(Component comp, Object constraints, int index) { + addImpl(comp, constraints, index); + } + + protected void addImpl(Component comp, Object constraints, int index) { + if (index > children.size() || index < -1) { + throw new IllegalArgumentException("illegal component position"); + } + if (comp == this) { + throw new IllegalArgumentException("adding container to itself"); + } + if (comp instanceof Container) { + for (Container c = this; c != null; c = c.getParent()) { + if (c == comp) { + throw new IllegalArgumentException("adding container's parent to itself"); + } + } + } + if (comp instanceof Window) { + throw new IllegalArgumentException("adding a window to a container"); + } + Container old = comp.getParent(); + if (old != null) { + old.remove(comp); + if (old == this && index > children.size()) { + index = children.size(); + } + } + int at = index == -1 ? children.size() : index; + children.add(at, comp); + comp.cn1SetParent(this); + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof ContainerPeer) { + ((ContainerPeer) p).addComponent(children.size() - 1 - at, comp.cn1Peer()); + } + if (layoutMgr != null) { + if (layoutMgr instanceof LayoutManager2) { + ((LayoutManager2) layoutMgr).addLayoutComponent(comp, constraints); + } else if (constraints instanceof String) { + layoutMgr.addLayoutComponent((String) constraints, comp); + } + } + invalidate(); + if (isDisplayable()) { + comp.addNotify(); + } + if (containerListeners != null && !containerListeners.isEmpty()) { + dispatchEvent(new ContainerEvent(this, ContainerEvent.COMPONENT_ADDED, comp)); + } + } + + public void remove(int index) { + Component comp = getComponent(index); + if (comp.isDisplayable()) { + comp.removeNotify(); + } + if (layoutMgr != null) { + layoutMgr.removeLayoutComponent(comp); + } + children.remove(index); + comp.cn1SetParent(null); + com.codename1.ui.Component p = cn1PeerOrNull(); + com.codename1.ui.Component cp = comp.cn1PeerOrNull(); + if (p instanceof ContainerPeer && cp != null) { + ((ContainerPeer) p).removeComponent(cp); + } + invalidate(); + if (containerListeners != null && !containerListeners.isEmpty()) { + dispatchEvent(new ContainerEvent(this, ContainerEvent.COMPONENT_REMOVED, comp)); + } + } + + public void remove(Component comp) { + int i = children.indexOf(comp); + if (i >= 0) { + remove(i); + } + } + + public void removeAll() { + while (!children.isEmpty()) { + remove(children.size() - 1); + } + } + + public int getComponentZOrder(Component comp) { + return children.indexOf(comp); + } + + public void setComponentZOrder(Component comp, int index) { + int i = children.indexOf(comp); + if (i < 0) { + addImpl(comp, null, index); + return; + } + if (i == index) { + return; + } + children.remove(i); + children.add(index, comp); + com.codename1.ui.Component p = cn1PeerOrNull(); + com.codename1.ui.Component cp = comp.cn1PeerOrNull(); + if (p instanceof ContainerPeer && cp != null) { + ((ContainerPeer) p).removeComponent(cp); + ((ContainerPeer) p).addComponent(children.size() - 1 - index, cp); + } + invalidate(); + } + + public LayoutManager getLayout() { + return layoutMgr; + } + + public void setLayout(LayoutManager mgr) { + layoutMgr = mgr; + invalidate(); + } + + @Override + public void doLayout() { + if (layoutMgr != null) { + layoutMgr.layoutContainer(this); + } + } + + public boolean isValidateRoot() { + return false; + } + + @Override + public void invalidate() { + if (layoutMgr instanceof LayoutManager2) { + ((LayoutManager2) layoutMgr).invalidateLayout(this); + } + super.invalidate(); + } + + @Override + public void validate() { + if (!cn1Valid()) { + validateTree(); + repaint(); + } + } + + protected void validateTree() { + if (!cn1Valid()) { + doLayout(); + for (int i = 0; i < children.size(); i++) { + Component c = children.get(i); + if (c instanceof Container) { + if (!c.cn1Valid()) { + ((Container) c).validateTree(); + } + } else { + c.validate(); + } + } + } + cn1SetValid(true); + } + + @Override + public void setFont(Font f) { + super.setFont(f); + invalidateTree(); + } + + private void invalidateTree() { + for (int i = 0; i < children.size(); i++) { + Component c = children.get(i); + if (c instanceof Container) { + ((Container) c).invalidateTree(); + } else { + c.cn1SetValid(false); + } + } + cn1SetValid(false); + } + + @Override + public Dimension getPreferredSize() { + if (isPreferredSizeSet() || layoutMgr == null) { + return super.getPreferredSize(); + } + return layoutMgr.preferredLayoutSize(this); + } + + @Override + public Dimension getMinimumSize() { + if (isMinimumSizeSet() || layoutMgr == null) { + return super.getMinimumSize(); + } + return layoutMgr.minimumLayoutSize(this); + } + + @Override + public Dimension getMaximumSize() { + if (!isMaximumSizeSet() && layoutMgr instanceof LayoutManager2) { + return ((LayoutManager2) layoutMgr).maximumLayoutSize(this); + } + return super.getMaximumSize(); + } + + @Override + public float getAlignmentX() { + if (layoutMgr instanceof LayoutManager2) { + return ((LayoutManager2) layoutMgr).getLayoutAlignmentX(this); + } + return super.getAlignmentX(); + } + + @Override + public float getAlignmentY() { + if (layoutMgr instanceof LayoutManager2) { + return ((LayoutManager2) layoutMgr).getLayoutAlignmentY(this); + } + return super.getAlignmentY(); + } + + @Override + public void paint(Graphics g) { + cn1PaintChildren(g); + } + + @Override + public void update(Graphics g) { + paint(g); + } + + public void paintComponents(Graphics g) { + cn1PaintChildren(g); + } + + @Override + public Component getComponentAt(int x, int y) { + if (!contains(x, y)) { + return null; + } + for (int i = 0; i < children.size(); i++) { + Component c = children.get(i); + if (c.contains(x - c.getX(), y - c.getY())) { + return c; + } + } + return this; + } + + public Component findComponentAt(int x, int y) { + if (!contains(x, y) || !isVisible()) { + return null; + } + for (int i = 0; i < children.size(); i++) { + Component c = children.get(i); + if (!c.isVisible()) { + continue; + } + int cx = x - c.getX(); + int cy = y - c.getY(); + if (c instanceof Container) { + Component found = ((Container) c).findComponentAt(cx, cy); + if (found != null) { + return found; + } + } else if (c.contains(cx, cy)) { + return c; + } + } + return this; + } + + public Component findComponentAt(Point p) { + return findComponentAt(p.x, p.y); + } + + public boolean isAncestorOf(Component c) { + for (Container p = c == null ? null : c.getParent(); p != null; p = p.getParent()) { + if (p == this) { + return true; + } + } + return false; + } + + @Override + public void addNotify() { + super.addNotify(); + for (int i = 0; i < children.size(); i++) { + children.get(i).addNotify(); + } + } + + @Override + public void removeNotify() { + for (int i = children.size() - 1; i >= 0 && i < children.size(); i--) { + children.get(i).removeNotify(); + } + super.removeNotify(); + } + + public void addContainerListener(ContainerListener l) { + if (l != null) { + if (containerListeners == null) { + containerListeners = new ArrayList(); + } + containerListeners.add(l); + } + } + + public void removeContainerListener(ContainerListener l) { + if (containerListeners != null) { + containerListeners.remove(l); + } + } + + public ContainerListener[] getContainerListeners() { + return containerListeners == null ? new ContainerListener[0] + : containerListeners.toArray(new ContainerListener[containerListeners.size()]); + } + + @Override + protected void processEvent(AWTEvent e) { + if (e instanceof ContainerEvent) { + processContainerEvent((ContainerEvent) e); + } else { + super.processEvent(e); + } + } + + protected void processContainerEvent(ContainerEvent e) { + ContainerListener[] ls = getContainerListeners(); + for (int i = 0; i < ls.length; i++) { + if (e.getID() == ContainerEvent.COMPONENT_ADDED) { + ls[i].componentAdded(e); + } else { + ls[i].componentRemoved(e); + } + } + } + + @Override + protected String paramString() { + String s = super.paramString(); + if (layoutMgr != null) { + s += ",layout=" + layoutMgr.getClass().getName(); + } + return s; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Cursor.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Cursor.java new file mode 100644 index 00000000000..ab2a4b36b40 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Cursor.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// A pointer shape. Cursors are recorded on components but the pointer's +/// shape is never changed. +public class Cursor { + + public static final int DEFAULT_CURSOR = 0; + public static final int CROSSHAIR_CURSOR = 1; + public static final int TEXT_CURSOR = 2; + public static final int WAIT_CURSOR = 3; + public static final int SW_RESIZE_CURSOR = 4; + public static final int SE_RESIZE_CURSOR = 5; + public static final int NW_RESIZE_CURSOR = 6; + public static final int NE_RESIZE_CURSOR = 7; + public static final int N_RESIZE_CURSOR = 8; + public static final int S_RESIZE_CURSOR = 9; + public static final int W_RESIZE_CURSOR = 10; + public static final int E_RESIZE_CURSOR = 11; + public static final int HAND_CURSOR = 12; + public static final int MOVE_CURSOR = 13; + public static final int CUSTOM_CURSOR = -1; + + private static final String[] NAMES = {"Default Cursor", "Crosshair Cursor", "Text Cursor", "Wait Cursor", + "Southwest Resize Cursor", "Southeast Resize Cursor", "Northwest Resize Cursor", "Northeast Resize Cursor", + "North Resize Cursor", "South Resize Cursor", "West Resize Cursor", "East Resize Cursor", "Hand Cursor", + "Move Cursor"}; + + private static final Cursor[] PREDEFINED = new Cursor[14]; + + protected String name; + private final int type; + + public Cursor(int type) { + if (type < DEFAULT_CURSOR || type > MOVE_CURSOR) { + throw new IllegalArgumentException("illegal cursor type"); + } + this.type = type; + this.name = NAMES[type]; + } + + protected Cursor(String name) { + this.type = CUSTOM_CURSOR; + this.name = name; + } + + public static Cursor getPredefinedCursor(int type) { + if (type < DEFAULT_CURSOR || type > MOVE_CURSOR) { + throw new IllegalArgumentException("illegal cursor type"); + } + Cursor c = PREDEFINED[type]; + if (c == null) { + c = new Cursor(type); + PREDEFINED[type] = c; + } + return c; + } + + public static Cursor getDefaultCursor() { + return getPredefinedCursor(DEFAULT_CURSOR); + } + + public int getType() { + return type; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return getClass().getName() + "[" + getName() + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Dialog.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Dialog.java new file mode 100644 index 00000000000..300b1efd806 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Dialog.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// A titled window owned by another. It is shown as a form like any +/// window, and a modal dialog does not block: `setVisible(true)` returns at +/// once. +public class Dialog extends Window { + + private String title; + private boolean modal; + private boolean resizable = true; + private boolean undecorated; + + public Dialog(Frame owner) { + this(owner, "", false); + } + + public Dialog(Frame owner, boolean modal) { + this(owner, "", modal); + } + + public Dialog(Frame owner, String title) { + this(owner, title, false); + } + + public Dialog(Frame owner, String title, boolean modal) { + super(owner); + this.title = title == null ? "" : title; + this.modal = modal; + } + + public Dialog(Dialog owner) { + this(owner, "", false); + } + + public Dialog(Dialog owner, String title) { + this(owner, title, false); + } + + public Dialog(Dialog owner, String title, boolean modal) { + super((Window) owner); + this.title = title == null ? "" : title; + this.modal = modal; + } + + @Override + public String cn1Title() { + return title; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + String old = this.title; + this.title = title == null ? "" : title; + if (cn1Form() != null) { + cn1Form().setTitle(this.title); + } + firePropertyChange("title", old, this.title); + } + + public boolean isModal() { + return modal; + } + + /// Recorded only; see the class description. + public void setModal(boolean modal) { + this.modal = modal; + } + + public boolean isResizable() { + return resizable; + } + + public void setResizable(boolean resizable) { + this.resizable = resizable; + } + + public boolean isUndecorated() { + return undecorated; + } + + public void setUndecorated(boolean undecorated) { + this.undecorated = undecorated; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Dimension.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Dimension.java new file mode 100644 index 00000000000..c489e3efe4e --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Dimension.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.geom.Dimension2D; + +/// A width and a height in integer units. +public class Dimension extends Dimension2D { + + public int width; + + public int height; + + public Dimension() { + } + + public Dimension(Dimension d) { + this.width = d.width; + this.height = d.height; + } + + public Dimension(int width, int height) { + this.width = width; + this.height = height; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + /// Rounds each value up to an integer. + @Override + public void setSize(double width, double height) { + this.width = (int) Math.ceil(width); + this.height = (int) Math.ceil(height); + } + + public Dimension getSize() { + return new Dimension(width, height); + } + + public void setSize(Dimension d) { + this.width = d.width; + this.height = d.height; + } + + public void setSize(int width, int height) { + this.width = width; + this.height = height; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Dimension) { + Dimension d = (Dimension) obj; + return width == d.width && height == d.height; + } + return false; + } + + @Override + public int hashCode() { + return width * 31 + height; + } + + @Override + public Object clone() { + return new Dimension(width, height); + } + + @Override + public String toString() { + return "java.awt.Dimension[width=" + width + ",height=" + height + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/EventQueue.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/EventQueue.java new file mode 100644 index 00000000000..90fe81cc966 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/EventQueue.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.ui.Display; + +/// The event dispatch thread, which is Codename One's own. +public class EventQueue { + + public EventQueue() { + } + + public static boolean isDispatchThread() { + return !Display.isInitialized() || Display.getInstance().isEdt(); + } + + /// Runs `runnable` on the event dispatch thread after the events + /// already queued. With no display running it runs at once. + public static void invokeLater(Runnable runnable) { + if (Display.isInitialized()) { + Display.getInstance().callSerially(runnable); + } else { + runnable.run(); + } + } + + /// Runs `runnable` on the event dispatch thread and returns when it + /// has. Called on that thread it simply runs it, where the desktop + /// throws an error; an exception the runnable throws propagates as it + /// is. + public static void invokeAndWait(Runnable runnable) throws InterruptedException { + if (!Display.isInitialized() || Display.getInstance().isEdt()) { + runnable.run(); + } else { + Display.getInstance().callSeriallyAndWait(runnable); + } + } + + public static long getMostRecentEventTime() { + return System.currentTimeMillis(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/FlowLayout.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/FlowLayout.java new file mode 100644 index 00000000000..80dd32a323a --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/FlowLayout.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// Arranges components in rows at their preferred sizes, starting a new row +/// when the next component does not fit. +/// +/// There is no component orientation in this layer: every container is laid +/// out left to right, so `LEADING` is the left edge and `TRAILING` the right +/// one. Baseline alignment is recorded but rows are always centred +/// vertically. +public class FlowLayout implements LayoutManager { + + public static final int LEFT = 0; + + public static final int CENTER = 1; + + public static final int RIGHT = 2; + + public static final int LEADING = 3; + + public static final int TRAILING = 4; + + private int align; + private int hgap; + private int vgap; + private boolean alignOnBaseline; + + public FlowLayout() { + this(CENTER, 5, 5); + } + + public FlowLayout(int align) { + this(align, 5, 5); + } + + public FlowLayout(int align, int hgap, int vgap) { + this.align = align; + this.hgap = hgap; + this.vgap = vgap; + } + + public int getAlignment() { + return align; + } + + public void setAlignment(int align) { + this.align = align; + } + + public int getHgap() { + return hgap; + } + + public void setHgap(int hgap) { + this.hgap = hgap; + } + + public int getVgap() { + return vgap; + } + + public void setVgap(int vgap) { + this.vgap = vgap; + } + + /// Recorded only: rows are centred vertically whatever this is set to. + public void setAlignOnBaseline(boolean alignOnBaseline) { + this.alignOnBaseline = alignOnBaseline; + } + + public boolean getAlignOnBaseline() { + return alignOnBaseline; + } + + @Override + public void addLayoutComponent(String name, Component comp) { + } + + @Override + public void removeLayoutComponent(Component comp) { + } + + @Override + public Dimension preferredLayoutSize(Container target) { + return measure(target, true); + } + + @Override + public Dimension minimumLayoutSize(Container target) { + return measure(target, false); + } + + private Dimension measure(Container target, boolean preferred) { + int w = 0; + int h = 0; + boolean first = true; + int n = target.getComponentCount(); + for (int i = 0; i < n; i++) { + Component c = target.getComponent(i); + if (!c.isVisible()) { + continue; + } + Dimension d = preferred ? c.getPreferredSize() : c.getMinimumSize(); + if (d.height > h) { + h = d.height; + } + if (first) { + first = false; + } else { + w += hgap; + } + w += d.width; + } + Insets in = target.getInsets(); + return new Dimension(w + in.left + in.right + hgap * 2, h + in.top + in.bottom + vgap * 2); + } + + @Override + public void layoutContainer(Container target) { + Insets in = target.getInsets(); + int avail = target.getWidth() - (in.left + in.right + hgap * 2); + int n = target.getComponentCount(); + int used = 0; + int y = in.top + vgap; + int rowHeight = 0; + int rowStart = 0; + for (int i = 0; i < n; i++) { + Component c = target.getComponent(i); + if (!c.isVisible()) { + continue; + } + Dimension d = c.getPreferredSize(); + c.setSize(d.width, d.height); + if (used == 0 || used + d.width <= avail) { + if (used > 0) { + used += hgap; + } + used += d.width; + if (d.height > rowHeight) { + rowHeight = d.height; + } + } else { + placeRow(target, in.left + hgap, y, avail - used, rowHeight, rowStart, i); + y += vgap + rowHeight; + used = d.width; + rowHeight = d.height; + rowStart = i; + } + } + placeRow(target, in.left + hgap, y, avail - used, rowHeight, rowStart, n); + } + + private void placeRow(Container target, int x, int y, int spare, int rowHeight, int from, int to) { + switch (align) { + case CENTER: + x += spare / 2; + break; + case RIGHT: + case TRAILING: + x += spare; + break; + default: + break; + } + for (int i = from; i < to; i++) { + Component c = target.getComponent(i); + if (c.isVisible()) { + c.setLocation(x, y + (rowHeight - c.getHeight()) / 2); + x += c.getWidth() + hgap; + } + } + } + + @Override + public String toString() { + String a; + switch (align) { + case LEFT: + a = ",align=left"; + break; + case CENTER: + a = ",align=center"; + break; + case RIGHT: + a = ",align=right"; + break; + case LEADING: + a = ",align=leading"; + break; + case TRAILING: + a = ",align=trailing"; + break; + default: + a = ""; + break; + } + return getClass().getName() + "[hgap=" + hgap + ",vgap=" + vgap + a + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Font.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Font.java new file mode 100644 index 00000000000..18ab12b506e --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Font.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// A typeface, a style and a size in logical pixels. +/// +/// The family is a request, not a file: every font is drawn with the +/// platform's own typeface in the asked weight and slant, so a 12 pixel +/// bold "Arial" is the device's 12 pixel bold font. Sizes follow the +/// desktop convention of one point per pixel. +public class Font { + + public static final String DIALOG = "Dialog"; + public static final String DIALOG_INPUT = "DialogInput"; + public static final String SANS_SERIF = "SansSerif"; + public static final String SERIF = "Serif"; + public static final String MONOSPACED = "Monospaced"; + public static final int PLAIN = 0; + public static final int BOLD = 1; + public static final int ITALIC = 2; + public static final int ROMAN_BASELINE = 0; + public static final int CENTER_BASELINE = 1; + public static final int HANGING_BASELINE = 2; + public static final int TRUETYPE_FONT = 0; + public static final int TYPE1_FONT = 1; + + protected String name; + protected int style; + protected int size; + protected float pointSize; + + public Font(String name, int style, int size) { + this.name = name != null ? name : "Default"; + this.style = (style & ~0x03) == 0 ? style : 0; + this.size = size; + this.pointSize = size; + } + + protected Font(Font font) { + this.name = font.name; + this.style = font.style; + this.size = font.size; + this.pointSize = font.pointSize; + } + + private Font(String name, int style, float sizePts) { + this.name = name; + this.style = (style & ~0x03) == 0 ? style : 0; + this.size = (int) (sizePts + 0.5); + this.pointSize = sizePts; + } + + public String getFamily() { + return name; + } + + public String getName() { + return name; + } + + public String getFontName() { + return name; + } + + public int getStyle() { + return style; + } + + public int getSize() { + return size; + } + + public float getSize2D() { + return pointSize; + } + + public boolean isPlain() { + return style == 0; + } + + public boolean isBold() { + return (style & BOLD) != 0; + } + + public boolean isItalic() { + return (style & ITALIC) != 0; + } + + public Font deriveFont(int style, float size) { + return new Font(name, style, size); + } + + public Font deriveFont(float size) { + return new Font(name, style, size); + } + + public Font deriveFont(int style) { + return new Font(name, style, pointSize); + } + + /// Whether the platform font can draw the character; the layer cannot + /// ask, so it answers true. + public boolean canDisplay(char c) { + return true; + } + + /// Parses `family-style-size`, `family style size` and their shorter + /// forms; a missing style is plain and a missing size is 12. + public static Font decode(String str) { + if (str == null) { + return new Font(DIALOG, PLAIN, 12); + } + String s = str.trim().replace('-', ' '); + String fontName = s; + int fontStyle = PLAIN; + int fontSize = 12; + int last = s.lastIndexOf(' '); + if (last > 0) { + String tail = s.substring(last + 1); + try { + fontSize = Integer.parseInt(tail); + s = s.substring(0, last).trim(); + fontName = s; + last = s.lastIndexOf(' '); + } catch (NumberFormatException e) { + fontSize = 12; + } + } + if (last > 0) { + String tail = s.substring(last + 1); + int parsed = -1; + if (tail.equalsIgnoreCase("bold")) { + parsed = BOLD; + } else if (tail.equalsIgnoreCase("italic")) { + parsed = ITALIC; + } else if (tail.equalsIgnoreCase("bolditalic")) { + parsed = BOLD | ITALIC; + } else if (tail.equalsIgnoreCase("plain")) { + parsed = PLAIN; + } + if (parsed >= 0) { + fontStyle = parsed; + fontName = s.substring(0, last).trim(); + } + } + return new Font(fontName.length() == 0 ? DIALOG : fontName, fontStyle, fontSize); + } + + @Override + public int hashCode() { + return name.hashCode() ^ style ^ Float.floatToIntBits(pointSize); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Font)) { + return false; + } + Font font = (Font) obj; + return size == font.size && style == font.style + && Float.floatToIntBits(pointSize) == Float.floatToIntBits(font.pointSize) && name.equals(font.name); + } + + @Override + public String toString() { + String strStyle = isBold() ? (isItalic() ? "bolditalic" : "bold") : (isItalic() ? "italic" : "plain"); + return "java.awt.Font[family=" + getFamily() + ",name=" + name + ",style=" + strStyle + ",size=" + size + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/FontMetrics.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/FontMetrics.java new file mode 100644 index 00000000000..4de5a194e56 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/FontMetrics.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.geom.Rectangle2D; + +/// The measurements of a font, in logical pixels. +/// +/// This base class estimates from the font size alone; the metrics a +/// component or a graphics hands out measure the platform font. +public abstract class FontMetrics { + + protected Font font; + + protected FontMetrics(Font font) { + this.font = font; + } + + public Font getFont() { + return font; + } + + public int getLeading() { + return 0; + } + + public int getAscent() { + return font.getSize(); + } + + public int getDescent() { + return 0; + } + + public int getHeight() { + return getLeading() + getAscent() + getDescent(); + } + + public int getMaxAscent() { + return getAscent(); + } + + public int getMaxDescent() { + return getDescent(); + } + + public int getMaxAdvance() { + return -1; + } + + public int charWidth(int codePoint) { + return charWidth((char) codePoint); + } + + public int charWidth(char ch) { + return stringWidth(String.valueOf(ch)); + } + + public int stringWidth(String str) { + return Math.round(str.length() * font.getSize2D() * 0.6f); + } + + public int charsWidth(char[] data, int off, int len) { + return stringWidth(new String(data, off, len)); + } + + public int[] getWidths() { + int[] widths = new int[256]; + for (char ch = 0; ch < 256; ch++) { + widths[ch] = charWidth(ch); + } + return widths; + } + + public boolean hasUniformLineMetrics() { + return true; + } + + public Rectangle2D getStringBounds(String str, Graphics context) { + return new Rectangle2D.Float(0, -getAscent(), stringWidth(str), getHeight()); + } + + public Rectangle2D getStringBounds(String str, int beginIndex, int limit, Graphics context) { + return getStringBounds(str.substring(beginIndex, limit), context); + } + + @Override + public String toString() { + return "FontMetrics[font=" + getFont() + "ascent=" + getAscent() + ", descent=" + getDescent() + + ", height=" + getHeight() + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Frame.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Frame.java new file mode 100644 index 00000000000..e68c6628d2a --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Frame.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// A window with a title, which the form shows in its title area. +public class Frame extends Window { + + public static final int NORMAL = 0; + public static final int ICONIFIED = 1; + public static final int MAXIMIZED_HORIZ = 2; + public static final int MAXIMIZED_VERT = 4; + public static final int MAXIMIZED_BOTH = 6; + + private String title; + private boolean resizable = true; + private boolean undecorated; + private int state; + + public Frame() { + this(""); + } + + public Frame(String title) { + super((Window) null); + this.title = title == null ? "" : title; + } + + @Override + public String cn1Title() { + return title; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + String old = this.title; + this.title = title == null ? "" : title; + if (cn1Form() != null) { + cn1Form().setTitle(this.title); + } + firePropertyChange("title", old, this.title); + } + + public boolean isResizable() { + return resizable; + } + + /// Recorded only. + public void setResizable(boolean resizable) { + this.resizable = resizable; + } + + public boolean isUndecorated() { + return undecorated; + } + + /// Recorded only. + public void setUndecorated(boolean undecorated) { + this.undecorated = undecorated; + } + + public int getExtendedState() { + return state; + } + + /// Recorded only: a frame always fills its form. + public void setExtendedState(int state) { + this.state = state; + } + + public int getState() { + return (state & ICONIFIED) != 0 ? ICONIFIED : NORMAL; + } + + public void setState(int state) { + this.state = state == ICONIFIED ? this.state | ICONIFIED : this.state & ~ICONIFIED; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GradientPaint.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GradientPaint.java new file mode 100644 index 00000000000..68526aa34c5 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GradientPaint.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.geom.Point2D; + +/// A linear gradient between two colors anchored at two points in user +/// space; beyond the points it holds the end colors, or repeats back and +/// forth when cyclic. +public class GradientPaint implements Paint { + + private final float x1; + private final float y1; + private final float x2; + private final float y2; + private final Color color1; + private final Color color2; + private final boolean cyclic; + + public GradientPaint(float x1, float y1, Color color1, float x2, float y2, Color color2) { + this(x1, y1, color1, x2, y2, color2, false); + } + + public GradientPaint(Point2D pt1, Color color1, Point2D pt2, Color color2) { + this((float) pt1.getX(), (float) pt1.getY(), color1, (float) pt2.getX(), (float) pt2.getY(), color2, false); + } + + public GradientPaint(float x1, float y1, Color color1, float x2, float y2, Color color2, boolean cyclic) { + if (color1 == null || color2 == null) { + throw new NullPointerException("Colors cannot be null"); + } + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.color1 = color1; + this.color2 = color2; + this.cyclic = cyclic; + } + + public GradientPaint(Point2D pt1, Color color1, Point2D pt2, Color color2, boolean cyclic) { + this((float) pt1.getX(), (float) pt1.getY(), color1, (float) pt2.getX(), (float) pt2.getY(), color2, cyclic); + } + + public Point2D getPoint1() { + return new Point2D.Float(x1, y1); + } + + public Color getColor1() { + return color1; + } + + public Point2D getPoint2() { + return new Point2D.Float(x2, y2); + } + + public Color getColor2() { + return color2; + } + + public boolean isCyclic() { + return cyclic; + } + + @Override + public int getTransparency() { + return (color1.getAlpha() & color2.getAlpha()) == 0xff ? OPAQUE : TRANSLUCENT; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Graphics.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Graphics.java new file mode 100644 index 00000000000..0b4fa50280c --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Graphics.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.image.ImageObserver; + +/// The drawing surface of a component or an image, in logical pixels. +/// +/// Every graphics the layer hands out is a [Graphics2D]; see +/// `com.codename1.desktopcompat.rt.G2D` for how it maps onto Codename One. +public abstract class Graphics { + + protected Graphics() { + } + + public abstract Graphics create(); + + public Graphics create(int x, int y, int width, int height) { + Graphics g = create(); + if (g == null) { + return null; + } + g.translate(x, y); + g.clipRect(0, 0, width, height); + return g; + } + + public abstract void translate(int x, int y); + + public abstract Color getColor(); + + public abstract void setColor(Color c); + + public abstract void setPaintMode(); + + public abstract void setXORMode(Color c1); + + public abstract Font getFont(); + + public abstract void setFont(Font font); + + public FontMetrics getFontMetrics() { + return getFontMetrics(getFont()); + } + + public abstract FontMetrics getFontMetrics(Font f); + + public abstract Rectangle getClipBounds(); + + public abstract void clipRect(int x, int y, int width, int height); + + public abstract void setClip(int x, int y, int width, int height); + + public abstract Shape getClip(); + + public abstract void setClip(Shape clip); + + public abstract void copyArea(int x, int y, int width, int height, int dx, int dy); + + public abstract void drawLine(int x1, int y1, int x2, int y2); + + public abstract void fillRect(int x, int y, int width, int height); + + public void drawRect(int x, int y, int width, int height) { + if (width < 0 || height < 0) { + return; + } + if (height == 0 || width == 0) { + drawLine(x, y, x + width, y + height); + } else { + drawLine(x, y, x + width - 1, y); + drawLine(x + width, y, x + width, y + height - 1); + drawLine(x + width, y + height, x + 1, y + height); + drawLine(x, y + height, x, y + 1); + } + } + + public abstract void clearRect(int x, int y, int width, int height); + + public abstract void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight); + + public abstract void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight); + + public void draw3DRect(int x, int y, int width, int height, boolean raised) { + Color c = getColor(); + Color brighter = c.brighter(); + Color darker = c.darker(); + setColor(raised ? brighter : darker); + drawLine(x, y, x, y + height); + drawLine(x + 1, y, x + width - 1, y); + setColor(raised ? darker : brighter); + drawLine(x + 1, y + height, x + width, y + height); + drawLine(x + width, y, x + width, y + height - 1); + setColor(c); + } + + public void fill3DRect(int x, int y, int width, int height, boolean raised) { + Color c = getColor(); + Color brighter = c.brighter(); + Color darker = c.darker(); + if (!raised) { + setColor(darker); + } + fillRect(x + 1, y + 1, width - 2, height - 2); + setColor(raised ? brighter : darker); + drawLine(x, y, x, y + height - 1); + drawLine(x + 1, y, x + width - 2, y); + setColor(raised ? darker : brighter); + drawLine(x + 1, y + height - 1, x + width - 1, y + height - 1); + drawLine(x + width - 1, y, x + width - 1, y + height - 2); + setColor(c); + } + + public abstract void drawOval(int x, int y, int width, int height); + + public abstract void fillOval(int x, int y, int width, int height); + + public abstract void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle); + + public abstract void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle); + + public abstract void drawPolyline(int[] xPoints, int[] yPoints, int nPoints); + + public abstract void drawPolygon(int[] xPoints, int[] yPoints, int nPoints); + + public void drawPolygon(Polygon p) { + drawPolygon(p.xpoints, p.ypoints, p.npoints); + } + + public abstract void fillPolygon(int[] xPoints, int[] yPoints, int nPoints); + + public void fillPolygon(Polygon p) { + fillPolygon(p.xpoints, p.ypoints, p.npoints); + } + + public abstract void drawString(String str, int x, int y); + + public void drawChars(char[] data, int offset, int length, int x, int y) { + drawString(new String(data, offset, length), x, y); + } + + public abstract boolean drawImage(Image img, int x, int y, ImageObserver observer); + + public abstract boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer); + + public abstract boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer); + + public abstract boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, + ImageObserver observer); + + public abstract boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, + int sy2, ImageObserver observer); + + public abstract boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, + int sy2, Color bgcolor, ImageObserver observer); + + public abstract void dispose(); + + @Override + public String toString() { + return getClass().getName() + "[font=" + getFont() + ",color=" + getColor() + "]"; + } + + public boolean hitClip(int x, int y, int width, int height) { + Rectangle clipRect = getClipBounds(); + if (clipRect == null) { + return true; + } + return clipRect.intersects(x, y, width, height); + } + + public Rectangle getClipBounds(Rectangle r) { + Rectangle clipRect = getClipBounds(); + if (clipRect != null) { + r.x = clipRect.x; + r.y = clipRect.y; + r.width = clipRect.width; + r.height = clipRect.height; + } else if (r == null) { + throw new NullPointerException("null rectangle parameter"); + } + return r; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Graphics2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Graphics2D.java new file mode 100644 index 00000000000..e5fb1d642fd --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Graphics2D.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.geom.AffineTransform; +import com.codename1.desktopcompat.java.awt.image.ImageObserver; + +import java.util.Map; + +/// A graphics with shapes, strokes, paints and an affine transform. +public abstract class Graphics2D extends Graphics { + + protected Graphics2D() { + } + + @Override + public void draw3DRect(int x, int y, int width, int height, boolean raised) { + Paint p = getPaint(); + Color c = getColor(); + Color brighter = c.brighter(); + Color darker = c.darker(); + setColor(raised ? brighter : darker); + fillRect(x, y, 1, height + 1); + fillRect(x + 1, y, width - 1, 1); + setColor(raised ? darker : brighter); + fillRect(x + 1, y + height, width, 1); + fillRect(x + width, y, 1, height); + setPaint(p); + } + + @Override + public void fill3DRect(int x, int y, int width, int height, boolean raised) { + Paint p = getPaint(); + Color c = getColor(); + Color brighter = c.brighter(); + Color darker = c.darker(); + if (!raised) { + setColor(darker); + } else if (p != c) { + setColor(c); + } + fillRect(x + 1, y + 1, width - 2, height - 2); + setColor(raised ? brighter : darker); + fillRect(x, y, 1, height); + fillRect(x + 1, y, width - 2, 1); + setColor(raised ? darker : brighter); + fillRect(x + 1, y + height - 1, width - 1, 1); + fillRect(x + width - 1, y, 1, height - 1); + setPaint(p); + } + + public abstract void draw(Shape s); + + public abstract boolean drawImage(Image img, AffineTransform xform, ImageObserver obs); + + @Override + public abstract void drawString(String str, int x, int y); + + public abstract void drawString(String str, float x, float y); + + public abstract void fill(Shape s); + + public abstract void setComposite(Composite comp); + + public abstract void setPaint(Paint paint); + + public abstract void setStroke(Stroke s); + + public abstract void setRenderingHint(RenderingHints.Key hintKey, Object hintValue); + + public abstract Object getRenderingHint(RenderingHints.Key hintKey); + + public abstract void setRenderingHints(Map hints); + + public abstract void addRenderingHints(Map hints); + + public abstract RenderingHints getRenderingHints(); + + @Override + public abstract void translate(int x, int y); + + public abstract void translate(double tx, double ty); + + public abstract void rotate(double theta); + + public abstract void rotate(double theta, double x, double y); + + public abstract void scale(double sx, double sy); + + public abstract void shear(double shx, double shy); + + public abstract void transform(AffineTransform Tx); + + public abstract void setTransform(AffineTransform Tx); + + public abstract AffineTransform getTransform(); + + public abstract Paint getPaint(); + + public abstract Composite getComposite(); + + public abstract void setBackground(Color color); + + public abstract Color getBackground(); + + public abstract Stroke getStroke(); + + public abstract void clip(Shape s); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridBagConstraints.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridBagConstraints.java new file mode 100644 index 00000000000..30e80829efd --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridBagConstraints.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// The placement of one component in a [GridBagLayout]: its cell, how many +/// cells it spans, how it shares spare space and how it sits inside its +/// display area. +public class GridBagConstraints implements Cloneable { + + public static final int RELATIVE = -1; + + public static final int REMAINDER = 0; + + public static final int NONE = 0; + + public static final int BOTH = 1; + + public static final int HORIZONTAL = 2; + + public static final int VERTICAL = 3; + + public static final int CENTER = 10; + + public static final int NORTH = 11; + + public static final int NORTHEAST = 12; + + public static final int EAST = 13; + + public static final int SOUTHEAST = 14; + + public static final int SOUTH = 15; + + public static final int SOUTHWEST = 16; + + public static final int WEST = 17; + + public static final int NORTHWEST = 18; + + public static final int PAGE_START = 19; + + public static final int PAGE_END = 20; + + public static final int LINE_START = 21; + + public static final int LINE_END = 22; + + public static final int FIRST_LINE_START = 23; + + public static final int FIRST_LINE_END = 24; + + public static final int LAST_LINE_START = 25; + + public static final int LAST_LINE_END = 26; + + public static final int BASELINE = 0x100; + + public static final int BASELINE_LEADING = 0x200; + + public static final int BASELINE_TRAILING = 0x300; + + public static final int ABOVE_BASELINE = 0x400; + + public static final int ABOVE_BASELINE_LEADING = 0x500; + + public static final int ABOVE_BASELINE_TRAILING = 0x600; + + public static final int BELOW_BASELINE = 0x700; + + public static final int BELOW_BASELINE_LEADING = 0x800; + + public static final int BELOW_BASELINE_TRAILING = 0x900; + + public int gridx; + + public int gridy; + + public int gridwidth; + + public int gridheight; + + public double weightx; + + public double weighty; + + public int anchor; + + public int fill; + + public Insets insets; + + public int ipadx; + + public int ipady; + + /// The resolved cell and span, written by the layout on each pass. + int tempX; + int tempY; + int tempWidth; + int tempHeight; + /// The component's own size as last measured by the layout. + int minWidth; + int minHeight; + + public GridBagConstraints() { + gridx = RELATIVE; + gridy = RELATIVE; + gridwidth = 1; + gridheight = 1; + anchor = CENTER; + fill = NONE; + insets = new Insets(0, 0, 0, 0); + } + + public GridBagConstraints(int gridx, int gridy, int gridwidth, int gridheight, double weightx, double weighty, + int anchor, int fill, Insets insets, int ipadx, int ipady) { + this.gridx = gridx; + this.gridy = gridy; + this.gridwidth = gridwidth; + this.gridheight = gridheight; + this.weightx = weightx; + this.weighty = weighty; + this.anchor = anchor; + this.fill = fill; + this.insets = insets; + this.ipadx = ipadx; + this.ipady = ipady; + } + + /// A copy of these constraints with its own `insets` object. Built field + /// by field, since `Object.clone()` is not available on a device. + @Override + public Object clone() { + Insets in = insets == null ? null : new Insets(insets.top, insets.left, insets.bottom, insets.right); + GridBagConstraints c = new GridBagConstraints(gridx, gridy, gridwidth, gridheight, weightx, weighty, + anchor, fill, in, ipadx, ipady); + c.tempX = tempX; + c.tempY = tempY; + c.tempWidth = tempWidth; + c.tempHeight = tempHeight; + c.minWidth = minWidth; + c.minHeight = minHeight; + return c; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridBagLayout.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridBagLayout.java new file mode 100644 index 00000000000..507c8953252 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridBagLayout.java @@ -0,0 +1,686 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import java.util.Hashtable; + +/// Lays components out in a grid of rows and columns of differing sizes, +/// each component described by a [GridBagConstraints]. +/// +/// Every container is laid out left to right, so the relative anchors +/// resolve as they do in that orientation (`LINE_START` is the west edge). +/// The baseline anchors are treated as their plain counterparts: `BASELINE` +/// as `CENTER`, `BASELINE_LEADING` as `LINE_START`, `ABOVE_BASELINE` as +/// `PAGE_START`, `BELOW_BASELINE` as `PAGE_END` and so on. +public class GridBagLayout implements LayoutManager2 { + + protected static final int MAXGRIDSIZE = 512; + + protected static final int MINSIZE = 1; + + protected static final int PREFERREDSIZE = 2; + + protected Hashtable comptable; + + protected GridBagConstraints defaultConstraints; + + public int[] columnWidths; + + public int[] rowHeights; + + public double[] columnWeights; + + public double[] rowWeights; + + /// The grid computed by one measuring pass. + private static final class Grid { + int width; + int height; + int startx; + int starty; + int[] minWidth; + int[] minHeight; + double[] weightX; + double[] weightY; + } + + private Grid lastGrid; + + public GridBagLayout() { + comptable = new Hashtable(); + defaultConstraints = new GridBagConstraints(); + } + + public void setConstraints(Component comp, GridBagConstraints constraints) { + Object copy = constraints.clone(); + if (copy instanceof GridBagConstraints) { + comptable.put(comp, (GridBagConstraints) copy); + } + } + + public GridBagConstraints getConstraints(Component comp) { + Object copy = lookupConstraints(comp).clone(); + return copy instanceof GridBagConstraints ? (GridBagConstraints) copy : null; + } + + protected GridBagConstraints lookupConstraints(Component comp) { + GridBagConstraints c = comptable.get(comp); + if (c == null) { + setConstraints(comp, defaultConstraints); + c = comptable.get(comp); + } + return c; + } + + public Point getLayoutOrigin() { + Point origin = new Point(0, 0); + if (lastGrid != null) { + origin.x = lastGrid.startx; + origin.y = lastGrid.starty; + } + return origin; + } + + public int[][] getLayoutDimensions() { + if (lastGrid == null) { + return new int[2][0]; + } + int[][] dim = new int[2][]; + dim[0] = new int[lastGrid.width]; + dim[1] = new int[lastGrid.height]; + System.arraycopy(lastGrid.minWidth, 0, dim[0], 0, lastGrid.width); + System.arraycopy(lastGrid.minHeight, 0, dim[1], 0, lastGrid.height); + return dim; + } + + public double[][] getLayoutWeights() { + if (lastGrid == null) { + return new double[2][0]; + } + double[][] weights = new double[2][]; + weights[0] = new double[lastGrid.width]; + weights[1] = new double[lastGrid.height]; + System.arraycopy(lastGrid.weightX, 0, weights[0], 0, lastGrid.width); + System.arraycopy(lastGrid.weightY, 0, weights[1], 0, lastGrid.height); + return weights; + } + + @Override + public void addLayoutComponent(String name, Component comp) { + } + + @Override + public void addLayoutComponent(Component comp, Object constraints) { + if (constraints instanceof GridBagConstraints) { + setConstraints(comp, (GridBagConstraints) constraints); + } else if (constraints != null) { + throw new IllegalArgumentException("cannot add to layout: constraints must be a GridBagConstraint"); + } + } + + @Override + public void removeLayoutComponent(Component comp) { + comptable.remove(comp); + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + return total(parent, measure(parent, PREFERREDSIZE)); + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return total(parent, measure(parent, MINSIZE)); + } + + @Override + public Dimension maximumLayoutSize(Container target) { + return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + @Override + public float getLayoutAlignmentX(Container parent) { + return 0.5f; + } + + @Override + public float getLayoutAlignmentY(Container parent) { + return 0.5f; + } + + @Override + public void invalidateLayout(Container target) { + } + + @Override + public void layoutContainer(Container parent) { + arrange(parent); + } + + @Override + public String toString() { + return getClass().getName(); + } + + private static int[] room(int[] a, int size) { + if (a.length >= size) { + return a; + } + int[] grown = new int[Math.max(size, a.length * 2)]; + System.arraycopy(a, 0, grown, 0, a.length); + return grown; + } + + private static int highest(int[] a, int from, int to) { + int m = 0; + for (int i = from; i < to; i++) { + if (a[i] > m) { + m = a[i]; + } + } + return m; + } + + private static void mark(int[] a, int from, int to, int value) { + for (int i = from; i < to; i++) { + a[i] = value; + } + } + + /// Works out the grid for the visible children of `parent`: which cells + /// each one occupies, and the size and weight of every column and row + /// before any spare space is shared out. + private Grid measure(Container parent, int sizeflag) { + Component[] comps = parent.getComponents(); + Grid grid = new Grid(); + int gridW = 0; + int gridH = 0; + int[] rowEnd = new int[16]; + int[] colEnd = new int[16]; + int curRow = -1; + int curCol = -1; + + // First pass: the extent of the grid, every open-ended span counted + // as a single cell. + for (int n = 0; n < comps.length; n++) { + Component comp = comps[n]; + if (!comp.isVisible()) { + continue; + } + GridBagConstraints gc = lookupConstraints(comp); + int cx = gc.gridx; + int cy = gc.gridy; + int cw = gc.gridwidth <= 0 ? 1 : gc.gridwidth; + int ch = gc.gridheight <= 0 ? 1 : gc.gridheight; + if (cx < 0 && cy < 0) { + if (curRow >= 0) { + cy = curRow; + } else if (curCol >= 0) { + cx = curCol; + } else { + cy = 0; + } + } + if (cx < 0) { + rowEnd = room(rowEnd, cy + ch); + cx = highest(rowEnd, cy, cy + ch) - cx - 1; + if (cx < 0) { + cx = 0; + } + } else if (cy < 0) { + colEnd = room(colEnd, cx + cw); + cy = highest(colEnd, cx, cx + cw) - cy - 1; + if (cy < 0) { + cy = 0; + } + } + int px = cx + cw; + int py = cy + ch; + if (gridW < px) { + gridW = px; + } + if (gridH < py) { + gridH = py; + } + colEnd = room(colEnd, px); + rowEnd = room(rowEnd, py); + mark(colEnd, cx, px, py); + mark(rowEnd, cy, py, px); + + Dimension d = sizeflag == PREFERREDSIZE ? comp.getPreferredSize() : comp.getMinimumSize(); + gc.minWidth = d.width; + gc.minHeight = d.height; + + if (gc.gridheight == 0 && gc.gridwidth == 0) { + curRow = -1; + curCol = -1; + } + if (gc.gridheight == 0 && curRow < 0) { + curCol = cx + cw; + } else if (gc.gridwidth == 0 && curCol < 0) { + curRow = cy + ch; + } + } + if (columnWidths != null && gridW < columnWidths.length) { + gridW = columnWidths.length; + } + if (rowHeights != null && gridH < rowHeights.length) { + gridH = rowHeights.length; + } + grid.width = gridW; + grid.height = gridH; + grid.minWidth = new int[gridW]; + grid.minHeight = new int[gridH]; + grid.weightX = new double[gridW]; + grid.weightY = new double[gridH]; + if (columnWidths != null) { + System.arraycopy(columnWidths, 0, grid.minWidth, 0, columnWidths.length); + } + if (rowHeights != null) { + System.arraycopy(rowHeights, 0, grid.minHeight, 0, rowHeights.length); + } + if (columnWeights != null) { + System.arraycopy(columnWeights, 0, grid.weightX, 0, Math.min(gridW, columnWeights.length)); + } + if (rowWeights != null) { + System.arraycopy(rowWeights, 0, grid.weightY, 0, Math.min(gridH, rowWeights.length)); + } + + // Second pass: the same walk with the grid's extent known, so the + // open-ended spans reach the last column or row. + int longest = 1; + curRow = -1; + curCol = -1; + rowEnd = new int[Math.max(gridH, 1)]; + colEnd = new int[Math.max(gridW, 1)]; + for (int n = 0; n < comps.length; n++) { + Component comp = comps[n]; + if (!comp.isVisible()) { + continue; + } + GridBagConstraints gc = lookupConstraints(comp); + int cx = gc.gridx; + int cy = gc.gridy; + int cw = gc.gridwidth; + int ch = gc.gridheight; + if (cx < 0 && cy < 0) { + if (curRow >= 0) { + cy = curRow; + } else if (curCol >= 0) { + cx = curCol; + } else { + cy = 0; + } + } + if (cx < 0) { + if (ch <= 0) { + ch += gridH - cy; + if (ch < 1) { + ch = 1; + } + } + rowEnd = room(rowEnd, cy + ch); + cx = highest(rowEnd, cy, cy + ch) - cx - 1; + if (cx < 0) { + cx = 0; + } + } else if (cy < 0) { + if (cw <= 0) { + cw += gridW - cx; + if (cw < 1) { + cw = 1; + } + } + colEnd = room(colEnd, cx + cw); + cy = highest(colEnd, cx, cx + cw) - cy - 1; + if (cy < 0) { + cy = 0; + } + } + if (cw <= 0) { + cw += gridW - cx; + if (cw < 1) { + cw = 1; + } + } + if (ch <= 0) { + ch += gridH - cy; + if (ch < 1) { + ch = 1; + } + } + int px = cx + cw; + int py = cy + ch; + colEnd = room(colEnd, px); + rowEnd = room(rowEnd, py); + mark(colEnd, cx, px, py); + mark(rowEnd, cy, py, px); + if (gc.gridheight == 0 && gc.gridwidth == 0) { + curRow = -1; + curCol = -1; + } + if (gc.gridheight == 0 && curRow < 0) { + curCol = cx + cw; + } else if (gc.gridwidth == 0 && curCol < 0) { + curRow = cy + ch; + } + gc.tempX = cx; + gc.tempY = cy; + gc.tempWidth = cw; + gc.tempHeight = ch; + // A relative position resolved in this pass can land past the + // extent the first pass counted. Such a cell gets a size and a + // place, but as in the JDK it is not part of the grid: it adds + // nothing to the layout's size and takes no spare space. + if (px > grid.minWidth.length || py > grid.minHeight.length) { + grow(grid, Math.max(px, grid.minWidth.length), Math.max(py, grid.minHeight.length)); + } + if (cw > longest) { + longest = cw; + } + if (ch > longest) { + longest = ch; + } + } + + // Third pass: sizes and weights, components spanning fewer cells + // first so a wide one only adds what its cells still lack. + for (int span = 1; span <= longest; span++) { + for (int n = 0; n < comps.length; n++) { + Component comp = comps[n]; + if (!comp.isVisible()) { + continue; + } + GridBagConstraints gc = lookupConstraints(comp); + Insets in = gc.insets; + if (gc.tempWidth == span) { + share(grid.minWidth, grid.weightX, gc.tempX, gc.tempX + span, gc.weightx, + gc.minWidth + gc.ipadx + in.left + in.right); + } + if (gc.tempHeight == span) { + share(grid.minHeight, grid.weightY, gc.tempY, gc.tempY + span, gc.weighty, + gc.minHeight + gc.ipady + in.top + in.bottom); + } + } + } + return grid; + } + + /// Makes room in the arrays for cells beyond the grid's extent, which + /// itself stays as it is. + private static void grow(Grid grid, int w, int h) { + int[] mw = new int[w]; + int[] mh = new int[h]; + double[] wx = new double[w]; + double[] wy = new double[h]; + System.arraycopy(grid.minWidth, 0, mw, 0, grid.minWidth.length); + System.arraycopy(grid.minHeight, 0, mh, 0, grid.minHeight.length); + System.arraycopy(grid.weightX, 0, wx, 0, grid.weightX.length); + System.arraycopy(grid.weightY, 0, wy, 0, grid.weightY.length); + grid.minWidth = mw; + grid.minHeight = mh; + grid.weightX = wx; + grid.weightY = wy; + } + + /// Raises the weights and sizes of the cells `from` to `to` until + /// together they carry `wantWeight` and `wantPixels`. What is missing + /// goes to the cells in proportion to their weight, the last cell + /// taking the rounding and, when none has weight, all of it. + private static void share(int[] size, double[] weights, int from, int to, double wantWeight, int wantPixels) { + double missing = wantWeight; + for (int k = from; k < to; k++) { + missing -= weights[k]; + } + if (missing > 0.0) { + double weight = 0.0; + for (int k = from; k < to; k++) { + weight += weights[k]; + } + for (int k = from; weight > 0.0 && k < to; k++) { + double wt = weights[k]; + double dx = (wt * missing) / weight; + weights[k] += dx; + missing -= dx; + weight -= wt; + } + weights[to - 1] += missing; + } + int pixels = wantPixels; + for (int k = from; k < to; k++) { + pixels -= size[k]; + } + if (pixels > 0) { + double weight = 0.0; + for (int k = from; k < to; k++) { + weight += weights[k]; + } + for (int k = from; weight > 0.0 && k < to; k++) { + double wt = weights[k]; + int dx = (int) ((wt * (double) pixels) / weight); + size[k] += dx; + pixels -= dx; + weight -= wt; + } + size[to - 1] += pixels; + } + } + + private static Dimension total(Container parent, Grid grid) { + Insets in = parent.getInsets(); + int w = 0; + for (int i = 0; i < grid.width; i++) { + w += grid.minWidth[i]; + } + int h = 0; + for (int i = 0; i < grid.height; i++) { + h += grid.minHeight[i]; + } + return new Dimension(w + in.left + in.right, h + in.top + in.bottom); + } + + /// Shares `spare` pixels over the cells by weight and answers the width + /// the cells then add up to, given they added up to `used` before. A + /// cell squeezed below nothing is held at zero. + private static int stretch(int[] size, double[] weights, int count, int spare, int used) { + double weight = 0.0; + for (int i = 0; i < count; i++) { + weight += weights[i]; + } + if (weight > 0.0) { + for (int i = 0; i < count; i++) { + int dx = (int) (((double) spare * weights[i]) / weight); + size[i] += dx; + used += dx; + if (size[i] < 0) { + used -= size[i]; + size[i] = 0; + } + } + } + return used; + } + + private void arrange(Container parent) { + Component[] comps = parent.getComponents(); + if (comps.length == 0 && (columnWidths == null || columnWidths.length == 0) + && (rowHeights == null || rowHeights.length == 0)) { + return; + } + Insets pin = parent.getInsets(); + int pw = parent.getWidth(); + int ph = parent.getHeight(); + Grid grid = measure(parent, PREFERREDSIZE); + Dimension d = total(parent, grid); + if (pw < d.width || ph < d.height) { + grid = measure(parent, MINSIZE); + d = total(parent, grid); + } + lastGrid = grid; + + int spareW = pw - d.width; + if (spareW != 0) { + spareW = pw - stretch(grid.minWidth, grid.weightX, grid.width, spareW, d.width); + } + int spareH = ph - d.height; + if (spareH != 0) { + spareH = ph - stretch(grid.minHeight, grid.weightY, grid.height, spareH, d.height); + } + grid.startx = spareW / 2 + pin.left; + grid.starty = spareH / 2 + pin.top; + + Rectangle r = new Rectangle(); + for (int n = 0; n < comps.length; n++) { + Component comp = comps[n]; + if (!comp.isVisible()) { + continue; + } + GridBagConstraints gc = lookupConstraints(comp); + r.x = grid.startx; + for (int i = 0; i < gc.tempX; i++) { + r.x += grid.minWidth[i]; + } + r.y = grid.starty; + for (int i = 0; i < gc.tempY; i++) { + r.y += grid.minHeight[i]; + } + r.width = 0; + for (int i = gc.tempX; i < gc.tempX + gc.tempWidth; i++) { + r.width += grid.minWidth[i]; + } + r.height = 0; + for (int i = gc.tempY; i < gc.tempY + gc.tempHeight; i++) { + r.height += grid.minHeight[i]; + } + settle(gc, r); + if (r.x < 0) { + r.width += r.x; + r.x = 0; + } + if (r.y < 0) { + r.height += r.y; + r.y = 0; + } + if (r.width <= 0 || r.height <= 0) { + comp.setBounds(0, 0, 0, 0); + } else if (comp.getX() != r.x || comp.getY() != r.y || comp.getWidth() != r.width + || comp.getHeight() != r.height) { + comp.setBounds(r.x, r.y, r.width, r.height); + } + } + } + + /// Shrinks a display area to the component's bounds: insets off, then + /// the component's own size unless it fills, then the anchor. + private static void settle(GridBagConstraints gc, Rectangle r) { + Insets in = gc.insets; + r.x += in.left; + r.width -= in.left + in.right; + r.y += in.top; + r.height -= in.top + in.bottom; + + int freeX = 0; + int ownW = gc.minWidth + gc.ipadx; + if (gc.fill != GridBagConstraints.HORIZONTAL && gc.fill != GridBagConstraints.BOTH && r.width > ownW) { + freeX = r.width - ownW; + r.width = ownW; + } + int freeY = 0; + int ownH = gc.minHeight + gc.ipady; + if (gc.fill != GridBagConstraints.VERTICAL && gc.fill != GridBagConstraints.BOTH && r.height > ownH) { + freeY = r.height - ownH; + r.height = ownH; + } + + // 0 = start, 1 = middle, 2 = end, on each axis. + int h; + int v; + switch (gc.anchor) { + case GridBagConstraints.NORTH: + case GridBagConstraints.PAGE_START: + case GridBagConstraints.ABOVE_BASELINE: + h = 1; + v = 0; + break; + case GridBagConstraints.NORTHEAST: + case GridBagConstraints.FIRST_LINE_END: + case GridBagConstraints.ABOVE_BASELINE_TRAILING: + h = 2; + v = 0; + break; + case GridBagConstraints.EAST: + case GridBagConstraints.LINE_END: + case GridBagConstraints.BASELINE_TRAILING: + h = 2; + v = 1; + break; + case GridBagConstraints.SOUTHEAST: + case GridBagConstraints.LAST_LINE_END: + case GridBagConstraints.BELOW_BASELINE_TRAILING: + h = 2; + v = 2; + break; + case GridBagConstraints.SOUTH: + case GridBagConstraints.PAGE_END: + case GridBagConstraints.BELOW_BASELINE: + h = 1; + v = 2; + break; + case GridBagConstraints.SOUTHWEST: + case GridBagConstraints.LAST_LINE_START: + case GridBagConstraints.BELOW_BASELINE_LEADING: + h = 0; + v = 2; + break; + case GridBagConstraints.WEST: + case GridBagConstraints.LINE_START: + case GridBagConstraints.BASELINE_LEADING: + h = 0; + v = 1; + break; + case GridBagConstraints.NORTHWEST: + case GridBagConstraints.FIRST_LINE_START: + case GridBagConstraints.ABOVE_BASELINE_LEADING: + h = 0; + v = 0; + break; + case GridBagConstraints.CENTER: + case GridBagConstraints.BASELINE: + h = 1; + v = 1; + break; + default: + throw new IllegalArgumentException("illegal anchor value"); + } + if (h == 1) { + r.x += freeX / 2; + } else if (h == 2) { + r.x += freeX; + } + if (v == 1) { + r.y += freeY / 2; + } else if (v == 2) { + r.y += freeY; + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridLayout.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridLayout.java new file mode 100644 index 00000000000..1bdd188f9f0 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/GridLayout.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// Divides a container into a grid of equally sized cells, one component to +/// a cell, filled row by row from the top left. +public class GridLayout implements LayoutManager { + + private int hgap; + private int vgap; + private int rows; + private int cols; + + public GridLayout() { + this(1, 0, 0, 0); + } + + public GridLayout(int rows, int cols) { + this(rows, cols, 0, 0); + } + + public GridLayout(int rows, int cols, int hgap, int vgap) { + if (rows == 0 && cols == 0) { + throw new IllegalArgumentException("rows and cols cannot both be zero"); + } + this.rows = rows; + this.cols = cols; + this.hgap = hgap; + this.vgap = vgap; + } + + public int getRows() { + return rows; + } + + public void setRows(int rows) { + if (rows == 0 && this.cols == 0) { + throw new IllegalArgumentException("rows and cols cannot both be zero"); + } + this.rows = rows; + } + + public int getColumns() { + return cols; + } + + public void setColumns(int cols) { + if (cols == 0 && this.rows == 0) { + throw new IllegalArgumentException("rows and cols cannot both be zero"); + } + this.cols = cols; + } + + public int getHgap() { + return hgap; + } + + public void setHgap(int hgap) { + this.hgap = hgap; + } + + public int getVgap() { + return vgap; + } + + public void setVgap(int vgap) { + this.vgap = vgap; + } + + @Override + public void addLayoutComponent(String name, Component comp) { + } + + @Override + public void removeLayoutComponent(Component comp) { + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + return measure(parent, true); + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return measure(parent, false); + } + + private Dimension measure(Container parent, boolean preferred) { + Insets in = parent.getInsets(); + int n = parent.getComponentCount(); + int nrows = rows; + int ncols = cols; + if (nrows > 0) { + ncols = (n + nrows - 1) / nrows; + } else { + nrows = (n + ncols - 1) / ncols; + } + int w = 0; + int h = 0; + for (int i = 0; i < n; i++) { + Component c = parent.getComponent(i); + Dimension d = preferred ? c.getPreferredSize() : c.getMinimumSize(); + if (w < d.width) { + w = d.width; + } + if (h < d.height) { + h = d.height; + } + } + return new Dimension(in.left + in.right + ncols * w + (ncols - 1) * hgap, + in.top + in.bottom + nrows * h + (nrows - 1) * vgap); + } + + @Override + public void layoutContainer(Container parent) { + Insets in = parent.getInsets(); + int n = parent.getComponentCount(); + if (n == 0) { + return; + } + int nrows = rows; + int ncols = cols; + if (nrows > 0) { + ncols = (n + nrows - 1) / nrows; + } else { + nrows = (n + ncols - 1) / ncols; + } + int gapsW = (ncols - 1) * hgap; + int innerW = parent.getWidth() - (in.left + in.right); + int cellW = (innerW - gapsW) / ncols; + int spareW = (innerW - (cellW * ncols + gapsW)) / 2; + int gapsH = (nrows - 1) * vgap; + int innerH = parent.getHeight() - (in.top + in.bottom); + int cellH = (innerH - gapsH) / nrows; + int spareH = (innerH - (cellH * nrows + gapsH)) / 2; + for (int i = 0; i < n; i++) { + int r = i / ncols; + int c = i % ncols; + if (r >= nrows) { + break; + } + parent.getComponent(i).setBounds(in.left + spareW + c * (cellW + hgap), + in.top + spareH + r * (cellH + vgap), cellW, cellH); + } + } + + @Override + public String toString() { + return getClass().getName() + "[hgap=" + hgap + ",vgap=" + vgap + ",rows=" + rows + ",cols=" + cols + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Image.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Image.java new file mode 100644 index 00000000000..d7568c8e968 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Image.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.image.ImageObserver; +import com.codename1.desktopcompat.rt.NativeImage; + +/// A picture that can be drawn. One image pixel is one logical pixel. +/// +/// Every image of the layer is backed by a Codename One image, which +/// [#cn1Image()] answers; that is what `Graphics.drawImage` draws. +public abstract class Image { + + public static final Object UndefinedProperty = new Object(); + public static final int SCALE_DEFAULT = 1; + public static final int SCALE_FAST = 2; + public static final int SCALE_SMOOTH = 4; + public static final int SCALE_REPLICATE = 8; + public static final int SCALE_AREA_AVERAGING = 16; + + public abstract int getWidth(ImageObserver observer); + + public abstract int getHeight(ImageObserver observer); + + public abstract Graphics getGraphics(); + + /// The Codename One image holding this image's pixels as they are now, + /// or null for an image that has none. + public com.codename1.ui.Image cn1Image() { + return null; + } + + public Image getScaledInstance(int width, int height, int hints) { + com.codename1.ui.Image src = cn1Image(); + if (src == null) { + return this; + } + int w = width; + int h = height; + if (w < 0 && h < 0) { + w = src.getWidth(); + h = src.getHeight(); + } else if (w < 0) { + w = Math.max(1, src.getWidth() * h / Math.max(1, src.getHeight())); + } else if (h < 0) { + h = Math.max(1, src.getHeight() * w / Math.max(1, src.getWidth())); + } + return new NativeImage(src.scaled(w, h)); + } + + public void flush() { + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Insets.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Insets.java new file mode 100644 index 00000000000..a047d8e4da1 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Insets.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// The space a container leaves free along each of its four edges. +public class Insets implements Cloneable { + + public int top; + + public int left; + + public int bottom; + + public int right; + + public Insets(int top, int left, int bottom, int right) { + this.top = top; + this.left = left; + this.bottom = bottom; + this.right = right; + } + + public void set(int top, int left, int bottom, int right) { + this.top = top; + this.left = left; + this.bottom = bottom; + this.right = right; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Insets) { + Insets i = (Insets) obj; + return top == i.top && left == i.left && bottom == i.bottom && right == i.right; + } + return false; + } + + @Override + public int hashCode() { + return ((top * 31 + left) * 31 + bottom) * 31 + right; + } + + @Override + public String toString() { + return "java.awt.Insets[top=" + top + ",left=" + left + ",bottom=" + bottom + ",right=" + right + "]"; + } + + @Override + public Object clone() { + return new Insets(top, left, bottom, right); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/ItemSelectable.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/ItemSelectable.java new file mode 100644 index 00000000000..9b99628403e --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/ItemSelectable.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.event.ItemListener; + +/// A component or model that has a set of selected items and reports changes +/// of that selection to item listeners. +public interface ItemSelectable { + + Object[] getSelectedObjects(); + + void addItemListener(ItemListener l); + + void removeItemListener(ItemListener l); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/LayoutManager.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/LayoutManager.java new file mode 100644 index 00000000000..0b543c37799 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/LayoutManager.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// The contract of a class that positions the children of a [Container]. +public interface LayoutManager { + + void addLayoutComponent(String name, Component comp); + + void removeLayoutComponent(Component comp); + + Dimension preferredLayoutSize(Container parent); + + Dimension minimumLayoutSize(Container parent); + + void layoutContainer(Container parent); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/LayoutManager2.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/LayoutManager2.java new file mode 100644 index 00000000000..a7a19c829f8 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/LayoutManager2.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// A [LayoutManager] that takes a constraints object for each child and can +/// answer a maximum size and an alignment for the container it lays out. +public interface LayoutManager2 extends LayoutManager { + + void addLayoutComponent(Component comp, Object constraints); + + Dimension maximumLayoutSize(Container target); + + float getLayoutAlignmentX(Container target); + + float getLayoutAlignmentY(Container target); + + void invalidateLayout(Container target); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Paint.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Paint.java new file mode 100644 index 00000000000..669216ea7b3 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Paint.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// What a `Graphics2D` fills and strokes with. +/// +/// The two paints the layer can draw are [Color] and [GradientPaint]; a +/// `Graphics2D` refuses any other with an `UnsupportedOperationException` +/// rather than draw the wrong thing. The JDK's `createContext` is not part +/// of this interface: nothing here rasterizes through paint contexts. +public interface Paint extends Transparency { +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Panel.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Panel.java new file mode 100644 index 00000000000..2d4d2ba7fa4 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Panel.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// The plain AWT container; a flow layout unless told otherwise. +public class Panel extends Container { + + public Panel() { + this(new FlowLayout()); + } + + public Panel(LayoutManager layout) { + setLayout(layout); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Point.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Point.java new file mode 100644 index 00000000000..edbce235998 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Point.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.geom.Point2D; + +/// A location in integer coordinates. +public class Point extends Point2D { + + public int x; + + public int y; + + public Point() { + } + + public Point(Point p) { + this.x = p.x; + this.y = p.y; + } + + public Point(int x, int y) { + this.x = x; + this.y = y; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + public Point getLocation() { + return new Point(x, y); + } + + public void setLocation(Point p) { + this.x = p.x; + this.y = p.y; + } + + public void setLocation(int x, int y) { + this.x = x; + this.y = y; + } + + /// Moves to the nearest integer location. + @Override + public void setLocation(double x, double y) { + this.x = (int) Math.floor(x + 0.5); + this.y = (int) Math.floor(y + 0.5); + } + + public void move(int x, int y) { + this.x = x; + this.y = y; + } + + public void translate(int dx, int dy) { + this.x += dx; + this.y += dy; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Point) { + Point p = (Point) obj; + return x == p.x && y == p.y; + } + return super.equals(obj); + } + + @Override + public Object clone() { + return new Point(x, y); + } + + @Override + public String toString() { + return "java.awt.Point[x=" + x + ",y=" + y + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Polygon.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Polygon.java new file mode 100644 index 00000000000..bcf00133d3a --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Polygon.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.geom.AffineTransform; +import com.codename1.desktopcompat.java.awt.geom.Path2D; +import com.codename1.desktopcompat.java.awt.geom.PathIterator; +import com.codename1.desktopcompat.java.awt.geom.Point2D; +import com.codename1.desktopcompat.java.awt.geom.Rectangle2D; + +/// A closed region bounded by straight edges between integer points, +/// filled by the even-odd rule. +public class Polygon implements Shape { + + public int npoints; + public int[] xpoints; + public int[] ypoints; + protected Rectangle bounds; + + public Polygon() { + xpoints = new int[4]; + ypoints = new int[4]; + } + + public Polygon(int[] xpoints, int[] ypoints, int npoints) { + if (npoints > xpoints.length || npoints > ypoints.length) { + throw new IndexOutOfBoundsException("npoints > xpoints.length || npoints > ypoints.length"); + } + if (npoints < 0) { + throw new NegativeArraySizeException("npoints < 0"); + } + this.npoints = npoints; + this.xpoints = new int[npoints]; + this.ypoints = new int[npoints]; + System.arraycopy(xpoints, 0, this.xpoints, 0, npoints); + System.arraycopy(ypoints, 0, this.ypoints, 0, npoints); + } + + public void reset() { + npoints = 0; + bounds = null; + } + + public void invalidate() { + bounds = null; + } + + public void translate(int deltaX, int deltaY) { + for (int i = 0; i < npoints; i++) { + xpoints[i] += deltaX; + ypoints[i] += deltaY; + } + if (bounds != null) { + bounds.translate(deltaX, deltaY); + } + } + + public void addPoint(int x, int y) { + if (npoints >= xpoints.length || npoints >= ypoints.length) { + int size = Math.max(4, npoints * 2); + int[] nx = new int[size]; + int[] ny = new int[size]; + System.arraycopy(xpoints, 0, nx, 0, npoints); + System.arraycopy(ypoints, 0, ny, 0, npoints); + xpoints = nx; + ypoints = ny; + } + xpoints[npoints] = x; + ypoints[npoints] = y; + npoints++; + bounds = null; + } + + @Override + public Rectangle getBounds() { + if (npoints == 0) { + return new Rectangle(); + } + if (bounds == null) { + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + for (int i = 0; i < npoints; i++) { + minX = Math.min(minX, xpoints[i]); + maxX = Math.max(maxX, xpoints[i]); + minY = Math.min(minY, ypoints[i]); + maxY = Math.max(maxY, ypoints[i]); + } + bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY); + } + return new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height); + } + + public boolean contains(Point p) { + return contains(p.x, p.y); + } + + public boolean contains(int x, int y) { + return contains((double) x, (double) y); + } + + @Override + public Rectangle2D getBounds2D() { + return getBounds(); + } + + private Path2D.Double path() { + Path2D.Double p = new Path2D.Double(Path2D.WIND_EVEN_ODD); + for (int i = 0; i < npoints; i++) { + if (i == 0) { + p.moveTo(xpoints[i], ypoints[i]); + } else { + p.lineTo(xpoints[i], ypoints[i]); + } + } + if (npoints > 0) { + p.closePath(); + } + return p; + } + + @Override + public boolean contains(double x, double y) { + return npoints > 2 && path().contains(x, y); + } + + @Override + public boolean contains(Point2D p) { + return contains(p.getX(), p.getY()); + } + + @Override + public boolean intersects(double x, double y, double w, double h) { + return npoints > 0 && path().intersects(x, y, w, h); + } + + @Override + public boolean intersects(Rectangle2D r) { + return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public boolean contains(double x, double y, double w, double h) { + return npoints > 2 && path().contains(x, y, w, h); + } + + @Override + public boolean contains(Rectangle2D r) { + return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public PathIterator getPathIterator(AffineTransform at) { + return path().getPathIterator(at); + } + + @Override + public PathIterator getPathIterator(AffineTransform at, double flatness) { + return path().getPathIterator(at); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Rectangle.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Rectangle.java new file mode 100644 index 00000000000..e627ace20c5 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Rectangle.java @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.geom.Rectangle2D; + +/// A rectangle in integer coordinates. +/// +/// A rectangle with a zero width or height is empty and contains nothing; one +/// with a negative width or height does not exist at all, and is ignored by +/// `union` and `add`. Sums that would not fit an `int` are worked out in +/// `long` and clamped. +public class Rectangle extends Rectangle2D implements Shape { + + public int x; + + public int y; + + public int width; + + public int height; + + public Rectangle() { + } + + public Rectangle(Rectangle r) { + this.x = r.x; + this.y = r.y; + this.width = r.width; + this.height = r.height; + } + + public Rectangle(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public Rectangle(int width, int height) { + this.width = width; + this.height = height; + } + + public Rectangle(Point p, Dimension d) { + this.x = p.x; + this.y = p.y; + this.width = d.width; + this.height = d.height; + } + + public Rectangle(Point p) { + this.x = p.x; + this.y = p.y; + } + + public Rectangle(Dimension d) { + this.width = d.width; + this.height = d.height; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public Rectangle getBounds() { + return new Rectangle(x, y, width, height); + } + + @Override + public Rectangle2D getBounds2D() { + return new Rectangle(x, y, width, height); + } + + public void setBounds(Rectangle r) { + setBounds(r.x, r.y, r.width, r.height); + } + + public void setBounds(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + /// Sets the smallest integer rectangle that encloses the given one. + @Override + public void setRect(double x, double y, double width, double height) { + int newX = (int) Math.floor(x); + int newY = (int) Math.floor(y); + // a cast clamps to the int range, so far-off values stay far off + this.width = width >= 0.0 ? (int) Math.ceil(width + (x - newX)) : (int) Math.floor(width); + this.height = height >= 0.0 ? (int) Math.ceil(height + (y - newY)) : (int) Math.floor(height); + this.x = newX; + this.y = newY; + } + + public Point getLocation() { + return new Point(x, y); + } + + public void setLocation(Point p) { + setLocation(p.x, p.y); + } + + public void setLocation(int x, int y) { + this.x = x; + this.y = y; + } + + public void translate(int dx, int dy) { + this.x = clamp((long) x + dx); + this.y = clamp((long) y + dy); + } + + public Dimension getSize() { + return new Dimension(width, height); + } + + public void setSize(Dimension d) { + setSize(d.width, d.height); + } + + public void setSize(int width, int height) { + this.width = width; + this.height = height; + } + + public boolean contains(Point p) { + return contains(p.x, p.y); + } + + public boolean contains(int px, int py) { + if (width < 0 || height < 0 || px < x || py < y) { + return false; + } + return px < (long) x + width && py < (long) y + height; + } + + public boolean contains(Rectangle r) { + return contains(r.x, r.y, r.width, r.height); + } + + /// Whether the given rectangle lies wholly inside this one. An empty + /// rectangle on either side gives false. + public boolean contains(int rx, int ry, int rw, int rh) { + if (width <= 0 || height <= 0 || rw <= 0 || rh <= 0 || rx < x || ry < y) { + return false; + } + return (long) rx + rw <= (long) x + width && (long) ry + rh <= (long) y + height; + } + + public boolean intersects(Rectangle r) { + if (width <= 0 || height <= 0 || r.width <= 0 || r.height <= 0) { + return false; + } + return (long) r.x + r.width > x && (long) r.y + r.height > y + && (long) x + width > r.x && (long) y + height > r.y; + } + + /// The overlap of the two rectangles. When they do not overlap the + /// result has a negative or zero width or height. + public Rectangle intersection(Rectangle r) { + int x1 = Math.max(x, r.x); + int y1 = Math.max(y, r.y); + long x2 = Math.min((long) x + width, (long) r.x + r.width); + long y2 = Math.min((long) y + height, (long) r.y + r.height); + return new Rectangle(x1, y1, clamp(x2 - x1), clamp(y2 - y1)); + } + + /// The smallest rectangle enclosing both. When this rectangle has a + /// negative size the other is returned, and the other way round. + public Rectangle union(Rectangle r) { + if (width < 0 || height < 0) { + return new Rectangle(r); + } + Rectangle result = new Rectangle(this); + result.add(r); + return result; + } + + /// Grows the rectangle to reach a point. A rectangle with a negative + /// size becomes the empty rectangle at that point. + public void add(int newx, int newy) { + if (width < 0 || height < 0) { + this.x = newx; + this.y = newy; + this.width = 0; + this.height = 0; + return; + } + long x2 = Math.max((long) x + width, (long) newx); + long y2 = Math.max((long) y + height, (long) newy); + this.x = Math.min(x, newx); + this.y = Math.min(y, newy); + this.width = clamp(x2 - this.x); + this.height = clamp(y2 - this.y); + } + + public void add(Point pt) { + add(pt.x, pt.y); + } + + /// Grows the rectangle to enclose another, ignoring one that has a + /// negative size. A rectangle that itself has a negative size first + /// moves to the other one; as in the JDK its own width and height still + /// take part from there, so a positive one of the two can survive. + public void add(Rectangle r) { + long w = width; + long h = height; + if (width < 0 || height < 0) { + setBounds(r.x, r.y, r.width, r.height); + } + if (r.width < 0 || r.height < 0) { + return; + } + long x2 = Math.max(x + w, (long) r.x + r.width); + long y2 = Math.max(y + h, (long) r.y + r.height); + this.x = Math.min(x, r.x); + this.y = Math.min(y, r.y); + this.width = clamp(x2 - this.x); + this.height = clamp(y2 - this.y); + } + + /// Moves each side outwards by the given amounts; negative amounts + /// shrink the rectangle and can leave it with a negative size. + public void grow(int h, int v) { + long x1 = (long) x - h; + long y1 = (long) y - v; + long x2 = (long) x + width + h; + long y2 = (long) y + height + v; + this.x = clamp(x1); + this.y = clamp(y1); + this.width = clamp(x2 - this.x); + this.height = clamp(y2 - this.y); + } + + private static int clamp(long v) { + if (v > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + if (v < Integer.MIN_VALUE) { + return Integer.MIN_VALUE; + } + return (int) v; + } + + @Override + public boolean isEmpty() { + return width <= 0 || height <= 0; + } + + @Override + public int outcode(double x, double y) { + int out = 0; + if (width <= 0) { + out |= OUT_LEFT | OUT_RIGHT; + } else if (x < this.x) { + out |= OUT_LEFT; + } else if (x > this.x + (double) width) { + out |= OUT_RIGHT; + } + if (height <= 0) { + out |= OUT_TOP | OUT_BOTTOM; + } else if (y < this.y) { + out |= OUT_TOP; + } else if (y > this.y + (double) height) { + out |= OUT_BOTTOM; + } + return out; + } + + @Override + public Rectangle2D createIntersection(Rectangle2D r) { + if (r instanceof Rectangle) { + return intersection((Rectangle) r); + } + Rectangle2D dest = new Rectangle2D.Double(); + Rectangle2D.intersect(this, r, dest); + return dest; + } + + @Override + public Rectangle2D createUnion(Rectangle2D r) { + if (r instanceof Rectangle) { + return union((Rectangle) r); + } + Rectangle2D dest = new Rectangle2D.Double(); + Rectangle2D.union(this, r, dest); + return dest; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Rectangle) { + Rectangle r = (Rectangle) obj; + return x == r.x && y == r.y && width == r.width && height == r.height; + } + return super.equals(obj); + } + + @Override + public Object clone() { + return new Rectangle(x, y, width, height); + } + + @Override + public String toString() { + return "java.awt.Rectangle[x=" + x + ",y=" + y + ",width=" + width + ",height=" + height + "]"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/RenderingHints.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/RenderingHints.java new file mode 100644 index 00000000000..a50244c08d1 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/RenderingHints.java @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/// Preferences for how a `Graphics2D` renders, keyed by the `KEY_` +/// constants. The antialiasing hints switch Codename One's antialiasing; +/// the others are remembered and answered back but change nothing. +public class RenderingHints implements Map, Cloneable { + + /// The identity of one hint. + public abstract static class Key { + + private final int privatekey; + + protected Key(int privatekey) { + this.privatekey = privatekey; + } + + public abstract boolean isCompatibleValue(Object val); + + protected final int intKey() { + return privatekey; + } + + @Override + public final int hashCode() { + return super.hashCode(); + } + + @Override + public final boolean equals(Object o) { + return this == o; + } + } + + private static final class HintKey extends Key { + + private final String description; + + HintKey(int number, String description) { + super(number); + this.description = description; + } + + @Override + public boolean isCompatibleValue(Object val) { + return val instanceof HintValue && ((HintValue) val).key == this; + } + + @Override + public String toString() { + return description; + } + } + + private static final class HintValue { + + final Key key; + private final String description; + + HintValue(Key key, String description) { + this.key = key; + this.description = description; + } + + @Override + public String toString() { + return description; + } + } + + public static final Key KEY_ANTIALIASING = new HintKey(1, "Global antialiasing enable key"); + public static final Object VALUE_ANTIALIAS_ON = new HintValue(KEY_ANTIALIASING, "Antialiased rendering mode"); + public static final Object VALUE_ANTIALIAS_OFF = new HintValue(KEY_ANTIALIASING, "Nonantialiased rendering mode"); + public static final Object VALUE_ANTIALIAS_DEFAULT = new HintValue(KEY_ANTIALIASING, + "Default antialiasing rendering mode"); + + public static final Key KEY_RENDERING = new HintKey(2, "Global rendering quality key"); + public static final Object VALUE_RENDER_SPEED = new HintValue(KEY_RENDERING, "Fastest rendering methods"); + public static final Object VALUE_RENDER_QUALITY = new HintValue(KEY_RENDERING, + "Highest quality rendering methods"); + public static final Object VALUE_RENDER_DEFAULT = new HintValue(KEY_RENDERING, "Default rendering methods"); + + public static final Key KEY_DITHERING = new HintKey(3, "Dithering quality key"); + public static final Object VALUE_DITHER_DISABLE = new HintValue(KEY_DITHERING, "Nondithered rendering mode"); + public static final Object VALUE_DITHER_ENABLE = new HintValue(KEY_DITHERING, "Dithered rendering mode"); + public static final Object VALUE_DITHER_DEFAULT = new HintValue(KEY_DITHERING, "Default dithering mode"); + + public static final Key KEY_TEXT_ANTIALIASING = new HintKey(4, "Text-specific antialiasing enable key"); + public static final Object VALUE_TEXT_ANTIALIAS_ON = new HintValue(KEY_TEXT_ANTIALIASING, + "Antialiased text mode"); + public static final Object VALUE_TEXT_ANTIALIAS_OFF = new HintValue(KEY_TEXT_ANTIALIASING, + "Nonantialiased text mode"); + public static final Object VALUE_TEXT_ANTIALIAS_DEFAULT = new HintValue(KEY_TEXT_ANTIALIASING, + "Default antialiasing text mode"); + public static final Object VALUE_TEXT_ANTIALIAS_GASP = new HintValue(KEY_TEXT_ANTIALIASING, + "GASP antialiasing text mode"); + public static final Object VALUE_TEXT_ANTIALIAS_LCD_HRGB = new HintValue(KEY_TEXT_ANTIALIASING, + "LCD HRGB antialiasing text mode"); + public static final Object VALUE_TEXT_ANTIALIAS_LCD_HBGR = new HintValue(KEY_TEXT_ANTIALIASING, + "LCD HBGR antialiasing text mode"); + public static final Object VALUE_TEXT_ANTIALIAS_LCD_VRGB = new HintValue(KEY_TEXT_ANTIALIASING, + "LCD VRGB antialiasing text mode"); + public static final Object VALUE_TEXT_ANTIALIAS_LCD_VBGR = new HintValue(KEY_TEXT_ANTIALIASING, + "LCD VBGR antialiasing text mode"); + + public static final Key KEY_FRACTIONALMETRICS = new HintKey(5, "Fractional metrics enable key"); + public static final Object VALUE_FRACTIONALMETRICS_OFF = new HintValue(KEY_FRACTIONALMETRICS, + "Integer text metrics mode"); + public static final Object VALUE_FRACTIONALMETRICS_ON = new HintValue(KEY_FRACTIONALMETRICS, + "Fractional text metrics mode"); + public static final Object VALUE_FRACTIONALMETRICS_DEFAULT = new HintValue(KEY_FRACTIONALMETRICS, + "Default fractional text metrics mode"); + + public static final Key KEY_INTERPOLATION = new HintKey(6, "Image interpolation method key"); + public static final Object VALUE_INTERPOLATION_NEAREST_NEIGHBOR = new HintValue(KEY_INTERPOLATION, + "Nearest Neighbor image interpolation mode"); + public static final Object VALUE_INTERPOLATION_BILINEAR = new HintValue(KEY_INTERPOLATION, + "Bilinear image interpolation mode"); + public static final Object VALUE_INTERPOLATION_BICUBIC = new HintValue(KEY_INTERPOLATION, + "Bicubic image interpolation mode"); + + public static final Key KEY_ALPHA_INTERPOLATION = new HintKey(7, "Alpha blending interpolation method key"); + public static final Object VALUE_ALPHA_INTERPOLATION_SPEED = new HintValue(KEY_ALPHA_INTERPOLATION, + "Fastest alpha blending methods"); + public static final Object VALUE_ALPHA_INTERPOLATION_QUALITY = new HintValue(KEY_ALPHA_INTERPOLATION, + "Highest quality alpha blending methods"); + public static final Object VALUE_ALPHA_INTERPOLATION_DEFAULT = new HintValue(KEY_ALPHA_INTERPOLATION, + "Default alpha blending methods"); + + public static final Key KEY_COLOR_RENDERING = new HintKey(8, "Color rendering quality key"); + public static final Object VALUE_COLOR_RENDER_SPEED = new HintValue(KEY_COLOR_RENDERING, + "Fastest color rendering methods"); + public static final Object VALUE_COLOR_RENDER_QUALITY = new HintValue(KEY_COLOR_RENDERING, + "Highest quality color rendering methods"); + public static final Object VALUE_COLOR_RENDER_DEFAULT = new HintValue(KEY_COLOR_RENDERING, + "Default color rendering methods"); + + public static final Key KEY_STROKE_CONTROL = new HintKey(9, "Stroke normalization control key"); + public static final Object VALUE_STROKE_DEFAULT = new HintValue(KEY_STROKE_CONTROL, + "Default stroke normalization"); + public static final Object VALUE_STROKE_NORMALIZE = new HintValue(KEY_STROKE_CONTROL, + "Stroke normalization for consistency"); + public static final Object VALUE_STROKE_PURE = new HintValue(KEY_STROKE_CONTROL, + "Pure stroke conversion for accurate paths"); + + private final HashMap hintmap = new HashMap(); + + public RenderingHints(Map init) { + if (init != null) { + for (Map.Entry e : init.entrySet()) { + hintmap.put(e.getKey(), e.getValue()); + } + } + } + + public RenderingHints(Key key, Object value) { + hintmap.put(key, value); + } + + @Override + public int size() { + return hintmap.size(); + } + + @Override + public boolean isEmpty() { + return hintmap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return hintmap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return hintmap.containsValue(value); + } + + @Override + public Object get(Object key) { + return hintmap.get(key); + } + + @Override + public Object put(Object key, Object value) { + if (!(key instanceof Key)) { + throw new IllegalArgumentException("a rendering hint key must be a RenderingHints.Key"); + } + if (!((Key) key).isCompatibleValue(value)) { + throw new IllegalArgumentException(value + " incompatible with " + key); + } + return hintmap.put(key, value); + } + + public void add(RenderingHints hints) { + hintmap.putAll(hints.hintmap); + } + + @Override + public void clear() { + hintmap.clear(); + } + + @Override + public Object remove(Object key) { + return hintmap.remove(key); + } + + @Override + public void putAll(Map m) { + for (Map.Entry e : m.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + @Override + public Set keySet() { + return hintmap.keySet(); + } + + @Override + public Collection values() { + return hintmap.values(); + } + + @Override + public Set> entrySet() { + return hintmap.entrySet(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof RenderingHints) { + return hintmap.equals(((RenderingHints) o).hintmap); + } + return o instanceof Map && hintmap.equals(o); + } + + @Override + public int hashCode() { + return hintmap.hashCode(); + } + + /// A copy holding the same hints. + @Override + public Object clone() { + RenderingHints copy = new RenderingHints(null); + copy.hintmap.putAll(hintmap); + return copy; + } + + @Override + public String toString() { + return getClass().getName() + "@" + Integer.toHexString(hashCode()) + " (" + hintmap.size() + " hints)"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Shape.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Shape.java new file mode 100644 index 00000000000..e2c610e759e --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Shape.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.geom.AffineTransform; +import com.codename1.desktopcompat.java.awt.geom.PathIterator; +import com.codename1.desktopcompat.java.awt.geom.Point2D; +import com.codename1.desktopcompat.java.awt.geom.Rectangle2D; + +/// A geometric outline that can report its bounds, answer hit tests and +/// describe itself as a sequence of path segments. +public interface Shape { + + Rectangle getBounds(); + + Rectangle2D getBounds2D(); + + boolean contains(double x, double y); + + boolean contains(Point2D p); + + boolean intersects(double x, double y, double w, double h); + + boolean intersects(Rectangle2D r); + + boolean contains(double x, double y, double w, double h); + + boolean contains(Rectangle2D r); + + PathIterator getPathIterator(AffineTransform at); + + PathIterator getPathIterator(AffineTransform at, double flatness); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Stroke.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Stroke.java new file mode 100644 index 00000000000..a812d9e9979 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Stroke.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// Turns the outline of a shape into the area a pen moving along it covers. +public interface Stroke { + Shape createStrokedShape(Shape p); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Toolkit.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Toolkit.java new file mode 100644 index 00000000000..0b2bb77e220 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Toolkit.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.rt.DesktopToolkit; + +/// The few toolkit services this layer has: screen metrics, font metrics, +/// image decoding and the event queue. +public abstract class Toolkit { + + private static Toolkit toolkit; + + public Toolkit() { + } + + public static Toolkit getDefaultToolkit() { + if (toolkit == null) { + toolkit = new DesktopToolkit(); + } + return toolkit; + } + + /// The display's size in logical pixels. + public abstract Dimension getScreenSize(); + + /// Always 96: a logical pixel is 1/96 inch. + public abstract int getScreenResolution(); + + public abstract FontMetrics getFontMetrics(Font font); + + /// Does nothing: painting is flushed by Codename One. + public abstract void sync(); + + public abstract void beep(); + + /// Decodes an encoded image (PNG, JPEG) at once. + public abstract Image createImage(byte[] imagedata, int imageoffset, int imagelength); + + public Image createImage(byte[] imagedata) { + return createImage(imagedata, 0, imagedata.length); + } + + protected abstract EventQueue getSystemEventQueueImpl(); + + public final EventQueue getSystemEventQueue() { + return getSystemEventQueueImpl(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Transparency.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Transparency.java new file mode 100644 index 00000000000..28c0cd8c90b --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Transparency.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +/// How much of what lies behind something shows through it. +public interface Transparency { + int OPAQUE = 1; + int BITMASK = 2; + int TRANSLUCENT = 3; + + int getTransparency(); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Window.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Window.java new file mode 100644 index 00000000000..f60ec1cee82 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/Window.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt; + +import com.codename1.desktopcompat.java.awt.event.WindowEvent; +import com.codename1.desktopcompat.java.awt.event.WindowListener; +import com.codename1.desktopcompat.rt.EventBridge; +import com.codename1.desktopcompat.rt.FrameForm; +import com.codename1.desktopcompat.rt.Units; +import com.codename1.ui.Display; +import java.util.ArrayList; + +/// A top level window, shown as a Codename One form that it fills. +/// +/// The size and position an application sets are recorded, but the window +/// is given the size of the form once it shows. Showing a second window +/// puts its form on top; hiding or disposing it goes back to the one +/// before. Windows start hidden. +public class Window extends Container { + + private final Window owner; + private ArrayList windowListeners; + private FrameForm form; + private boolean visible; + private boolean opened; + private Image iconImage; + + public Window(Frame owner) { + this((Window) owner); + } + + public Window(Window owner) { + this.owner = owner; + setLayout(new BorderLayout()); + } + + /// The form showing this window, `null` until it is shown. + public FrameForm cn1Form() { + return form; + } + + /// The title the form carries; frames and dialogs answer theirs. + public String cn1Title() { + return ""; + } + + /// Runs what a user's request to close the window runs. + public void cn1Closing() { + dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING)); + } + + @Override + public void cn1SyncPeerBounds() { + } + + public Window getOwner() { + return owner; + } + + @Override + public Toolkit getToolkit() { + return Toolkit.getDefaultToolkit(); + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public boolean isShowing() { + return visible; + } + + @Override + public boolean isValidateRoot() { + return true; + } + + @Override + public Point getLocationOnScreen() { + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p == null) { + return new Point(getX(), getY()); + } + return new Point(Units.toLogical(p.getAbsoluteX()), Units.toLogical(p.getAbsoluteY())); + } + + @Override + public Color getBackground() { + Color c = super.getBackground(); + return c != null ? c : EventBridge.defaultBackground(); + } + + @Override + public Color getForeground() { + Color c = super.getForeground(); + return c != null ? c : EventBridge.defaultForeground(); + } + + @Override + public Font getFont() { + Font f = super.getFont(); + return f != null ? f : com.codename1.desktopcompat.rt.Fonts.defaultFont(); + } + + @Override + public void paint(Graphics g) { + g.setColor(getBackground()); + g.fillRect(0, 0, getWidth(), getHeight()); + super.paint(g); + } + + /// Sizes the window to its preferred size where it has none yet and + /// lays it out. Once shown the window keeps the size of its form. + public void pack() { + if (!isDisplayable()) { + addNotify(); + } + if (form == null) { + Dimension d = getPreferredSize(); + setSize(d.width, d.height); + } + validate(); + } + + @Override + public void setVisible(boolean b) { + if (b == visible) { + return; + } + visible = b; + if (b) { + if (!isDisplayable()) { + addNotify(); + } + if (Display.isInitialized()) { + if (form == null) { + form = new FrameForm(this); + } + form.cn1Show(); + } else { + validate(); + } + if (!opened) { + opened = true; + dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_OPENED)); + } + dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_ACTIVATED)); + } else { + if (form != null) { + form.cn1Hide(); + } + dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_DEACTIVATED)); + } + } + + /// Hides the window and releases its form. + public void dispose() { + boolean was = isDisplayable(); + setVisible(false); + if (was) { + removeNotify(); + form = null; + dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSED)); + } + } + + public void toFront() { + if (visible && form != null) { + form.cn1Show(); + } + } + + public void toBack() { + } + + public boolean isActive() { + return visible && form != null && Display.isInitialized() && Display.getInstance().getCurrent() == form; + } + + public boolean isFocused() { + return isActive(); + } + + public Component getFocusOwner() { + Component c = EventBridge.focusOwner(); + return c != null && (c == this || isAncestorOf(c)) ? c : null; + } + + /// Does nothing: a window fills its form. + public void setLocationRelativeTo(Component c) { + } + + public void setIconImage(Image image) { + iconImage = image; + } + + public java.util.List getIconImages() { + ArrayList l = new ArrayList(); + if (iconImage != null) { + l.add(iconImage); + } + return l; + } + + public void addWindowListener(WindowListener l) { + if (l != null) { + if (windowListeners == null) { + windowListeners = new ArrayList(); + } + windowListeners.add(l); + } + } + + public void removeWindowListener(WindowListener l) { + if (windowListeners != null) { + windowListeners.remove(l); + } + } + + public WindowListener[] getWindowListeners() { + return windowListeners == null ? new WindowListener[0] + : windowListeners.toArray(new WindowListener[windowListeners.size()]); + } + + @Override + protected void processEvent(AWTEvent e) { + if (e instanceof WindowEvent) { + processWindowEvent((WindowEvent) e); + } else { + super.processEvent(e); + } + } + + protected void processWindowEvent(WindowEvent e) { + WindowListener[] ls = getWindowListeners(); + for (int i = 0; i < ls.length; i++) { + switch (e.getID()) { + case WindowEvent.WINDOW_OPENED: + ls[i].windowOpened(e); + break; + case WindowEvent.WINDOW_CLOSING: + ls[i].windowClosing(e); + break; + case WindowEvent.WINDOW_CLOSED: + ls[i].windowClosed(e); + break; + case WindowEvent.WINDOW_ICONIFIED: + ls[i].windowIconified(e); + break; + case WindowEvent.WINDOW_DEICONIFIED: + ls[i].windowDeiconified(e); + break; + case WindowEvent.WINDOW_ACTIVATED: + ls[i].windowActivated(e); + break; + case WindowEvent.WINDOW_DEACTIVATED: + ls[i].windowDeactivated(e); + break; + default: + break; + } + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ActionEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ActionEvent.java new file mode 100644 index 00000000000..cf304f1f0af --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ActionEvent.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.AWTEvent; + +/// A component-defined action happened, such as a button being pressed. +public class ActionEvent extends AWTEvent { + + public static final int SHIFT_MASK = 1; + + public static final int CTRL_MASK = 2; + + public static final int META_MASK = 4; + + public static final int ALT_MASK = 8; + + public static final int ACTION_FIRST = 1001; + + public static final int ACTION_LAST = 1001; + + public static final int ACTION_PERFORMED = 1001; + + private final String actionCommand; + + private final long when; + + private final int modifiers; + + public ActionEvent(Object source, int id, String command) { + this(source, id, command, 0); + } + + public ActionEvent(Object source, int id, String command, int modifiers) { + this(source, id, command, 0L, modifiers); + } + + public ActionEvent(Object source, int id, String command, long when, int modifiers) { + super(source, id); + this.actionCommand = command; + this.when = when; + this.modifiers = modifiers; + } + + public String getActionCommand() { + return actionCommand; + } + + public long getWhen() { + return when; + } + + public int getModifiers() { + return modifiers; + } + + @Override + public String paramString() { + String typeStr; + switch (id) { + case ACTION_PERFORMED: + typeStr = "ACTION_PERFORMED"; + break; + default: + typeStr = "unknown type"; + break; + } + return typeStr + ",cmd=" + actionCommand + ",when=" + when + ",modifiers=" + + KeyEvent.getKeyModifiersText(modifiers); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ActionListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ActionListener.java new file mode 100644 index 00000000000..4ac3b4ec9f1 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ActionListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives action events. +public interface ActionListener extends EventListener { + + void actionPerformed(ActionEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/AdjustmentEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/AdjustmentEvent.java new file mode 100644 index 00000000000..b1629d8ac78 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/AdjustmentEvent.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.AWTEvent; +import com.codename1.desktopcompat.java.awt.Adjustable; + +/// An adjustable value, such as a scroll bar position, changed. +public class AdjustmentEvent extends AWTEvent { + + public static final int ADJUSTMENT_FIRST = 601; + + public static final int ADJUSTMENT_LAST = 601; + + public static final int ADJUSTMENT_VALUE_CHANGED = 601; + + public static final int UNIT_INCREMENT = 1; + + public static final int UNIT_DECREMENT = 2; + + public static final int BLOCK_DECREMENT = 3; + + public static final int BLOCK_INCREMENT = 4; + + public static final int TRACK = 5; + + private final int adjustmentType; + + private final int value; + + private final boolean isAdjusting; + + public AdjustmentEvent(Adjustable source, int id, int type, int value) { + this(source, id, type, value, false); + } + + public AdjustmentEvent(Adjustable source, int id, int type, int value, boolean isAdjusting) { + super(source, id); + this.adjustmentType = type; + this.value = value; + this.isAdjusting = isAdjusting; + } + + public Adjustable getAdjustable() { + return source instanceof Adjustable ? (Adjustable) source : null; + } + + public int getValue() { + return value; + } + + public int getAdjustmentType() { + return adjustmentType; + } + + public boolean getValueIsAdjusting() { + return isAdjusting; + } + + @Override + public String paramString() { + String typeStr; + switch (id) { + case ADJUSTMENT_VALUE_CHANGED: + typeStr = "ADJUSTMENT_VALUE_CHANGED"; + break; + default: + typeStr = "unknown type"; + break; + } + String adjTypeStr; + switch (adjustmentType) { + case UNIT_INCREMENT: + adjTypeStr = "UNIT_INCREMENT"; + break; + case UNIT_DECREMENT: + adjTypeStr = "UNIT_DECREMENT"; + break; + case BLOCK_INCREMENT: + adjTypeStr = "BLOCK_INCREMENT"; + break; + case BLOCK_DECREMENT: + adjTypeStr = "BLOCK_DECREMENT"; + break; + case TRACK: + adjTypeStr = "TRACK"; + break; + default: + adjTypeStr = "unknown type"; + break; + } + return typeStr + ",adjType=" + adjTypeStr + ",value=" + value + ",isAdjusting=" + isAdjusting; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/AdjustmentListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/AdjustmentListener.java new file mode 100644 index 00000000000..ef8a07a7679 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/AdjustmentListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives adjustment events. +public interface AdjustmentListener extends EventListener { + + void adjustmentValueChanged(AdjustmentEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentAdapter.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentAdapter.java new file mode 100644 index 00000000000..4df28b0e0dc --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentAdapter.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +/// A `ComponentListener` whose methods do nothing, to be subclassed for the +/// few events of interest. +public abstract class ComponentAdapter implements ComponentListener { + + public void componentResized(ComponentEvent e) { + } + + public void componentMoved(ComponentEvent e) { + } + + public void componentShown(ComponentEvent e) { + } + + public void componentHidden(ComponentEvent e) { + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentEvent.java new file mode 100644 index 00000000000..31d255dcb44 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentEvent.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.AWTEvent; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Rectangle; + +/// A component was moved, resized, shown or hidden. +public class ComponentEvent extends AWTEvent { + + public static final int COMPONENT_FIRST = 100; + + public static final int COMPONENT_LAST = 103; + + public static final int COMPONENT_MOVED = 100; + + public static final int COMPONENT_RESIZED = 101; + + public static final int COMPONENT_SHOWN = 102; + + public static final int COMPONENT_HIDDEN = 103; + + public ComponentEvent(Component source, int id) { + super(source, id); + } + + public Component getComponent() { + return source instanceof Component ? (Component) source : null; + } + + @Override + public String paramString() { + String type; + Component c = getComponent(); + Rectangle b = c == null ? new Rectangle() : c.getBounds(); + switch (id) { + case COMPONENT_SHOWN: + type = "COMPONENT_SHOWN"; + break; + case COMPONENT_HIDDEN: + type = "COMPONENT_HIDDEN"; + break; + case COMPONENT_MOVED: + type = "COMPONENT_MOVED (" + b.x + "," + b.y + " " + b.width + "x" + b.height + ")"; + break; + case COMPONENT_RESIZED: + type = "COMPONENT_RESIZED (" + b.x + "," + b.y + " " + b.width + "x" + b.height + ")"; + break; + default: + type = "unknown type"; + break; + } + return type; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentListener.java new file mode 100644 index 00000000000..f287d6e7dff --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ComponentListener.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives the events of a component being moved, resized, shown or hidden. +public interface ComponentListener extends EventListener { + + void componentResized(ComponentEvent e); + + void componentMoved(ComponentEvent e); + + void componentShown(ComponentEvent e); + + void componentHidden(ComponentEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerAdapter.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerAdapter.java new file mode 100644 index 00000000000..4b83a1bfee8 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerAdapter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +/// A `ContainerListener` whose methods do nothing, to be subclassed for the +/// few events of interest. +public abstract class ContainerAdapter implements ContainerListener { + + public void componentAdded(ContainerEvent e) { + } + + public void componentRemoved(ContainerEvent e) { + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerEvent.java new file mode 100644 index 00000000000..6508a89bd84 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerEvent.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Container; + +/// A component was added to or removed from a container. +public class ContainerEvent extends ComponentEvent { + + public static final int CONTAINER_FIRST = 300; + + public static final int CONTAINER_LAST = 301; + + public static final int COMPONENT_ADDED = 300; + + public static final int COMPONENT_REMOVED = 301; + + private final Component child; + + public ContainerEvent(Component source, int id, Component child) { + super(source, id); + this.child = child; + } + + public Container getContainer() { + return source instanceof Container ? (Container) source : null; + } + + public Component getChild() { + return child; + } + + @Override + public String paramString() { + String typeStr; + switch (id) { + case COMPONENT_ADDED: + typeStr = "COMPONENT_ADDED"; + break; + case COMPONENT_REMOVED: + typeStr = "COMPONENT_REMOVED"; + break; + default: + typeStr = "unknown type"; + break; + } + return typeStr + ",child=" + child; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerListener.java new file mode 100644 index 00000000000..0dead4cddbc --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ContainerListener.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives the events of components being added to or removed from a +/// container. +public interface ContainerListener extends EventListener { + + void componentAdded(ContainerEvent e); + + void componentRemoved(ContainerEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusAdapter.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusAdapter.java new file mode 100644 index 00000000000..74da537badc --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusAdapter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +/// A `FocusListener` whose methods do nothing, to be subclassed for the few +/// events of interest. +public abstract class FocusAdapter implements FocusListener { + + public void focusGained(FocusEvent e) { + } + + public void focusLost(FocusEvent e) { + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusEvent.java new file mode 100644 index 00000000000..558130f9a2e --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusEvent.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.Component; + +/// A component gained or lost the keyboard focus. +/// +/// The JDK 9 `Cause` of a focus change is not provided. +public class FocusEvent extends ComponentEvent { + + public static final int FOCUS_FIRST = 1004; + + public static final int FOCUS_LAST = 1005; + + public static final int FOCUS_GAINED = 1004; + + public static final int FOCUS_LOST = 1005; + + private final boolean temporary; + + private final Component opposite; + + public FocusEvent(Component source, int id, boolean temporary, Component opposite) { + super(source, id); + this.temporary = temporary; + this.opposite = opposite; + } + + public FocusEvent(Component source, int id, boolean temporary) { + this(source, id, temporary, null); + } + + public FocusEvent(Component source, int id) { + this(source, id, false); + } + + public boolean isTemporary() { + return temporary; + } + + public Component getOppositeComponent() { + return opposite; + } + + @Override + public String paramString() { + String typeStr; + switch (id) { + case FOCUS_GAINED: + typeStr = "FOCUS_GAINED"; + break; + case FOCUS_LOST: + typeStr = "FOCUS_LOST"; + break; + default: + typeStr = "unknown type"; + break; + } + return typeStr + (temporary ? ",temporary" : ",permanent") + ",opposite=" + opposite; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusListener.java new file mode 100644 index 00000000000..290ac855adc --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/FocusListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives the events of a component gaining or losing the keyboard focus. +public interface FocusListener extends EventListener { + + void focusGained(FocusEvent e); + + void focusLost(FocusEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/HierarchyEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/HierarchyEvent.java new file mode 100644 index 00000000000..f76f12498d5 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/HierarchyEvent.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.AWTEvent; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Container; + +/// The hierarchy a component belongs to changed: a parent was swapped, or +/// the component became displayable or showing. +public class HierarchyEvent extends AWTEvent { + + public static final int HIERARCHY_FIRST = 1400; + + public static final int HIERARCHY_CHANGED = 1400; + + public static final int ANCESTOR_MOVED = 1401; + + public static final int ANCESTOR_RESIZED = 1402; + + public static final int HIERARCHY_LAST = 1402; + + public static final int PARENT_CHANGED = 0x1; + + public static final int DISPLAYABILITY_CHANGED = 0x2; + + public static final int SHOWING_CHANGED = 0x4; + + private final Component changed; + + private final Container changedParent; + + private final long changeFlags; + + public HierarchyEvent(Component source, int id, Component changed, Container changedParent) { + this(source, id, changed, changedParent, 0L); + } + + public HierarchyEvent(Component source, int id, Component changed, Container changedParent, + long changeFlags) { + super(source, id); + this.changed = changed; + this.changedParent = changedParent; + this.changeFlags = changeFlags; + } + + public Component getComponent() { + return source instanceof Component ? (Component) source : null; + } + + public Component getChanged() { + return changed; + } + + public Container getChangedParent() { + return changedParent; + } + + public long getChangeFlags() { + return changeFlags; + } + + @Override + public String paramString() { + String typeStr; + switch (id) { + case ANCESTOR_MOVED: + typeStr = "ANCESTOR_MOVED (" + changed + "," + changedParent + ")"; + break; + case ANCESTOR_RESIZED: + typeStr = "ANCESTOR_RESIZED (" + changed + "," + changedParent + ")"; + break; + case HIERARCHY_CHANGED: + typeStr = "HIERARCHY_CHANGED (" + changed + "," + changedParent + ")"; + if ((changeFlags & PARENT_CHANGED) != 0) { + typeStr += ",PARENT_CHANGED"; + } + if ((changeFlags & DISPLAYABILITY_CHANGED) != 0) { + typeStr += ",DISPLAYABILITY_CHANGED"; + } + if ((changeFlags & SHOWING_CHANGED) != 0) { + typeStr += ",SHOWING_CHANGED"; + } + break; + default: + typeStr = "unknown type"; + break; + } + return typeStr; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/HierarchyListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/HierarchyListener.java new file mode 100644 index 00000000000..84a697892df --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/HierarchyListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives the events of the hierarchy a component belongs to changing. +public interface HierarchyListener extends EventListener { + + void hierarchyChanged(HierarchyEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/InputEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/InputEvent.java new file mode 100644 index 00000000000..cc627c66c64 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/InputEvent.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.Component; + +/// The root of the events that come from the keyboard and the mouse. +/// +/// Modifiers exist in two encodings: the old masks (`SHIFT_MASK`, ...) and the +/// extended "down" masks (`SHIFT_DOWN_MASK`, ...). Like the JDK, the key and +/// mouse event constructors accept either and keep both, so `getModifiers()` +/// and `getModifiersEx()` answer for the same event. +public abstract class InputEvent extends ComponentEvent { + + public static final int SHIFT_MASK = 1; + + public static final int CTRL_MASK = 2; + + public static final int META_MASK = 4; + + public static final int ALT_MASK = 8; + + public static final int ALT_GRAPH_MASK = 32; + + public static final int BUTTON1_MASK = 16; + + public static final int BUTTON2_MASK = 8; + + public static final int BUTTON3_MASK = 4; + + public static final int SHIFT_DOWN_MASK = 64; + + public static final int CTRL_DOWN_MASK = 128; + + public static final int META_DOWN_MASK = 256; + + public static final int ALT_DOWN_MASK = 512; + + public static final int BUTTON1_DOWN_MASK = 1024; + + public static final int BUTTON2_DOWN_MASK = 2048; + + public static final int BUTTON3_DOWN_MASK = 4096; + + public static final int ALT_GRAPH_DOWN_MASK = 8192; + + /// The old encoding occupies the bits below `SHIFT_DOWN_MASK`. + static final int JDK_1_3_MODIFIERS = SHIFT_DOWN_MASK - 1; + + /// Bits from the fourteenth up are the extra mouse buttons, which have no + /// old encoding. + static final int HIGH_MODIFIERS = ~((1 << 14) - 1); + + private static final int BUTTON_COUNT = 20; + + private final long when; + + int modifiers; + + InputEvent(Component source, int id, long when, int modifiers) { + super(source, id); + this.when = when; + this.modifiers = modifiers; + } + + /// Fills in whichever encoding of the modifiers the caller left out. + static int normalize(int m, boolean mouse) { + boolean old = (m & (JDK_1_3_MODIFIERS | HIGH_MODIFIERS)) != 0; + boolean extended = (m & ~JDK_1_3_MODIFIERS) != 0; + int r = m; + if (old && !extended) { + if ((m & SHIFT_MASK) != 0) { + r |= SHIFT_DOWN_MASK; + } + if ((m & CTRL_MASK) != 0) { + r |= CTRL_DOWN_MASK; + } + if ((m & ALT_GRAPH_MASK) != 0) { + r |= ALT_GRAPH_DOWN_MASK; + } + if (mouse) { + if ((m & BUTTON1_MASK) != 0) { + r |= BUTTON1_DOWN_MASK; + } + if ((m & BUTTON2_MASK) != 0) { + r |= BUTTON2_DOWN_MASK; + } + if ((m & BUTTON3_MASK) != 0) { + r |= BUTTON3_DOWN_MASK; + } + } else { + if ((m & ALT_MASK) != 0) { + r |= ALT_DOWN_MASK; + } + if ((m & META_MASK) != 0) { + r |= META_DOWN_MASK; + } + } + } else if (extended && !old) { + if ((m & SHIFT_DOWN_MASK) != 0) { + r |= SHIFT_MASK; + } + if ((m & CTRL_DOWN_MASK) != 0) { + r |= CTRL_MASK; + } + if ((m & ALT_GRAPH_DOWN_MASK) != 0) { + r |= ALT_GRAPH_MASK; + } + if ((m & ALT_DOWN_MASK) != 0) { + r |= ALT_MASK; + } + if ((m & META_DOWN_MASK) != 0) { + r |= META_MASK; + } + if (mouse) { + if ((m & BUTTON1_DOWN_MASK) != 0) { + r |= BUTTON1_MASK; + } + if ((m & BUTTON2_DOWN_MASK) != 0) { + r |= BUTTON2_MASK; + } + if ((m & BUTTON3_DOWN_MASK) != 0) { + r |= BUTTON3_MASK; + } + } + } + return r; + } + + private static int buttonMask(int index) { + if (index == 0) { + return BUTTON1_DOWN_MASK; + } + if (index == 1) { + return BUTTON2_DOWN_MASK; + } + if (index == 2) { + return BUTTON3_DOWN_MASK; + } + return 1 << (11 + index); + } + + public boolean isShiftDown() { + return (modifiers & SHIFT_DOWN_MASK) != 0; + } + + public boolean isControlDown() { + return (modifiers & CTRL_DOWN_MASK) != 0; + } + + public boolean isMetaDown() { + return (modifiers & META_DOWN_MASK) != 0; + } + + public boolean isAltDown() { + return (modifiers & ALT_DOWN_MASK) != 0; + } + + public boolean isAltGraphDown() { + return (modifiers & ALT_GRAPH_DOWN_MASK) != 0; + } + + public long getWhen() { + return when; + } + + public int getModifiers() { + return modifiers & (JDK_1_3_MODIFIERS | HIGH_MODIFIERS); + } + + public int getModifiersEx() { + return modifiers & ~JDK_1_3_MODIFIERS; + } + + @Override + public void consume() { + consumed = true; + } + + @Override + public boolean isConsumed() { + return consumed; + } + + /// The extended mask of a mouse button, counting from one. + /// + /// #### Throws + /// + /// - `IllegalArgumentException`: if the button number is not positive or + /// beyond the twenty buttons supported + public static int getMaskForButton(int button) { + if (button <= 0 || button > BUTTON_COUNT) { + throw new IllegalArgumentException("button doesn't exist " + button); + } + return buttonMask(button - 1); + } + + public static String getModifiersExText(int modifiers) { + StringBuilder buf = new StringBuilder(); + if ((modifiers & META_DOWN_MASK) != 0) { + add(buf, "Meta"); + } + if ((modifiers & CTRL_DOWN_MASK) != 0) { + add(buf, "Ctrl"); + } + if ((modifiers & ALT_DOWN_MASK) != 0) { + add(buf, "Alt"); + } + if ((modifiers & SHIFT_DOWN_MASK) != 0) { + add(buf, "Shift"); + } + if ((modifiers & ALT_GRAPH_DOWN_MASK) != 0) { + add(buf, "Alt Graph"); + } + for (int i = 0; i < BUTTON_COUNT; i++) { + if ((modifiers & buttonMask(i)) != 0) { + add(buf, "Button" + (i + 1)); + } + } + return buf.toString(); + } + + private static void add(StringBuilder buf, String name) { + if (buf.length() > 0) { + buf.append('+'); + } + buf.append(name); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/InvocationEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/InvocationEvent.java new file mode 100644 index 00000000000..19f3f360dd4 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/InvocationEvent.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.ActiveEvent; +import com.codename1.desktopcompat.java.awt.AWTEvent; + +/// An event that runs a `Runnable` when it is dispatched. +/// +/// The user interface is single threaded here, so the notifier of the JDK +/// constructors is accepted but nobody waits on it. +public class InvocationEvent extends AWTEvent implements ActiveEvent { + + public static final int INVOCATION_FIRST = 1200; + + public static final int INVOCATION_DEFAULT = 1200; + + public static final int INVOCATION_LAST = 1200; + + private final Runnable runnable; + + private final boolean catchExceptions; + + private final long when; + + private Throwable throwable; + + public InvocationEvent(Object source, Runnable runnable) { + this(source, INVOCATION_DEFAULT, runnable, null, false); + } + + public InvocationEvent(Object source, Runnable runnable, Object notifier, boolean catchThrowables) { + this(source, INVOCATION_DEFAULT, runnable, notifier, catchThrowables); + } + + protected InvocationEvent(Object source, int id, Runnable runnable, Object notifier, + boolean catchThrowables) { + super(source, id); + this.runnable = runnable; + this.catchExceptions = catchThrowables; + this.when = System.currentTimeMillis(); + } + + public void dispatch() { + if (catchExceptions) { + try { + runnable.run(); + } catch (Throwable t) { + throwable = t; + } + } else { + runnable.run(); + } + } + + public Exception getException() { + return catchExceptions && throwable instanceof Exception ? (Exception) throwable : null; + } + + public Throwable getThrowable() { + return catchExceptions ? throwable : null; + } + + public long getWhen() { + return when; + } + + @Override + public String paramString() { + String typeStr; + switch (id) { + case INVOCATION_DEFAULT: + typeStr = "INVOCATION_DEFAULT"; + break; + default: + typeStr = "unknown type"; + break; + } + return typeStr + ",runnable=" + runnable + ",when=" + when; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ItemEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ItemEvent.java new file mode 100644 index 00000000000..f4ed8e2409d --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ItemEvent.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.AWTEvent; +import com.codename1.desktopcompat.java.awt.ItemSelectable; + +/// An item was selected or deselected. +public class ItemEvent extends AWTEvent { + + public static final int ITEM_FIRST = 701; + + public static final int ITEM_LAST = 701; + + public static final int ITEM_STATE_CHANGED = 701; + + public static final int SELECTED = 1; + + public static final int DESELECTED = 2; + + private final Object item; + + private final int stateChange; + + public ItemEvent(ItemSelectable source, int id, Object item, int stateChange) { + super(source, id); + this.item = item; + this.stateChange = stateChange; + } + + public ItemSelectable getItemSelectable() { + return source instanceof ItemSelectable ? (ItemSelectable) source : null; + } + + public Object getItem() { + return item; + } + + public int getStateChange() { + return stateChange; + } + + @Override + public String paramString() { + String typeStr; + switch (id) { + case ITEM_STATE_CHANGED: + typeStr = "ITEM_STATE_CHANGED"; + break; + default: + typeStr = "unknown type"; + break; + } + String stateStr; + switch (stateChange) { + case SELECTED: + stateStr = "SELECTED"; + break; + case DESELECTED: + stateStr = "DESELECTED"; + break; + default: + stateStr = "unknown type"; + break; + } + return typeStr + ",item=" + item + ",stateChange=" + stateStr; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ItemListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ItemListener.java new file mode 100644 index 00000000000..6c8984f08e7 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/ItemListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives item selection events. +public interface ItemListener extends EventListener { + + void itemStateChanged(ItemEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyAdapter.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyAdapter.java new file mode 100644 index 00000000000..4f006b00312 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyAdapter.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +/// A `KeyListener` whose methods do nothing, to be subclassed for the few +/// events of interest. +public abstract class KeyAdapter implements KeyListener { + + public void keyTyped(KeyEvent e) { + } + + public void keyPressed(KeyEvent e) { + } + + public void keyReleased(KeyEvent e) { + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyEvent.java new file mode 100644 index 00000000000..904005dbb5b --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyEvent.java @@ -0,0 +1,1013 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.Component; + +/// A keyboard event: a key was pressed, released or typed. +/// +/// Key texts are English; the JDK looks them up in a localized resource +/// bundle. The `FocusEvent` style cause and the input-method related members +/// of the JDK class are not provided. +public class KeyEvent extends InputEvent { + + public static final int KEY_FIRST = 400; + + public static final int KEY_LAST = 402; + + public static final int KEY_TYPED = 400; + + public static final int KEY_PRESSED = 401; + + public static final int KEY_RELEASED = 402; + + public static final char CHAR_UNDEFINED = 0xFFFF; + + public static final int KEY_LOCATION_UNKNOWN = 0; + + public static final int KEY_LOCATION_STANDARD = 1; + + public static final int KEY_LOCATION_LEFT = 2; + + public static final int KEY_LOCATION_RIGHT = 3; + + public static final int KEY_LOCATION_NUMPAD = 4; + + private static final int EXTENDED_FLAG = 0x01000000; + + public static final int VK_UNDEFINED = 0x0; + + public static final int VK_CANCEL = 0x3; + + public static final int VK_BACK_SPACE = 0x8; + + public static final int VK_TAB = 0x9; + + public static final int VK_ENTER = 0xA; + + public static final int VK_CLEAR = 0xC; + + public static final int VK_SHIFT = 0x10; + + public static final int VK_CONTROL = 0x11; + + public static final int VK_ALT = 0x12; + + public static final int VK_PAUSE = 0x13; + + public static final int VK_CAPS_LOCK = 0x14; + + public static final int VK_KANA = 0x15; + + public static final int VK_FINAL = 0x18; + + public static final int VK_KANJI = 0x19; + + public static final int VK_ESCAPE = 0x1B; + + public static final int VK_CONVERT = 0x1C; + + public static final int VK_NONCONVERT = 0x1D; + + public static final int VK_ACCEPT = 0x1E; + + public static final int VK_MODECHANGE = 0x1F; + + public static final int VK_SPACE = 0x20; + + public static final int VK_PAGE_UP = 0x21; + + public static final int VK_PAGE_DOWN = 0x22; + + public static final int VK_END = 0x23; + + public static final int VK_HOME = 0x24; + + public static final int VK_LEFT = 0x25; + + public static final int VK_UP = 0x26; + + public static final int VK_RIGHT = 0x27; + + public static final int VK_DOWN = 0x28; + + public static final int VK_COMMA = 0x2C; + + public static final int VK_MINUS = 0x2D; + + public static final int VK_PERIOD = 0x2E; + + public static final int VK_SLASH = 0x2F; + + public static final int VK_0 = 0x30; + + public static final int VK_1 = 0x31; + + public static final int VK_2 = 0x32; + + public static final int VK_3 = 0x33; + + public static final int VK_4 = 0x34; + + public static final int VK_5 = 0x35; + + public static final int VK_6 = 0x36; + + public static final int VK_7 = 0x37; + + public static final int VK_8 = 0x38; + + public static final int VK_9 = 0x39; + + public static final int VK_SEMICOLON = 0x3B; + + public static final int VK_EQUALS = 0x3D; + + public static final int VK_A = 0x41; + + public static final int VK_B = 0x42; + + public static final int VK_C = 0x43; + + public static final int VK_D = 0x44; + + public static final int VK_E = 0x45; + + public static final int VK_F = 0x46; + + public static final int VK_G = 0x47; + + public static final int VK_H = 0x48; + + public static final int VK_I = 0x49; + + public static final int VK_J = 0x4A; + + public static final int VK_K = 0x4B; + + public static final int VK_L = 0x4C; + + public static final int VK_M = 0x4D; + + public static final int VK_N = 0x4E; + + public static final int VK_O = 0x4F; + + public static final int VK_P = 0x50; + + public static final int VK_Q = 0x51; + + public static final int VK_R = 0x52; + + public static final int VK_S = 0x53; + + public static final int VK_T = 0x54; + + public static final int VK_U = 0x55; + + public static final int VK_V = 0x56; + + public static final int VK_W = 0x57; + + public static final int VK_X = 0x58; + + public static final int VK_Y = 0x59; + + public static final int VK_Z = 0x5A; + + public static final int VK_OPEN_BRACKET = 0x5B; + + public static final int VK_BACK_SLASH = 0x5C; + + public static final int VK_CLOSE_BRACKET = 0x5D; + + public static final int VK_NUMPAD0 = 0x60; + + public static final int VK_NUMPAD1 = 0x61; + + public static final int VK_NUMPAD2 = 0x62; + + public static final int VK_NUMPAD3 = 0x63; + + public static final int VK_NUMPAD4 = 0x64; + + public static final int VK_NUMPAD5 = 0x65; + + public static final int VK_NUMPAD6 = 0x66; + + public static final int VK_NUMPAD7 = 0x67; + + public static final int VK_NUMPAD8 = 0x68; + + public static final int VK_NUMPAD9 = 0x69; + + public static final int VK_MULTIPLY = 0x6A; + + public static final int VK_ADD = 0x6B; + + public static final int VK_SEPARATER = 0x6C; + + public static final int VK_SEPARATOR = 0x6C; + + public static final int VK_SUBTRACT = 0x6D; + + public static final int VK_DECIMAL = 0x6E; + + public static final int VK_DIVIDE = 0x6F; + + public static final int VK_F1 = 0x70; + + public static final int VK_F2 = 0x71; + + public static final int VK_F3 = 0x72; + + public static final int VK_F4 = 0x73; + + public static final int VK_F5 = 0x74; + + public static final int VK_F6 = 0x75; + + public static final int VK_F7 = 0x76; + + public static final int VK_F8 = 0x77; + + public static final int VK_F9 = 0x78; + + public static final int VK_F10 = 0x79; + + public static final int VK_F11 = 0x7A; + + public static final int VK_F12 = 0x7B; + + public static final int VK_DELETE = 0x7F; + + public static final int VK_DEAD_GRAVE = 0x80; + + public static final int VK_DEAD_ACUTE = 0x81; + + public static final int VK_DEAD_CIRCUMFLEX = 0x82; + + public static final int VK_DEAD_TILDE = 0x83; + + public static final int VK_DEAD_MACRON = 0x84; + + public static final int VK_DEAD_BREVE = 0x85; + + public static final int VK_DEAD_ABOVEDOT = 0x86; + + public static final int VK_DEAD_DIAERESIS = 0x87; + + public static final int VK_DEAD_ABOVERING = 0x88; + + public static final int VK_DEAD_DOUBLEACUTE = 0x89; + + public static final int VK_DEAD_CARON = 0x8A; + + public static final int VK_DEAD_CEDILLA = 0x8B; + + public static final int VK_DEAD_OGONEK = 0x8C; + + public static final int VK_DEAD_IOTA = 0x8D; + + public static final int VK_DEAD_VOICED_SOUND = 0x8E; + + public static final int VK_DEAD_SEMIVOICED_SOUND = 0x8F; + + public static final int VK_NUM_LOCK = 0x90; + + public static final int VK_SCROLL_LOCK = 0x91; + + public static final int VK_AMPERSAND = 0x96; + + public static final int VK_ASTERISK = 0x97; + + public static final int VK_QUOTEDBL = 0x98; + + public static final int VK_LESS = 0x99; + + public static final int VK_PRINTSCREEN = 0x9A; + + public static final int VK_INSERT = 0x9B; + + public static final int VK_HELP = 0x9C; + + public static final int VK_META = 0x9D; + + public static final int VK_GREATER = 0xA0; + + public static final int VK_BRACELEFT = 0xA1; + + public static final int VK_BRACERIGHT = 0xA2; + + public static final int VK_BACK_QUOTE = 0xC0; + + public static final int VK_QUOTE = 0xDE; + + public static final int VK_KP_UP = 0xE0; + + public static final int VK_KP_DOWN = 0xE1; + + public static final int VK_KP_LEFT = 0xE2; + + public static final int VK_KP_RIGHT = 0xE3; + + public static final int VK_ALPHANUMERIC = 0xF0; + + public static final int VK_KATAKANA = 0xF1; + + public static final int VK_HIRAGANA = 0xF2; + + public static final int VK_FULL_WIDTH = 0xF3; + + public static final int VK_HALF_WIDTH = 0xF4; + + public static final int VK_ROMAN_CHARACTERS = 0xF5; + + public static final int VK_ALL_CANDIDATES = 0x100; + + public static final int VK_PREVIOUS_CANDIDATE = 0x101; + + public static final int VK_CODE_INPUT = 0x102; + + public static final int VK_JAPANESE_KATAKANA = 0x103; + + public static final int VK_JAPANESE_HIRAGANA = 0x104; + + public static final int VK_JAPANESE_ROMAN = 0x105; + + public static final int VK_KANA_LOCK = 0x106; + + public static final int VK_INPUT_METHOD_ON_OFF = 0x107; + + public static final int VK_AT = 0x200; + + public static final int VK_COLON = 0x201; + + public static final int VK_CIRCUMFLEX = 0x202; + + public static final int VK_DOLLAR = 0x203; + + public static final int VK_EURO_SIGN = 0x204; + + public static final int VK_EXCLAMATION_MARK = 0x205; + + public static final int VK_INVERTED_EXCLAMATION_MARK = 0x206; + + public static final int VK_LEFT_PARENTHESIS = 0x207; + + public static final int VK_NUMBER_SIGN = 0x208; + + public static final int VK_PLUS = 0x209; + + public static final int VK_RIGHT_PARENTHESIS = 0x20A; + + public static final int VK_UNDERSCORE = 0x20B; + + public static final int VK_WINDOWS = 0x20C; + + public static final int VK_CONTEXT_MENU = 0x20D; + + public static final int VK_F13 = 0xF000; + + public static final int VK_F14 = 0xF001; + + public static final int VK_F15 = 0xF002; + + public static final int VK_F16 = 0xF003; + + public static final int VK_F17 = 0xF004; + + public static final int VK_F18 = 0xF005; + + public static final int VK_F19 = 0xF006; + + public static final int VK_F20 = 0xF007; + + public static final int VK_F21 = 0xF008; + + public static final int VK_F22 = 0xF009; + + public static final int VK_F23 = 0xF00A; + + public static final int VK_F24 = 0xF00B; + + public static final int VK_COMPOSE = 0xFF20; + + public static final int VK_BEGIN = 0xFF58; + + public static final int VK_ALT_GRAPH = 0xFF7E; + + public static final int VK_STOP = 0xFFC8; + + public static final int VK_AGAIN = 0xFFC9; + + public static final int VK_PROPS = 0xFFCA; + + public static final int VK_UNDO = 0xFFCB; + + public static final int VK_COPY = 0xFFCD; + + public static final int VK_PASTE = 0xFFCF; + + public static final int VK_FIND = 0xFFD0; + + public static final int VK_CUT = 0xFFD1; + + private int keyCode; + + private char keyChar; + + private final int keyLocation; + + private final int extendedKeyCode; + + public KeyEvent(Component source, int id, long when, int modifiers, int keyCode, char keyChar, + int keyLocation) { + super(source, id, when, modifiers); + if (id == KEY_TYPED) { + if (keyChar == CHAR_UNDEFINED) { + throw new IllegalArgumentException("invalid keyChar"); + } + if (keyCode != VK_UNDEFINED) { + throw new IllegalArgumentException("invalid keyCode"); + } + if (keyLocation != KEY_LOCATION_UNKNOWN) { + throw new IllegalArgumentException("invalid keyLocation"); + } + } + this.keyCode = keyCode; + this.keyChar = keyChar; + this.keyLocation = keyLocation >= KEY_LOCATION_UNKNOWN && keyLocation <= KEY_LOCATION_NUMPAD + ? keyLocation : KEY_LOCATION_UNKNOWN; + this.extendedKeyCode = keyCode == VK_UNDEFINED ? getExtendedKeyCodeForChar(keyChar) : keyCode; + this.modifiers = normalize(modifiers, false); + } + + public KeyEvent(Component source, int id, long when, int modifiers, int keyCode, char keyChar) { + this(source, id, when, modifiers, keyCode, keyChar, KEY_LOCATION_UNKNOWN); + } + + @Deprecated + public KeyEvent(Component source, int id, long when, int modifiers, int keyCode) { + this(source, id, when, modifiers, keyCode, (char) keyCode); + } + + public int getKeyCode() { + return keyCode; + } + + public void setKeyCode(int keyCode) { + this.keyCode = keyCode; + } + + public char getKeyChar() { + return keyChar; + } + + public void setKeyChar(char keyChar) { + this.keyChar = keyChar; + } + + @Deprecated + public void setModifiers(int modifiers) { + this.modifiers = normalize(modifiers, false); + } + + public int getKeyLocation() { + return keyLocation; + } + + public int getExtendedKeyCode() { + return extendedKeyCode; + } + + /// The extended key code of a character: the virtual key code when the + /// character has an unambiguous one, otherwise the character with the + /// extended flag set. + public static int getExtendedKeyCodeForChar(int c) { + if (c >= 'a' && c <= 'z') { + return c - 'a' + 'A'; + } + if (c >= 'A' && c <= 'Z' || c >= '0' && c <= '9') { + return c; + } + switch (c) { + case ' ': + return VK_SPACE; + case ',': + return VK_COMMA; + case '-': + return VK_MINUS; + case '.': + return VK_PERIOD; + case '/': + return VK_SLASH; + case ';': + return VK_SEMICOLON; + case '=': + return VK_EQUALS; + case '[': + return VK_OPEN_BRACKET; + case '\\': + return VK_BACK_SLASH; + case ']': + return VK_CLOSE_BRACKET; + case '`': + return VK_BACK_QUOTE; + case '\'': + return VK_QUOTE; + case '\t': + return VK_TAB; + case '\n': + return VK_ENTER; + case '\b': + return VK_BACK_SPACE; + case 0x1b: + return VK_ESCAPE; + case 0x7f: + return VK_DELETE; + default: + return c | EXTENDED_FLAG; + } + } + + public boolean isActionKey() { + switch (keyCode) { + case VK_HOME: + case VK_END: + case VK_PAGE_UP: + case VK_PAGE_DOWN: + case VK_UP: + case VK_DOWN: + case VK_LEFT: + case VK_RIGHT: + case VK_BEGIN: + case VK_KP_LEFT: + case VK_KP_UP: + case VK_KP_RIGHT: + case VK_KP_DOWN: + case VK_F1: + case VK_F2: + case VK_F3: + case VK_F4: + case VK_F5: + case VK_F6: + case VK_F7: + case VK_F8: + case VK_F9: + case VK_F10: + case VK_F11: + case VK_F12: + case VK_F13: + case VK_F14: + case VK_F15: + case VK_F16: + case VK_F17: + case VK_F18: + case VK_F19: + case VK_F20: + case VK_F21: + case VK_F22: + case VK_F23: + case VK_F24: + case VK_PRINTSCREEN: + case VK_SCROLL_LOCK: + case VK_CAPS_LOCK: + case VK_NUM_LOCK: + case VK_PAUSE: + case VK_INSERT: + case VK_FINAL: + case VK_CONVERT: + case VK_NONCONVERT: + case VK_ACCEPT: + case VK_MODECHANGE: + case VK_KANA: + case VK_KANJI: + case VK_ALPHANUMERIC: + case VK_KATAKANA: + case VK_HIRAGANA: + case VK_FULL_WIDTH: + case VK_HALF_WIDTH: + case VK_ROMAN_CHARACTERS: + case VK_ALL_CANDIDATES: + case VK_PREVIOUS_CANDIDATE: + case VK_CODE_INPUT: + case VK_JAPANESE_KATAKANA: + case VK_JAPANESE_HIRAGANA: + case VK_JAPANESE_ROMAN: + case VK_KANA_LOCK: + case VK_INPUT_METHOD_ON_OFF: + case VK_AGAIN: + case VK_UNDO: + case VK_COPY: + case VK_PASTE: + case VK_CUT: + case VK_FIND: + case VK_PROPS: + case VK_STOP: + case VK_HELP: + case VK_WINDOWS: + case VK_CONTEXT_MENU: + return true; + default: + return false; + } + } + + /// The English name of a key code. + public static String getKeyText(int keyCode) { + if (keyCode >= VK_0 && keyCode <= VK_9 || keyCode >= VK_A && keyCode <= VK_Z) { + return String.valueOf((char) keyCode); + } + if (keyCode >= VK_NUMPAD0 && keyCode <= VK_NUMPAD9) { + return "NumPad-" + (char) ('0' + keyCode - VK_NUMPAD0); + } + if (keyCode >= VK_F1 && keyCode <= VK_F12) { + return "F" + (keyCode - VK_F1 + 1); + } + if (keyCode >= VK_F13 && keyCode <= VK_F24) { + return "F" + (keyCode - VK_F13 + 13); + } + String name = namedKey(keyCode); + if (name != null) { + return name; + } + if ((keyCode & EXTENDED_FLAG) != 0) { + return String.valueOf((char) (keyCode ^ EXTENDED_FLAG)); + } + return "Unknown keyCode: 0x" + Integer.toHexString(keyCode); + } + + private static String namedKey(int keyCode) { + switch (keyCode) { + case VK_ENTER: + return "Enter"; + case VK_BACK_SPACE: + return "Backspace"; + case VK_TAB: + return "Tab"; + case VK_CANCEL: + return "Cancel"; + case VK_CLEAR: + return "Clear"; + case VK_COMPOSE: + return "Compose"; + case VK_PAUSE: + return "Pause"; + case VK_CAPS_LOCK: + return "Caps Lock"; + case VK_ESCAPE: + return "Escape"; + case VK_SPACE: + return "Space"; + case VK_PAGE_UP: + return "Page Up"; + case VK_PAGE_DOWN: + return "Page Down"; + case VK_END: + return "End"; + case VK_HOME: + return "Home"; + case VK_LEFT: + return "Left"; + case VK_UP: + return "Up"; + case VK_RIGHT: + return "Right"; + case VK_DOWN: + return "Down"; + case VK_BEGIN: + return "Begin"; + case VK_SHIFT: + return "Shift"; + case VK_CONTROL: + return "Control"; + case VK_ALT: + return "Alt"; + case VK_META: + return "Meta"; + case VK_ALT_GRAPH: + return "Alt Graph"; + case VK_COMMA: + return "Comma"; + case VK_PERIOD: + return "Period"; + case VK_SLASH: + return "Slash"; + case VK_SEMICOLON: + return "Semicolon"; + case VK_EQUALS: + return "Equals"; + case VK_OPEN_BRACKET: + return "Open Bracket"; + case VK_BACK_SLASH: + return "Back Slash"; + case VK_CLOSE_BRACKET: + return "Close Bracket"; + case VK_MULTIPLY: + return "NumPad *"; + case VK_ADD: + return "NumPad +"; + case VK_SEPARATOR: + return "NumPad ,"; + case VK_SUBTRACT: + return "NumPad -"; + case VK_DECIMAL: + return "NumPad ."; + case VK_DIVIDE: + return "NumPad /"; + case VK_DELETE: + return "Delete"; + case VK_NUM_LOCK: + return "Num Lock"; + case VK_SCROLL_LOCK: + return "Scroll Lock"; + case VK_WINDOWS: + return "Windows"; + case VK_CONTEXT_MENU: + return "Context Menu"; + case VK_PRINTSCREEN: + return "Print Screen"; + case VK_INSERT: + return "Insert"; + case VK_HELP: + return "Help"; + case VK_BACK_QUOTE: + return "Back Quote"; + case VK_QUOTE: + return "Quote"; + case VK_KP_UP: + return "NumPad Up"; + case VK_KP_DOWN: + return "NumPad Down"; + case VK_KP_LEFT: + return "NumPad Left"; + case VK_KP_RIGHT: + return "NumPad Right"; + case VK_DEAD_GRAVE: + return "Dead Grave"; + case VK_DEAD_ACUTE: + return "Dead Acute"; + case VK_DEAD_CIRCUMFLEX: + return "Dead Circumflex"; + case VK_DEAD_TILDE: + return "Dead Tilde"; + case VK_DEAD_MACRON: + return "Dead Macron"; + case VK_DEAD_BREVE: + return "Dead Breve"; + case VK_DEAD_ABOVEDOT: + return "Dead Above Dot"; + case VK_DEAD_DIAERESIS: + return "Dead Diaeresis"; + case VK_DEAD_ABOVERING: + return "Dead Above Ring"; + case VK_DEAD_DOUBLEACUTE: + return "Dead Double Acute"; + case VK_DEAD_CARON: + return "Dead Caron"; + case VK_DEAD_CEDILLA: + return "Dead Cedilla"; + case VK_DEAD_OGONEK: + return "Dead Ogonek"; + case VK_DEAD_IOTA: + return "Dead Iota"; + case VK_DEAD_VOICED_SOUND: + return "Dead Voiced Sound"; + case VK_DEAD_SEMIVOICED_SOUND: + return "Dead Semivoiced Sound"; + case VK_AMPERSAND: + return "Ampersand"; + case VK_ASTERISK: + return "Asterisk"; + case VK_QUOTEDBL: + return "Double Quote"; + case VK_LESS: + return "Less"; + case VK_GREATER: + return "Greater"; + case VK_BRACELEFT: + return "Left Brace"; + case VK_BRACERIGHT: + return "Right Brace"; + case VK_AT: + return "At"; + case VK_COLON: + return "Colon"; + case VK_CIRCUMFLEX: + return "Circumflex"; + case VK_DOLLAR: + return "Dollar"; + case VK_EURO_SIGN: + return "Euro"; + case VK_EXCLAMATION_MARK: + return "Exclamation Mark"; + case VK_INVERTED_EXCLAMATION_MARK: + return "Inverted Exclamation Mark"; + case VK_LEFT_PARENTHESIS: + return "Left Parenthesis"; + case VK_NUMBER_SIGN: + return "Number Sign"; + case VK_MINUS: + return "Minus"; + case VK_PLUS: + return "Plus"; + case VK_RIGHT_PARENTHESIS: + return "Right Parenthesis"; + case VK_UNDERSCORE: + return "Underscore"; + case VK_FINAL: + return "Final"; + case VK_CONVERT: + return "Convert"; + case VK_NONCONVERT: + return "No Convert"; + case VK_ACCEPT: + return "Accept"; + case VK_MODECHANGE: + return "Mode Change"; + case VK_KANA: + return "Kana"; + case VK_KANJI: + return "Kanji"; + case VK_ALPHANUMERIC: + return "Alphanumeric"; + case VK_KATAKANA: + return "Katakana"; + case VK_HIRAGANA: + return "Hiragana"; + case VK_FULL_WIDTH: + return "Full-Width"; + case VK_HALF_WIDTH: + return "Half-Width"; + case VK_ROMAN_CHARACTERS: + return "Roman Characters"; + case VK_ALL_CANDIDATES: + return "All Candidates"; + case VK_PREVIOUS_CANDIDATE: + return "Previous Candidate"; + case VK_CODE_INPUT: + return "Code Input"; + case VK_JAPANESE_KATAKANA: + return "Japanese Katakana"; + case VK_JAPANESE_HIRAGANA: + return "Japanese Hiragana"; + case VK_JAPANESE_ROMAN: + return "Japanese Roman"; + case VK_KANA_LOCK: + return "Kana Lock"; + case VK_INPUT_METHOD_ON_OFF: + return "Input Method On/Off"; + case VK_AGAIN: + return "Again"; + case VK_UNDO: + return "Undo"; + case VK_COPY: + return "Copy"; + case VK_PASTE: + return "Paste"; + case VK_CUT: + return "Cut"; + case VK_FIND: + return "Find"; + case VK_PROPS: + return "Props"; + case VK_STOP: + return "Stop"; + default: + return null; + } + } + + /// The English names of the old-style modifier bits, joined by plus + /// signs. + public static String getKeyModifiersText(int modifiers) { + StringBuilder buf = new StringBuilder(); + if ((modifiers & META_MASK) != 0) { + add(buf, "Meta"); + } + if ((modifiers & CTRL_MASK) != 0) { + add(buf, "Ctrl"); + } + if ((modifiers & ALT_MASK) != 0) { + add(buf, "Alt"); + } + if ((modifiers & SHIFT_MASK) != 0) { + add(buf, "Shift"); + } + if ((modifiers & ALT_GRAPH_MASK) != 0) { + add(buf, "Alt Graph"); + } + if ((modifiers & BUTTON1_MASK) != 0) { + add(buf, "Button1"); + } + return buf.toString(); + } + + private static void add(StringBuilder buf, String name) { + if (buf.length() > 0) { + buf.append('+'); + } + buf.append(name); + } + + @Override + public String paramString() { + StringBuilder str = new StringBuilder(100); + switch (id) { + case KEY_PRESSED: + str.append("KEY_PRESSED"); + break; + case KEY_RELEASED: + str.append("KEY_RELEASED"); + break; + case KEY_TYPED: + str.append("KEY_TYPED"); + break; + default: + str.append("unknown type"); + break; + } + str.append(",keyCode=").append(keyCode); + str.append(",keyText=").append(getKeyText(keyCode)); + str.append(",keyChar="); + switch (keyChar) { + case '\b': + str.append("Backspace"); + break; + case '\t': + str.append("Tab"); + break; + case '\n': + str.append("Newline"); + break; + case 0x18: + str.append("Cancel"); + break; + case 0x1b: + str.append("Escape"); + break; + case 0x7f: + str.append("Delete"); + break; + case CHAR_UNDEFINED: + str.append("Undefined"); + break; + default: + if (keyChar < ' ') { + str.append("Unknown keyChar 0x").append(Integer.toHexString(keyChar)); + } else { + str.append('\'').append(keyChar).append('\''); + } + break; + } + if (getModifiers() != 0) { + str.append(",modifiers=").append(getKeyModifiersText(modifiers)); + } + if (getModifiersEx() != 0) { + str.append(",extModifiers=").append(getModifiersExText(modifiers)); + } + str.append(",keyLocation="); + switch (keyLocation) { + case KEY_LOCATION_UNKNOWN: + str.append("KEY_LOCATION_UNKNOWN"); + break; + case KEY_LOCATION_STANDARD: + str.append("KEY_LOCATION_STANDARD"); + break; + case KEY_LOCATION_LEFT: + str.append("KEY_LOCATION_LEFT"); + break; + case KEY_LOCATION_RIGHT: + str.append("KEY_LOCATION_RIGHT"); + break; + case KEY_LOCATION_NUMPAD: + str.append("KEY_LOCATION_NUMPAD"); + break; + default: + str.append("KEY_LOCATION_UNKNOWN"); + break; + } + return str.toString(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyListener.java new file mode 100644 index 00000000000..b60f32523c1 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/KeyListener.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives keyboard events. +public interface KeyListener extends EventListener { + + void keyTyped(KeyEvent e); + + void keyPressed(KeyEvent e); + + void keyReleased(KeyEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseAdapter.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseAdapter.java new file mode 100644 index 00000000000..9fa95f2280d --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseAdapter.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +/// A mouse, mouse motion and mouse wheel listener whose methods do nothing, +/// to be subclassed for the few events of interest. +public abstract class MouseAdapter implements MouseListener, MouseWheelListener, MouseMotionListener { + + public void mouseClicked(MouseEvent e) { + } + + public void mousePressed(MouseEvent e) { + } + + public void mouseReleased(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void mouseWheelMoved(MouseWheelEvent e) { + } + + public void mouseDragged(MouseEvent e) { + } + + public void mouseMoved(MouseEvent e) { + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseEvent.java new file mode 100644 index 00000000000..6fed2c49cc1 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseEvent.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Point; + +/// A mouse action in a component: press, release, click, move, drag, enter, +/// exit or wheel. +/// +/// The on-screen position is taken from the source component when it is +/// showing, and is zero otherwise, unless the constructor that passes it +/// explicitly is used. +public class MouseEvent extends InputEvent { + + public static final int MOUSE_FIRST = 500; + + public static final int MOUSE_LAST = 507; + + public static final int MOUSE_CLICKED = 500; + + public static final int MOUSE_PRESSED = 501; + + public static final int MOUSE_RELEASED = 502; + + public static final int MOUSE_MOVED = 503; + + public static final int MOUSE_ENTERED = 504; + + public static final int MOUSE_EXITED = 505; + + public static final int MOUSE_DRAGGED = 506; + + public static final int MOUSE_WHEEL = 507; + + public static final int NOBUTTON = 0; + + public static final int BUTTON1 = 1; + + public static final int BUTTON2 = 2; + + public static final int BUTTON3 = 3; + + private static final int MAX_BUTTON = 20; + + private int x; + + private int y; + + private int xAbs; + + private int yAbs; + + private final int clickCount; + + private final int button; + + private final boolean popupTrigger; + + public MouseEvent(Component source, int id, long when, int modifiers, int x, int y, int clickCount, + boolean popupTrigger, int button) { + super(source, id, when, modifiers); + checkButton(button); + this.x = x; + this.y = y; + this.clickCount = clickCount; + this.popupTrigger = popupTrigger; + this.button = button; + if (source != null && source.isShowing()) { + Point p = source.getLocationOnScreen(); + this.xAbs = p.x + x; + this.yAbs = p.y + y; + } + this.modifiers = normalize(modifiers, true); + } + + public MouseEvent(Component source, int id, long when, int modifiers, int x, int y, int clickCount, + boolean popupTrigger) { + this(source, id, when, modifiers, x, y, clickCount, popupTrigger, NOBUTTON); + } + + public MouseEvent(Component source, int id, long when, int modifiers, int x, int y, int xAbs, int yAbs, + int clickCount, boolean popupTrigger, int button) { + super(source, id, when, modifiers); + checkButton(button); + this.x = x; + this.y = y; + this.xAbs = xAbs; + this.yAbs = yAbs; + this.clickCount = clickCount; + this.popupTrigger = popupTrigger; + this.button = button; + this.modifiers = normalize(modifiers, true); + } + + private static void checkButton(int button) { + if (button < NOBUTTON || button > MAX_BUTTON) { + throw new IllegalArgumentException("Invalid button value :" + button); + } + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public Point getPoint() { + return new Point(x, y); + } + + public Point getLocationOnScreen() { + return new Point(xAbs, yAbs); + } + + public int getXOnScreen() { + return xAbs; + } + + public int getYOnScreen() { + return yAbs; + } + + public void translatePoint(int dx, int dy) { + x += dx; + y += dy; + } + + public int getClickCount() { + return clickCount; + } + + public int getButton() { + return button; + } + + public boolean isPopupTrigger() { + return popupTrigger; + } + + public static String getMouseModifiersText(int modifiers) { + StringBuilder buf = new StringBuilder(); + if ((modifiers & ALT_MASK) != 0) { + add(buf, "Alt"); + } + if ((modifiers & META_MASK) != 0) { + add(buf, "Meta"); + } + if ((modifiers & SHIFT_MASK) != 0) { + add(buf, "Shift"); + } + if ((modifiers & CTRL_MASK) != 0) { + add(buf, "Ctrl"); + } + if ((modifiers & ALT_GRAPH_MASK) != 0) { + add(buf, "Alt Graph"); + } + if ((modifiers & BUTTON1_MASK) != 0) { + add(buf, "Button1"); + } + return buf.toString(); + } + + private static void add(StringBuilder buf, String name) { + if (buf.length() > 0) { + buf.append('+'); + } + buf.append(name); + } + + @Override + public String paramString() { + StringBuilder str = new StringBuilder(80); + switch (id) { + case MOUSE_PRESSED: + str.append("MOUSE_PRESSED"); + break; + case MOUSE_RELEASED: + str.append("MOUSE_RELEASED"); + break; + case MOUSE_CLICKED: + str.append("MOUSE_CLICKED"); + break; + case MOUSE_ENTERED: + str.append("MOUSE_ENTERED"); + break; + case MOUSE_EXITED: + str.append("MOUSE_EXITED"); + break; + case MOUSE_MOVED: + str.append("MOUSE_MOVED"); + break; + case MOUSE_DRAGGED: + str.append("MOUSE_DRAGGED"); + break; + case MOUSE_WHEEL: + str.append("MOUSE_WHEEL"); + break; + default: + str.append("unknown type"); + break; + } + str.append(",(").append(x).append(',').append(y).append(')'); + str.append(",absolute(").append(xAbs).append(',').append(yAbs).append(')'); + if (id != MOUSE_DRAGGED && id != MOUSE_MOVED) { + str.append(",button=").append(getButton()); + } + if (getModifiers() != 0) { + str.append(",modifiers=").append(getMouseModifiersText(modifiers)); + } + if (getModifiersEx() != 0) { + str.append(",extModifiers=").append(getModifiersExText(modifiers)); + } + str.append(",clickCount=").append(clickCount); + return str.toString(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseListener.java new file mode 100644 index 00000000000..24f52645304 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseListener.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives the presses, releases, clicks and crossings of the mouse. +public interface MouseListener extends EventListener { + + void mouseClicked(MouseEvent e); + + void mousePressed(MouseEvent e); + + void mouseReleased(MouseEvent e); + + void mouseEntered(MouseEvent e); + + void mouseExited(MouseEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseMotionAdapter.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseMotionAdapter.java new file mode 100644 index 00000000000..9cee131a71b --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseMotionAdapter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +/// A `MouseMotionListener` whose methods do nothing, to be subclassed for the +/// few events of interest. +public abstract class MouseMotionAdapter implements MouseMotionListener { + + public void mouseDragged(MouseEvent e) { + } + + public void mouseMoved(MouseEvent e) { + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseMotionListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseMotionListener.java new file mode 100644 index 00000000000..c40795df5e3 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseMotionListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives the moves and drags of the mouse. +public interface MouseMotionListener extends EventListener { + + void mouseDragged(MouseEvent e); + + void mouseMoved(MouseEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseWheelEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseWheelEvent.java new file mode 100644 index 00000000000..5df91fb6ecc --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseWheelEvent.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.Component; + +/// The mouse wheel was rotated in a component. +public class MouseWheelEvent extends MouseEvent { + + public static final int WHEEL_UNIT_SCROLL = 0; + + public static final int WHEEL_BLOCK_SCROLL = 1; + + private final int scrollType; + + private final int scrollAmount; + + private final int wheelRotation; + + private final double preciseWheelRotation; + + public MouseWheelEvent(Component source, int id, long when, int modifiers, int x, int y, int clickCount, + boolean popupTrigger, int scrollType, int scrollAmount, int wheelRotation) { + super(source, id, when, modifiers, x, y, clickCount, popupTrigger); + this.scrollType = scrollType; + this.scrollAmount = scrollAmount; + this.wheelRotation = wheelRotation; + this.preciseWheelRotation = wheelRotation; + } + + public MouseWheelEvent(Component source, int id, long when, int modifiers, int x, int y, int xAbs, int yAbs, + int clickCount, boolean popupTrigger, int scrollType, int scrollAmount, int wheelRotation) { + super(source, id, when, modifiers, x, y, xAbs, yAbs, clickCount, popupTrigger, NOBUTTON); + this.scrollType = scrollType; + this.scrollAmount = scrollAmount; + this.wheelRotation = wheelRotation; + this.preciseWheelRotation = wheelRotation; + } + + public MouseWheelEvent(Component source, int id, long when, int modifiers, int x, int y, int xAbs, int yAbs, + int clickCount, boolean popupTrigger, int scrollType, int scrollAmount, int wheelRotation, + double preciseWheelRotation) { + super(source, id, when, modifiers, x, y, xAbs, yAbs, clickCount, popupTrigger, NOBUTTON); + this.scrollType = scrollType; + this.scrollAmount = scrollAmount; + this.wheelRotation = wheelRotation; + this.preciseWheelRotation = preciseWheelRotation; + } + + public int getScrollType() { + return scrollType; + } + + public int getScrollAmount() { + return scrollAmount; + } + + public int getWheelRotation() { + return wheelRotation; + } + + public double getPreciseWheelRotation() { + return preciseWheelRotation; + } + + public int getUnitsToScroll() { + return scrollAmount * wheelRotation; + } + + @Override + public String paramString() { + return super.paramString() + ",scrollType=" + + (scrollType == WHEEL_UNIT_SCROLL ? "WHEEL_UNIT_SCROLL" : scrollType == WHEEL_BLOCK_SCROLL + ? "WHEEL_BLOCK_SCROLL" : "unknown scroll type") + + ",scrollAmount=" + scrollAmount + ",wheelRotation=" + wheelRotation + + ",preciseWheelRotation=" + preciseWheelRotation; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseWheelListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseWheelListener.java new file mode 100644 index 00000000000..e63a6731198 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/MouseWheelListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives the rotations of the mouse wheel. +public interface MouseWheelListener extends EventListener { + + void mouseWheelMoved(MouseWheelEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/TextEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/TextEvent.java new file mode 100644 index 00000000000..abba72668c0 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/TextEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.AWTEvent; + +/// The text of a text component changed. +public class TextEvent extends AWTEvent { + + public static final int TEXT_FIRST = 900; + + public static final int TEXT_LAST = 900; + + public static final int TEXT_VALUE_CHANGED = 900; + + public TextEvent(Object source, int id) { + super(source, id); + } + + @Override + public String paramString() { + return id == TEXT_VALUE_CHANGED ? "TEXT_VALUE_CHANGED" : "unknown type"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/TextListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/TextListener.java new file mode 100644 index 00000000000..06692b6259b --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/TextListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives text events. +public interface TextListener extends EventListener { + + void textValueChanged(TextEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowAdapter.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowAdapter.java new file mode 100644 index 00000000000..1bbc6140ac8 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowAdapter.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +/// A window, window focus and window state listener whose methods do +/// nothing, to be subclassed for the few events of interest. +public abstract class WindowAdapter implements WindowListener, WindowStateListener, WindowFocusListener { + + public void windowOpened(WindowEvent e) { + } + + public void windowClosing(WindowEvent e) { + } + + public void windowClosed(WindowEvent e) { + } + + public void windowIconified(WindowEvent e) { + } + + public void windowDeiconified(WindowEvent e) { + } + + public void windowActivated(WindowEvent e) { + } + + public void windowDeactivated(WindowEvent e) { + } + + public void windowStateChanged(WindowEvent e) { + } + + public void windowGainedFocus(WindowEvent e) { + } + + public void windowLostFocus(WindowEvent e) { + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowEvent.java new file mode 100644 index 00000000000..a73b998621d --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowEvent.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import com.codename1.desktopcompat.java.awt.Window; + +/// A window was opened, closed, iconified, activated, focused or changed +/// state. +public class WindowEvent extends ComponentEvent { + + public static final int WINDOW_FIRST = 200; + + public static final int WINDOW_OPENED = 200; + + public static final int WINDOW_CLOSING = 201; + + public static final int WINDOW_CLOSED = 202; + + public static final int WINDOW_ICONIFIED = 203; + + public static final int WINDOW_DEICONIFIED = 204; + + public static final int WINDOW_ACTIVATED = 205; + + public static final int WINDOW_DEACTIVATED = 206; + + public static final int WINDOW_GAINED_FOCUS = 207; + + public static final int WINDOW_LOST_FOCUS = 208; + + public static final int WINDOW_STATE_CHANGED = 209; + + public static final int WINDOW_LAST = 209; + + private final Window opposite; + + private final int oldState; + + private final int newState; + + public WindowEvent(Window source, int id, Window opposite, int oldState, int newState) { + super(source, id); + this.opposite = opposite; + this.oldState = oldState; + this.newState = newState; + } + + public WindowEvent(Window source, int id, Window opposite) { + this(source, id, opposite, 0, 0); + } + + public WindowEvent(Window source, int id, int oldState, int newState) { + this(source, id, null, oldState, newState); + } + + public WindowEvent(Window source, int id) { + this(source, id, null, 0, 0); + } + + public Window getWindow() { + return source instanceof Window ? (Window) source : null; + } + + public Window getOppositeWindow() { + return opposite; + } + + public int getOldState() { + return oldState; + } + + public int getNewState() { + return newState; + } + + @Override + public String paramString() { + String typeStr; + switch (id) { + case WINDOW_OPENED: + typeStr = "WINDOW_OPENED"; + break; + case WINDOW_CLOSING: + typeStr = "WINDOW_CLOSING"; + break; + case WINDOW_CLOSED: + typeStr = "WINDOW_CLOSED"; + break; + case WINDOW_ICONIFIED: + typeStr = "WINDOW_ICONIFIED"; + break; + case WINDOW_DEICONIFIED: + typeStr = "WINDOW_DEICONIFIED"; + break; + case WINDOW_ACTIVATED: + typeStr = "WINDOW_ACTIVATED"; + break; + case WINDOW_DEACTIVATED: + typeStr = "WINDOW_DEACTIVATED"; + break; + case WINDOW_GAINED_FOCUS: + typeStr = "WINDOW_GAINED_FOCUS"; + break; + case WINDOW_LOST_FOCUS: + typeStr = "WINDOW_LOST_FOCUS"; + break; + case WINDOW_STATE_CHANGED: + typeStr = "WINDOW_STATE_CHANGED"; + break; + default: + typeStr = "unknown type"; + break; + } + return typeStr + ",opposite=" + opposite + ",oldState=" + oldState + ",newState=" + newState; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowFocusListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowFocusListener.java new file mode 100644 index 00000000000..3c8601b9817 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowFocusListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives the events of a window gaining or losing the focus. +public interface WindowFocusListener extends EventListener { + + void windowGainedFocus(WindowEvent e); + + void windowLostFocus(WindowEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowListener.java new file mode 100644 index 00000000000..aa2b3f412a8 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowListener.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives the events of a window being opened, closed, iconified or +/// activated. +public interface WindowListener extends EventListener { + + void windowOpened(WindowEvent e); + + void windowClosing(WindowEvent e); + + void windowClosed(WindowEvent e); + + void windowIconified(WindowEvent e); + + void windowDeiconified(WindowEvent e); + + void windowActivated(WindowEvent e); + + void windowDeactivated(WindowEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowStateListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowStateListener.java new file mode 100644 index 00000000000..f0cd97377bb --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/WindowStateListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.event; + +import java.util.EventListener; + +/// Receives the events of a window changing state. +public interface WindowStateListener extends EventListener { + + void windowStateChanged(WindowEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/package-info.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/package-info.java new file mode 100644 index 00000000000..35d5264c2c2 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/event/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The `java.awt.event` events, listeners and adapters. +/// +/// Event ids, modifier masks and virtual key codes carry the values of the +/// JDK, because applications inline them when they are compiled. Key and +/// modifier texts are English only. +package com.codename1.desktopcompat.java.awt.event; diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/AffineTransform.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/AffineTransform.java new file mode 100644 index 00000000000..bdd37b4c89c --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/AffineTransform.java @@ -0,0 +1,674 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import com.codename1.desktopcompat.java.awt.Shape; + +/// A linear mapping of the plane that keeps straight lines straight and +/// parallel lines parallel: any mix of translation, scaling, rotation and +/// shearing. +/// +/// A point `(x, y)` maps to `(m00*x + m01*y + m02, m10*x + m11*y + m12)`. +/// Rotations by an exact multiple of a quarter turn store exact zeros and +/// ones, so [#getType()] reports them as quadrant rotations. +public class AffineTransform implements Cloneable { + + public static final int TYPE_IDENTITY = 0; + public static final int TYPE_TRANSLATION = 1; + public static final int TYPE_UNIFORM_SCALE = 2; + public static final int TYPE_GENERAL_SCALE = 4; + public static final int TYPE_MASK_SCALE = TYPE_UNIFORM_SCALE | TYPE_GENERAL_SCALE; + public static final int TYPE_FLIP = 64; + public static final int TYPE_QUADRANT_ROTATION = 8; + public static final int TYPE_GENERAL_ROTATION = 16; + public static final int TYPE_MASK_ROTATION = TYPE_QUADRANT_ROTATION | TYPE_GENERAL_ROTATION; + public static final int TYPE_GENERAL_TRANSFORM = 32; + + private double m00; + private double m10; + private double m01; + private double m11; + private double m02; + private double m12; + + public AffineTransform() { + m00 = 1.0; + m11 = 1.0; + } + + public AffineTransform(AffineTransform tx) { + this(tx.m00, tx.m10, tx.m01, tx.m11, tx.m02, tx.m12); + } + + public AffineTransform(float m00, float m10, float m01, float m11, float m02, float m12) { + this((double) m00, (double) m10, (double) m01, (double) m11, (double) m02, (double) m12); + } + + /// Reads four or six values in the order m00, m10, m01, m11, m02, m12. + public AffineTransform(float[] flatmatrix) { + m00 = flatmatrix[0]; + m10 = flatmatrix[1]; + m01 = flatmatrix[2]; + m11 = flatmatrix[3]; + if (flatmatrix.length > 5) { + m02 = flatmatrix[4]; + m12 = flatmatrix[5]; + } + } + + public AffineTransform(double m00, double m10, double m01, double m11, double m02, double m12) { + this.m00 = m00; + this.m10 = m10; + this.m01 = m01; + this.m11 = m11; + this.m02 = m02; + this.m12 = m12; + } + + /// Reads four or six values in the order m00, m10, m01, m11, m02, m12. + public AffineTransform(double[] flatmatrix) { + m00 = flatmatrix[0]; + m10 = flatmatrix[1]; + m01 = flatmatrix[2]; + m11 = flatmatrix[3]; + if (flatmatrix.length > 5) { + m02 = flatmatrix[4]; + m12 = flatmatrix[5]; + } + } + + public static AffineTransform getTranslateInstance(double tx, double ty) { + AffineTransform t = new AffineTransform(); + t.setToTranslation(tx, ty); + return t; + } + + public static AffineTransform getRotateInstance(double theta) { + AffineTransform t = new AffineTransform(); + t.setToRotation(theta); + return t; + } + + public static AffineTransform getRotateInstance(double theta, double anchorx, double anchory) { + AffineTransform t = new AffineTransform(); + t.setToRotation(theta, anchorx, anchory); + return t; + } + + public static AffineTransform getRotateInstance(double vecx, double vecy) { + AffineTransform t = new AffineTransform(); + t.setToRotation(vecx, vecy); + return t; + } + + public static AffineTransform getRotateInstance(double vecx, double vecy, double anchorx, double anchory) { + AffineTransform t = new AffineTransform(); + t.setToRotation(vecx, vecy, anchorx, anchory); + return t; + } + + public static AffineTransform getQuadrantRotateInstance(int numquadrants) { + AffineTransform t = new AffineTransform(); + t.setToQuadrantRotation(numquadrants); + return t; + } + + public static AffineTransform getQuadrantRotateInstance(int numquadrants, double anchorx, double anchory) { + AffineTransform t = new AffineTransform(); + t.setToQuadrantRotation(numquadrants, anchorx, anchory); + return t; + } + + public static AffineTransform getScaleInstance(double sx, double sy) { + AffineTransform t = new AffineTransform(); + t.setToScale(sx, sy); + return t; + } + + public static AffineTransform getShearInstance(double shx, double shy) { + AffineTransform t = new AffineTransform(); + t.setToShear(shx, shy); + return t; + } + + /// Classifies the matrix as a combination of the `TYPE_` flags. The + /// answer is computed from the six values on every call. + /// + /// As in the JDK, a mirrored rotation that is not a plain flip of one + /// axis always carries a scale flag, uniform when nothing else applies, + /// and `TYPE_GENERAL_TRANSFORM` is reported on its own, without the + /// translation flag. + /// + /// One deliberate difference: a rotation whose sine and cosine squares + /// sum to one only within rounding is a pure rotation here however the + /// matrix was made. The JDK remembers how a transform was built, so it + /// says the same of one made by a rotation method but reports a uniform + /// scale for the identical values passed to a constructor. + public int getType() { + if (m00 * m01 + m10 * m11 != 0.0) { + // the images of the two axes are no longer perpendicular + return TYPE_GENERAL_TRANSFORM; + } + int type = (m02 != 0.0 || m12 != 0.0) ? TYPE_TRANSLATION : TYPE_IDENTITY; + if (m01 == 0.0 && m10 == 0.0) { + boolean xPositive = m00 >= 0.0; + if (xPositive == (m11 >= 0.0)) { + if (!xPositive) { + // both axes reversed: a half turn + type |= TYPE_QUADRANT_ROTATION; + } + type |= scaleFlags(m00 != m11, m00); + } else { + type |= TYPE_FLIP | scaleFlags(m00 != -m11, m00); + } + } else if (m00 == 0.0 && m11 == 0.0) { + type |= TYPE_QUADRANT_ROTATION; + if ((m01 >= 0.0) != (m10 >= 0.0)) { + type |= scaleFlags(m01 != -m10, m01); + } else { + type |= TYPE_FLIP | (m01 != m10 ? TYPE_GENERAL_SCALE : TYPE_UNIFORM_SCALE); + } + } else { + type |= TYPE_GENERAL_ROTATION; + if ((m00 >= 0.0) == (m11 >= 0.0)) { + if (m00 != m11 || m01 != -m10) { + type |= TYPE_GENERAL_SCALE; + } else if (Math.abs(m00 * m11 - m01 * m10 - 1.0) > ROUNDING) { + type |= TYPE_UNIFORM_SCALE; + } + } else { + type |= TYPE_FLIP | (m00 != -m11 || m01 != m10 ? TYPE_GENERAL_SCALE : TYPE_UNIFORM_SCALE); + } + } + return type; + } + + /// How far the determinant of a rotation may stray from one through + /// rounding of its sine and cosine alone. + private static final double ROUNDING = 1e-14; + + private static int scaleFlags(boolean uneven, double factor) { + if (uneven) { + return TYPE_GENERAL_SCALE; + } + return factor != 1.0 && factor != -1.0 ? TYPE_UNIFORM_SCALE : 0; + } + + public double getDeterminant() { + return m00 * m11 - m01 * m10; + } + + /// Stores m00, m10, m01, m11 and, when the array has room for six + /// values, m02 and m12. + public void getMatrix(double[] flatmatrix) { + flatmatrix[0] = m00; + flatmatrix[1] = m10; + flatmatrix[2] = m01; + flatmatrix[3] = m11; + if (flatmatrix.length > 5) { + flatmatrix[4] = m02; + flatmatrix[5] = m12; + } + } + + public double getScaleX() { + return m00; + } + + public double getScaleY() { + return m11; + } + + public double getShearX() { + return m01; + } + + public double getShearY() { + return m10; + } + + public double getTranslateX() { + return m02; + } + + public double getTranslateY() { + return m12; + } + + public void translate(double tx, double ty) { + m02 = tx * m00 + ty * m01 + m02; + m12 = tx * m10 + ty * m11 + m12; + } + + public void rotate(double theta) { + rotateBy(sine(theta), cosine(theta)); + } + + public void rotate(double theta, double anchorx, double anchory) { + translate(anchorx, anchory); + rotate(theta); + translate(-anchorx, -anchory); + } + + /// Rotates by the angle of the vector; a zero vector leaves the + /// transform unchanged. + public void rotate(double vecx, double vecy) { + double len = vectorLength(vecx, vecy); + if (len != 0.0) { + rotateBy(vectorSine(vecx, vecy, len), vectorCosine(vecx, vecy, len)); + } + } + + public void rotate(double vecx, double vecy, double anchorx, double anchory) { + translate(anchorx, anchory); + rotate(vecx, vecy); + translate(-anchorx, -anchory); + } + + public void quadrantRotate(int numquadrants) { + rotateBy(QUADRANT_SINE[numquadrants & 3], QUADRANT_SINE[(numquadrants + 1) & 3]); + } + + public void quadrantRotate(int numquadrants, double anchorx, double anchory) { + translate(anchorx, anchory); + quadrantRotate(numquadrants); + translate(-anchorx, -anchory); + } + + public void scale(double sx, double sy) { + m00 *= sx; + m10 *= sx; + m01 *= sy; + m11 *= sy; + } + + public void shear(double shx, double shy) { + double a = m00; + double b = m01; + m00 = a + b * shy; + m01 = a * shx + b; + a = m10; + b = m11; + m10 = a + b * shy; + m11 = a * shx + b; + } + + public void setToIdentity() { + setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0); + } + + public void setToTranslation(double tx, double ty) { + setTransform(1.0, 0.0, 0.0, 1.0, tx, ty); + } + + public void setToRotation(double theta) { + double sin = sine(theta); + double cos = cosine(theta); + setTransform(cos, sin, -sin, cos, 0.0, 0.0); + } + + public void setToRotation(double theta, double anchorx, double anchory) { + setToRotation(theta); + anchor(anchorx, anchory); + } + + /// Sets a rotation by the angle of the vector; a zero vector gives the + /// identity. + public void setToRotation(double vecx, double vecy) { + double len = vectorLength(vecx, vecy); + if (len == 0.0) { + setToIdentity(); + } else { + double sin = vectorSine(vecx, vecy, len); + double cos = vectorCosine(vecx, vecy, len); + setTransform(cos, sin, -sin, cos, 0.0, 0.0); + } + } + + public void setToRotation(double vecx, double vecy, double anchorx, double anchory) { + setToRotation(vecx, vecy); + anchor(anchorx, anchory); + } + + public void setToQuadrantRotation(int numquadrants) { + double sin = QUADRANT_SINE[numquadrants & 3]; + double cos = QUADRANT_SINE[(numquadrants + 1) & 3]; + setTransform(cos, sin, -sin, cos, 0.0, 0.0); + } + + public void setToQuadrantRotation(int numquadrants, double anchorx, double anchory) { + setToQuadrantRotation(numquadrants); + anchor(anchorx, anchory); + } + + public void setToScale(double sx, double sy) { + setTransform(sx, 0.0, 0.0, sy, 0.0, 0.0); + } + + public void setToShear(double shx, double shy) { + setTransform(1.0, shy, shx, 1.0, 0.0, 0.0); + } + + public void setTransform(AffineTransform tx) { + setTransform(tx.m00, tx.m10, tx.m01, tx.m11, tx.m02, tx.m12); + } + + public void setTransform(double m00, double m10, double m01, double m11, double m02, double m12) { + this.m00 = m00; + this.m10 = m10; + this.m01 = m01; + this.m11 = m11; + this.m02 = m02; + this.m12 = m12; + } + + /// Makes this transform apply `tx` first and then what it did before. + public void concatenate(AffineTransform tx) { + setTransform( + m00 * tx.m00 + m01 * tx.m10, + m10 * tx.m00 + m11 * tx.m10, + m00 * tx.m01 + m01 * tx.m11, + m10 * tx.m01 + m11 * tx.m11, + m00 * tx.m02 + m01 * tx.m12 + m02, + m10 * tx.m02 + m11 * tx.m12 + m12); + } + + /// Makes this transform do what it did before and then apply `tx`. + public void preConcatenate(AffineTransform tx) { + setTransform( + tx.m00 * m00 + tx.m01 * m10, + tx.m10 * m00 + tx.m11 * m10, + tx.m00 * m01 + tx.m01 * m11, + tx.m10 * m01 + tx.m11 * m11, + tx.m00 * m02 + tx.m01 * m12 + tx.m02, + tx.m10 * m02 + tx.m11 * m12 + tx.m12); + } + + public AffineTransform createInverse() throws NoninvertibleTransformException { + AffineTransform t = new AffineTransform(this); + t.invert(); + return t; + } + + public void invert() throws NoninvertibleTransformException { + double det = invertibleDeterminant(); + setTransform( + m11 / det, + -m10 / det, + -m01 / det, + m00 / det, + (m01 * m12 - m11 * m02) / det, + (m10 * m02 - m00 * m12) / det); + } + + private double invertibleDeterminant() throws NoninvertibleTransformException { + double det = getDeterminant(); + if (det == 0.0 || Double.isNaN(det)) { + throw new NoninvertibleTransformException("Determinant is " + det); + } + return det; + } + + /// Maps a point. When `ptDst` is null a new point is created, double + /// precision if the source is and single precision otherwise. + public Point2D transform(Point2D ptSrc, Point2D ptDst) { + if (ptDst == null) { + ptDst = newPointLike(ptSrc); + } + double x = ptSrc.getX(); + double y = ptSrc.getY(); + ptDst.setLocation(x * m00 + y * m01 + m02, x * m10 + y * m11 + m12); + return ptDst; + } + + private static Point2D newPointLike(Point2D p) { + if (p instanceof Point2D.Double) { + return new Point2D.Double(); + } + return new Point2D.Float(); + } + + public void transform(Point2D[] ptSrc, int srcOff, Point2D[] ptDst, int dstOff, int numPts) { + for (int i = 0; i < numPts; i++) { + Point2D src = ptSrc[srcOff + i]; + ptDst[dstOff + i] = transform(src, ptDst[dstOff + i]); + } + } + + public void transform(float[] srcPts, int srcOff, float[] dstPts, int dstOff, int numPts) { + if (dstPts == srcPts && dstOff > srcOff && dstOff < srcOff + numPts * 2) { + System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2); + srcOff = dstOff; + } + for (int i = 0; i < numPts; i++) { + double x = srcPts[srcOff++]; + double y = srcPts[srcOff++]; + dstPts[dstOff++] = (float) (x * m00 + y * m01 + m02); + dstPts[dstOff++] = (float) (x * m10 + y * m11 + m12); + } + } + + public void transform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) { + if (dstPts == srcPts && dstOff > srcOff && dstOff < srcOff + numPts * 2) { + System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2); + srcOff = dstOff; + } + for (int i = 0; i < numPts; i++) { + double x = srcPts[srcOff++]; + double y = srcPts[srcOff++]; + dstPts[dstOff++] = x * m00 + y * m01 + m02; + dstPts[dstOff++] = x * m10 + y * m11 + m12; + } + } + + public void transform(float[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) { + for (int i = 0; i < numPts; i++) { + double x = srcPts[srcOff++]; + double y = srcPts[srcOff++]; + dstPts[dstOff++] = x * m00 + y * m01 + m02; + dstPts[dstOff++] = x * m10 + y * m11 + m12; + } + } + + public void transform(double[] srcPts, int srcOff, float[] dstPts, int dstOff, int numPts) { + for (int i = 0; i < numPts; i++) { + double x = srcPts[srcOff++]; + double y = srcPts[srcOff++]; + dstPts[dstOff++] = (float) (x * m00 + y * m01 + m02); + dstPts[dstOff++] = (float) (x * m10 + y * m11 + m12); + } + } + + public Point2D inverseTransform(Point2D ptSrc, Point2D ptDst) throws NoninvertibleTransformException { + double det = invertibleDeterminant(); + if (ptDst == null) { + ptDst = newPointLike(ptSrc); + } + double x = ptSrc.getX() - m02; + double y = ptSrc.getY() - m12; + ptDst.setLocation((x * m11 - y * m01) / det, (y * m00 - x * m10) / det); + return ptDst; + } + + public void inverseTransform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) + throws NoninvertibleTransformException { + double det = invertibleDeterminant(); + if (dstPts == srcPts && dstOff > srcOff && dstOff < srcOff + numPts * 2) { + System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2); + srcOff = dstOff; + } + for (int i = 0; i < numPts; i++) { + double x = srcPts[srcOff++] - m02; + double y = srcPts[srcOff++] - m12; + dstPts[dstOff++] = (x * m11 - y * m01) / det; + dstPts[dstOff++] = (y * m00 - x * m10) / det; + } + } + + /// Maps a vector: like [#transform(Point2D, Point2D)] without the + /// translation. + public Point2D deltaTransform(Point2D ptSrc, Point2D ptDst) { + if (ptDst == null) { + ptDst = newPointLike(ptSrc); + } + double x = ptSrc.getX(); + double y = ptSrc.getY(); + ptDst.setLocation(x * m00 + y * m01, x * m10 + y * m11); + return ptDst; + } + + public void deltaTransform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) { + if (dstPts == srcPts && dstOff > srcOff && dstOff < srcOff + numPts * 2) { + System.arraycopy(srcPts, srcOff, dstPts, dstOff, numPts * 2); + srcOff = dstOff; + } + for (int i = 0; i < numPts; i++) { + double x = srcPts[srcOff++]; + double y = srcPts[srcOff++]; + dstPts[dstOff++] = x * m00 + y * m01; + dstPts[dstOff++] = x * m10 + y * m11; + } + } + + /// Returns the transformed outline of a shape as a double precision + /// path, or null for a null shape. + public Shape createTransformedShape(Shape pSrc) { + if (pSrc == null) { + return null; + } + return new Path2D.Double(pSrc, this); + } + + @Override + public String toString() { + return "AffineTransform[[" + m00 + ", " + m01 + ", " + m02 + "], [" + + m10 + ", " + m11 + ", " + m12 + "]]"; + } + + public boolean isIdentity() { + return m00 == 1.0 && m11 == 1.0 && m01 == 0.0 && m10 == 0.0 && m02 == 0.0 && m12 == 0.0; + } + + @Override + public Object clone() { + return new AffineTransform(this); + } + + @Override + public int hashCode() { + long bits = Double.doubleToLongBits(m00); + bits = bits * 31 + Double.doubleToLongBits(m01); + bits = bits * 31 + Double.doubleToLongBits(m02); + bits = bits * 31 + Double.doubleToLongBits(m10); + bits = bits * 31 + Double.doubleToLongBits(m11); + bits = bits * 31 + Double.doubleToLongBits(m12); + return (int) bits ^ (int) (bits >> 32); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AffineTransform)) { + return false; + } + AffineTransform a = (AffineTransform) obj; + return m00 == a.m00 && m01 == a.m01 && m02 == a.m02 && m10 == a.m10 && m11 == a.m11 && m12 == a.m12; + } + + /// Sines of 0, 1, 2 and 3 quarter turns; the cosine of n quarter turns is + /// the sine of n + 1. + private static final double[] QUADRANT_SINE = {0.0, 1.0, 0.0, -1.0}; + + /// The sine of an angle, exactly zero when the angle is a half turn as + /// closely as a double can say. + private static double sine(double theta) { + double sin = Math.sin(theta); + if (sin != 1.0 && sin != -1.0) { + double cos = Math.cos(theta); + if (cos == 1.0 || cos == -1.0) { + return 0.0; + } + } + return sin; + } + + /// The cosine of an angle, exactly zero when its sine is exactly one. + private static double cosine(double theta) { + double sin = Math.sin(theta); + if (sin == 1.0 || sin == -1.0) { + return 0.0; + } + return Math.cos(theta); + } + + /// The length of a direction vector, or zero for a vector that names no + /// direction. An axis aligned vector has length one whatever its size, so + /// the sine and cosine derived from it are exact. + private static double vectorLength(double vecx, double vecy) { + if (vecy == 0.0) { + return vecx < 0.0 ? 1.0 : (vecx > 0.0 ? 1.0 : 0.0); + } + if (vecx == 0.0) { + return 1.0; + } + return Math.sqrt(vecx * vecx + vecy * vecy); + } + + private static double vectorSine(double vecx, double vecy, double len) { + if (vecy == 0.0) { + return 0.0; + } + if (vecx == 0.0) { + return vecy > 0.0 ? 1.0 : -1.0; + } + return vecy / len; + } + + private static double vectorCosine(double vecx, double vecy, double len) { + if (vecx == 0.0) { + return 0.0; + } + if (vecy == 0.0) { + return vecx > 0.0 ? 1.0 : -1.0; + } + return vecx / len; + } + + /// Concatenates a rotation given by its sine and cosine. + private void rotateBy(double sin, double cos) { + double a = m00; + double b = m01; + m00 = cos * a + sin * b; + m01 = -sin * a + cos * b; + a = m10; + b = m11; + m10 = cos * a + sin * b; + m11 = -sin * a + cos * b; + } + + /// Moves the fixed point of the current rotation from the origin to the + /// given anchor. + private void anchor(double x, double y) { + m02 = x - (m00 * x + m01 * y); + m12 = y - (m10 * x + m11 * y); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Arc2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Arc2D.java new file mode 100644 index 00000000000..d65a9b214c7 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Arc2D.java @@ -0,0 +1,588 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import com.codename1.util.MathUtil; + +/// Part of the ellipse inscribed in a rectangular frame, from a start angle +/// through an extent, both in degrees. +/// +/// Angles are measured from the positive x axis and grow towards the +/// negative y axis, which is counter-clockwise on a screen. They are angles +/// of the frame's parameter, so 45 degrees always points at the frame's +/// corner however far the frame is from square. The arc is left open, closed +/// with a chord, or closed through the centre as a pie slice. +/// +/// `setArcByTangent` is not provided. +public abstract class Arc2D extends RectangularShape { + + /// No closing segment; hit tests treat the arc as if it had a chord. + public static final int OPEN = 0; + + /// Closed by a straight line between the ends of the arc. + public static final int CHORD = 1; + + /// Closed by lines from the ends of the arc to the centre. + public static final int PIE = 2; + + /// An arc stored in single precision. + public static class Float extends Arc2D { + + public float x; + public float y; + public float width; + public float height; + public float start; + public float extent; + + public Float() { + super(OPEN); + } + + public Float(int type) { + super(type); + } + + public Float(float x, float y, float w, float h, float start, float extent, int type) { + super(type); + this.x = x; + this.y = y; + this.width = w; + this.height = h; + this.start = start; + this.extent = extent; + } + + public Float(Rectangle2D ellipseBounds, float start, float extent, int type) { + super(type); + this.x = (float) ellipseBounds.getX(); + this.y = (float) ellipseBounds.getY(); + this.width = (float) ellipseBounds.getWidth(); + this.height = (float) ellipseBounds.getHeight(); + this.start = start; + this.extent = extent; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public double getAngleStart() { + return start; + } + + @Override + public double getAngleExtent() { + return extent; + } + + @Override + public boolean isEmpty() { + return width <= 0.0f || height <= 0.0f; + } + + @Override + public void setArc(double x, double y, double w, double h, double angSt, double angExt, int closure) { + setArcType(closure); + this.x = (float) x; + this.y = (float) y; + this.width = (float) w; + this.height = (float) h; + this.start = (float) angSt; + this.extent = (float) angExt; + } + + @Override + public void setAngleStart(double angSt) { + this.start = (float) angSt; + } + + @Override + public void setAngleExtent(double angExt) { + this.extent = (float) angExt; + } + + @Override + protected Rectangle2D makeBounds(double x, double y, double w, double h) { + return new Rectangle2D.Float((float) x, (float) y, (float) w, (float) h); + } + + @Override + public Object clone() { + return new Float(x, y, width, height, start, extent, getArcType()); + } + } + + /// An arc stored in double precision. + public static class Double extends Arc2D { + + public double x; + public double y; + public double width; + public double height; + public double start; + public double extent; + + public Double() { + super(OPEN); + } + + public Double(int type) { + super(type); + } + + public Double(double x, double y, double w, double h, double start, double extent, int type) { + super(type); + this.x = x; + this.y = y; + this.width = w; + this.height = h; + this.start = start; + this.extent = extent; + } + + public Double(Rectangle2D ellipseBounds, double start, double extent, int type) { + super(type); + this.x = ellipseBounds.getX(); + this.y = ellipseBounds.getY(); + this.width = ellipseBounds.getWidth(); + this.height = ellipseBounds.getHeight(); + this.start = start; + this.extent = extent; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public double getAngleStart() { + return start; + } + + @Override + public double getAngleExtent() { + return extent; + } + + @Override + public boolean isEmpty() { + return width <= 0.0 || height <= 0.0; + } + + @Override + public void setArc(double x, double y, double w, double h, double angSt, double angExt, int closure) { + setArcType(closure); + this.x = x; + this.y = y; + this.width = w; + this.height = h; + this.start = angSt; + this.extent = angExt; + } + + @Override + public void setAngleStart(double angSt) { + this.start = angSt; + } + + @Override + public void setAngleExtent(double angExt) { + this.extent = angExt; + } + + @Override + protected Rectangle2D makeBounds(double x, double y, double w, double h) { + return new Rectangle2D.Double(x, y, w, h); + } + + @Override + public Object clone() { + return new Double(x, y, width, height, start, extent, getArcType()); + } + } + + private int type; + + protected Arc2D() { + this(OPEN); + } + + protected Arc2D(int type) { + this.type = checkType(type); + } + + private static int checkType(int type) { + if (type < OPEN || type > PIE) { + throw new IllegalArgumentException("invalid type for Arc: " + type); + } + return type; + } + + public abstract double getAngleStart(); + + public abstract double getAngleExtent(); + + public int getArcType() { + return type; + } + + public Point2D getStartPoint() { + return pointAt(getAngleStart()); + } + + public Point2D getEndPoint() { + return pointAt(getAngleStart() + getAngleExtent()); + } + + private Point2D pointAt(double degrees) { + double a = Math.toRadians(-degrees); + return new Point2D.Double(getX() + (Math.cos(a) * 0.5 + 0.5) * getWidth(), + getY() + (Math.sin(a) * 0.5 + 0.5) * getHeight()); + } + + public abstract void setArc(double x, double y, double w, double h, double angSt, double angExt, int closure); + + public void setArc(Point2D loc, Dimension2D size, double angSt, double angExt, int closure) { + setArc(loc.getX(), loc.getY(), size.getWidth(), size.getHeight(), angSt, angExt, closure); + } + + public void setArc(Rectangle2D rect, double angSt, double angExt, int closure) { + setArc(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight(), angSt, angExt, closure); + } + + public void setArc(Arc2D a) { + setArc(a.getX(), a.getY(), a.getWidth(), a.getHeight(), a.getAngleStart(), a.getAngleExtent(), a.type); + } + + public void setArcByCenter(double x, double y, double radius, double angSt, double angExt, int closure) { + setArc(x - radius, y - radius, radius * 2.0, radius * 2.0, angSt, angExt, closure); + } + + public abstract void setAngleStart(double angSt); + + public abstract void setAngleExtent(double angExt); + + /// Starts the arc where the ray from the centre through the point + /// leaves the ellipse. + public void setAngleStart(Point2D p) { + setAngleStart(Math.toDegrees(angleTo(p.getX(), p.getY()))); + } + + /// Runs the arc counter-clockwise from the direction of the first point + /// to the direction of the second. + public void setAngles(double x1, double y1, double x2, double y2) { + double a1 = angleTo(x1, y1); + double sweep = angleTo(x2, y2) - a1; + if (sweep <= 0.0) { + sweep += Math.PI * 2.0; + } + setAngleStart(Math.toDegrees(a1)); + setAngleExtent(Math.toDegrees(sweep)); + } + + public void setAngles(Point2D p1, Point2D p2) { + setAngles(p1.getX(), p1.getY(), p2.getX(), p2.getY()); + } + + /// The arc angle, in radians, at which the ray from the centre through + /// a point crosses the ellipse. + private double angleTo(double px, double py) { + return MathUtil.atan2(getWidth() * (getCenterY() - py), getHeight() * (px - getCenterX())); + } + + public void setArcType(int type) { + this.type = checkType(type); + } + + /// Moves and resizes the frame, keeping the angles and the closure. + @Override + public void setFrame(double x, double y, double w, double h) { + setArc(x, y, w, h, getAngleStart(), getAngleExtent(), type); + } + + /// The tight bounds of the arc and its closing lines, not the frame of + /// the whole ellipse. + @Override + public Rectangle2D getBounds2D() { + if (isEmpty()) { + return makeBounds(getX(), getY(), getWidth(), getHeight()); + } + // in the frame where the ellipse is the unit circle about the origin + double a0 = Math.toRadians(-getAngleStart()); + double a1 = Math.toRadians(-getAngleStart() - getAngleExtent()); + double[] xs = {Math.cos(a0), Math.cos(a1), 0.0, 1.0, 0.0, -1.0, 0.0}; + double[] ys = {Math.sin(a0), Math.sin(a1), 0.0, 0.0, -1.0, 0.0, 1.0}; + double x1 = Math.min(xs[0], xs[1]); + double x2 = Math.max(xs[0], xs[1]); + double y1 = Math.min(ys[0], ys[1]); + double y2 = Math.max(ys[0], ys[1]); + for (int i = 2; i < 7; i++) { + // the centre counts for a pie, an axis end when the arc passes it + if (i == 2 ? type == PIE : containsAngle((i - 3) * 90.0)) { + x1 = Math.min(x1, xs[i]); + x2 = Math.max(x2, xs[i]); + y1 = Math.min(y1, ys[i]); + y2 = Math.max(y2, ys[i]); + } + } + double w = getWidth() / 2.0; + double h = getHeight() / 2.0; + return makeBounds(getX() + w + x1 * w, getY() + h + y1 * h, (x2 - x1) * w, (y2 - y1) * h); + } + + protected abstract Rectangle2D makeBounds(double x, double y, double w, double h); + + /// Brings an angle into the range above -180 up to and including 180. + private static double normalize(double degrees) { + if (degrees > 180.0) { + if (degrees <= 540.0) { + return degrees - 360.0; + } + degrees = degrees - Math.floor(degrees / 360.0) * 360.0; + return degrees > 180.0 ? degrees - 360.0 : degrees; + } + if (degrees <= -180.0) { + if (degrees > -540.0) { + return degrees + 360.0; + } + degrees = degrees - Math.floor(degrees / 360.0) * 360.0; + return degrees > 180.0 ? degrees - 360.0 : degrees; + } + return degrees; + } + + /// Whether an angle in degrees falls within the arc: at or after the + /// start in the arc's direction and before its end. + public boolean containsAngle(double angle) { + double extent = getAngleExtent(); + boolean backwards = extent < 0.0; + if (backwards) { + extent = -extent; + } + if (extent >= 360.0) { + return true; + } + angle = normalize(angle) - normalize(getAngleStart()); + if (backwards) { + angle = -angle; + } + if (angle < 0.0) { + angle += 360.0; + } + return angle >= 0.0 && angle < extent; + } + + @Override + public boolean contains(double x, double y) { + double w = getWidth(); + double h = getHeight(); + if (w <= 0.0 || h <= 0.0) { + return false; + } + // in the frame where the ellipse is the circle of diameter one + // about the origin + double nx = (x - getX()) / w - 0.5; + double ny = (y - getY()) / h - 0.5; + if (nx * nx + ny * ny >= 0.25) { + return false; + } + double extent = Math.abs(getAngleExtent()); + if (extent >= 360.0) { + return true; + } + boolean inSector = containsAngle(-Math.toDegrees(MathUtil.atan2(ny, nx))); + if (type == PIE) { + return inSector; + } + // closed by the chord: the sector with the centre triangle removed + // when the arc is under a half turn, added when it is over + if (inSector) { + if (extent >= 180.0) { + return true; + } + } else if (extent <= 180.0) { + return false; + } + double a0 = Math.toRadians(-getAngleStart()); + double a1 = Math.toRadians(-getAngleStart() - getAngleExtent()); + double sx = Math.cos(a0); + double sy = Math.sin(a0); + double ex = Math.cos(a1); + double ey = Math.sin(a1); + boolean centreSide = Line2D.relativeCCW(sx, sy, ex, ey, nx * 2.0, ny * 2.0) + * Line2D.relativeCCW(sx, sy, ex, ey, 0.0, 0.0) >= 0; + return inSector ? !centreSide : centreSide; + } + + /// Tested over the flattened outline unless the arc is a whole ellipse. + @Override + public boolean intersects(double x, double y, double w, double h) { + if (isEmpty() || w <= 0.0 || h <= 0.0) { + return false; + } + if (Math.abs(getAngleExtent()) >= 360.0) { + return new Ellipse2D.Double(getX(), getY(), getWidth(), getHeight()).intersects(x, y, w, h); + } + return Outline.of(this).intersects(x, y, w, h, PathIterator.WIND_NON_ZERO); + } + + @Override + public boolean contains(double x, double y, double w, double h) { + if (!(contains(x, y) && contains(x + w, y) && contains(x, y + h) && contains(x + w, y + h))) { + return false; + } + // every closure is convex except a pie of more than a half turn, + // whose notch a rectangle can straddle with all four corners inside + if (type != PIE || Math.abs(getAngleExtent()) <= 180.0) { + return true; + } + Rectangle2D r = new Rectangle2D.Double(x, y, w, h); + double cx = getCenterX(); + double cy = getCenterY(); + Point2D s = getStartPoint(); + Point2D e = getEndPoint(); + return !r.intersectsLine(cx, cy, s.getX(), s.getY()) && !r.intersectsLine(cx, cy, e.getX(), e.getY()); + } + + @Override + public boolean contains(Rectangle2D r) { + return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + /// The arc as at most four cubic Bezier curves, each spanning no more + /// than a quarter turn, followed by the closing lines of its type. A + /// frame with a negative size has no outline at all. + @Override + public PathIterator getPathIterator(AffineTransform at) { + SegmentIterator it = new SegmentIterator(PathIterator.WIND_NON_ZERO, at, 7); + double w = getWidth() / 2.0; + double h = getHeight() / 2.0; + if (w < 0.0 || h < 0.0) { + return it; + } + double cx = getX() + w; + double cy = getY() + h; + // device angles: y grows downwards, so the sign flips + double start = -Math.toRadians(getAngleStart()); + double sweep = -getAngleExtent(); + int pieces; + double step; + if (sweep >= 360.0 || sweep <= -360.0) { + pieces = 4; + step = sweep < 0.0 ? -Math.PI / 2.0 : Math.PI / 2.0; + } else { + pieces = (int) Math.ceil(Math.abs(sweep) / 90.0); + step = pieces == 0 ? 0.0 : Math.toRadians(sweep / pieces); + } + // control points sit on the tangents at each end, this far along + double k = 4.0 / 3.0 * Math.tan(step / 4.0); + double cos = Math.cos(start); + double sin = Math.sin(start); + it.move(cx + cos * w, cy + sin * h); + for (int i = 1; i <= pieces; i++) { + double a = start + step * i; + double cos2 = Math.cos(a); + double sin2 = Math.sin(a); + it.cubic(cx + (cos - k * sin) * w, cy + (sin + k * cos) * h, + cx + (cos2 + k * sin2) * w, cy + (sin2 - k * cos2) * h, + cx + cos2 * w, cy + sin2 * h); + cos = cos2; + sin = sin2; + } + if (type == PIE) { + it.line(cx, cy); + } + if (type != OPEN) { + it.close(); + } + return it; + } + + @Override + public int hashCode() { + long bits = java.lang.Double.doubleToLongBits(getX()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getY()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getWidth()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getHeight()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getAngleStart()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getAngleExtent()); + bits = bits * 31 + type; + return (int) bits ^ (int) (bits >> 32); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Arc2D) { + Arc2D a = (Arc2D) obj; + return getX() == a.getX() && getY() == a.getY() + && getWidth() == a.getWidth() && getHeight() == a.getHeight() + && getAngleStart() == a.getAngleStart() && getAngleExtent() == a.getAngleExtent() + && type == a.type; + } + return false; + } + + /// Returns a copy of this arc. A subclass from elsewhere that does not + /// override it gets a double precision arc back. + @Override + public Object clone() { + return new Double(getX(), getY(), getWidth(), getHeight(), getAngleStart(), getAngleExtent(), type); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/CubicCurve2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/CubicCurve2D.java new file mode 100644 index 00000000000..61e62b0ae82 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/CubicCurve2D.java @@ -0,0 +1,477 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import com.codename1.desktopcompat.java.awt.Rectangle; +import com.codename1.desktopcompat.java.awt.Shape; + +/// A cubic Bezier curve: two end points and two control points. +/// +/// The area tested by `contains` and `intersects` is the region between the +/// curve and the straight line joining its ends, and is evaluated over the +/// flattened curve. `getBounds2D` answers the bounds of the four defining +/// points, which enclose the curve without being tight. `solveCubic` is not +/// provided. +public abstract class CubicCurve2D implements Shape, Cloneable { + + /// A curve stored in single precision. + public static class Float extends CubicCurve2D { + + public float x1; + public float y1; + public float ctrlx1; + public float ctrly1; + public float ctrlx2; + public float ctrly2; + public float x2; + public float y2; + + public Float() { + } + + public Float(float x1, float y1, float ctrlx1, float ctrly1, + float ctrlx2, float ctrly2, float x2, float y2) { + this.x1 = x1; + this.y1 = y1; + this.ctrlx1 = ctrlx1; + this.ctrly1 = ctrly1; + this.ctrlx2 = ctrlx2; + this.ctrly2 = ctrly2; + this.x2 = x2; + this.y2 = y2; + } + + @Override + public double getX1() { + return x1; + } + + @Override + public double getY1() { + return y1; + } + + @Override + public Point2D getP1() { + return new Point2D.Float(x1, y1); + } + + @Override + public double getCtrlX1() { + return ctrlx1; + } + + @Override + public double getCtrlY1() { + return ctrly1; + } + + @Override + public Point2D getCtrlP1() { + return new Point2D.Float(ctrlx1, ctrly1); + } + + @Override + public double getCtrlX2() { + return ctrlx2; + } + + @Override + public double getCtrlY2() { + return ctrly2; + } + + @Override + public Point2D getCtrlP2() { + return new Point2D.Float(ctrlx2, ctrly2); + } + + @Override + public double getX2() { + return x2; + } + + @Override + public double getY2() { + return y2; + } + + @Override + public Point2D getP2() { + return new Point2D.Float(x2, y2); + } + + @Override + public void setCurve(double x1, double y1, double ctrlx1, double ctrly1, + double ctrlx2, double ctrly2, double x2, double y2) { + this.x1 = (float) x1; + this.y1 = (float) y1; + this.ctrlx1 = (float) ctrlx1; + this.ctrly1 = (float) ctrly1; + this.ctrlx2 = (float) ctrlx2; + this.ctrly2 = (float) ctrly2; + this.x2 = (float) x2; + this.y2 = (float) y2; + } + + public void setCurve(float x1, float y1, float ctrlx1, float ctrly1, + float ctrlx2, float ctrly2, float x2, float y2) { + this.x1 = x1; + this.y1 = y1; + this.ctrlx1 = ctrlx1; + this.ctrly1 = ctrly1; + this.ctrlx2 = ctrlx2; + this.ctrly2 = ctrly2; + this.x2 = x2; + this.y2 = y2; + } + + @Override + public Rectangle2D getBounds2D() { + float left = Math.min(Math.min(x1, x2), Math.min(ctrlx1, ctrlx2)); + float top = Math.min(Math.min(y1, y2), Math.min(ctrly1, ctrly2)); + float right = Math.max(Math.max(x1, x2), Math.max(ctrlx1, ctrlx2)); + float bottom = Math.max(Math.max(y1, y2), Math.max(ctrly1, ctrly2)); + return new Rectangle2D.Float(left, top, right - left, bottom - top); + } + + @Override + public Object clone() { + return new Float(x1, y1, ctrlx1, ctrly1, ctrlx2, ctrly2, x2, y2); + } + } + + /// A curve stored in double precision. + public static class Double extends CubicCurve2D { + + public double x1; + public double y1; + public double ctrlx1; + public double ctrly1; + public double ctrlx2; + public double ctrly2; + public double x2; + public double y2; + + public Double() { + } + + public Double(double x1, double y1, double ctrlx1, double ctrly1, + double ctrlx2, double ctrly2, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + this.ctrlx1 = ctrlx1; + this.ctrly1 = ctrly1; + this.ctrlx2 = ctrlx2; + this.ctrly2 = ctrly2; + this.x2 = x2; + this.y2 = y2; + } + + @Override + public double getX1() { + return x1; + } + + @Override + public double getY1() { + return y1; + } + + @Override + public Point2D getP1() { + return new Point2D.Double(x1, y1); + } + + @Override + public double getCtrlX1() { + return ctrlx1; + } + + @Override + public double getCtrlY1() { + return ctrly1; + } + + @Override + public Point2D getCtrlP1() { + return new Point2D.Double(ctrlx1, ctrly1); + } + + @Override + public double getCtrlX2() { + return ctrlx2; + } + + @Override + public double getCtrlY2() { + return ctrly2; + } + + @Override + public Point2D getCtrlP2() { + return new Point2D.Double(ctrlx2, ctrly2); + } + + @Override + public double getX2() { + return x2; + } + + @Override + public double getY2() { + return y2; + } + + @Override + public Point2D getP2() { + return new Point2D.Double(x2, y2); + } + + @Override + public void setCurve(double x1, double y1, double ctrlx1, double ctrly1, + double ctrlx2, double ctrly2, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + this.ctrlx1 = ctrlx1; + this.ctrly1 = ctrly1; + this.ctrlx2 = ctrlx2; + this.ctrly2 = ctrly2; + this.x2 = x2; + this.y2 = y2; + } + + @Override + public Rectangle2D getBounds2D() { + double left = Math.min(Math.min(x1, x2), Math.min(ctrlx1, ctrlx2)); + double top = Math.min(Math.min(y1, y2), Math.min(ctrly1, ctrly2)); + double right = Math.max(Math.max(x1, x2), Math.max(ctrlx1, ctrlx2)); + double bottom = Math.max(Math.max(y1, y2), Math.max(ctrly1, ctrly2)); + return new Rectangle2D.Double(left, top, right - left, bottom - top); + } + + @Override + public Object clone() { + return new Double(x1, y1, ctrlx1, ctrly1, ctrlx2, ctrly2, x2, y2); + } + } + + protected CubicCurve2D() { + } + + public abstract double getX1(); + + public abstract double getY1(); + + public abstract Point2D getP1(); + + public abstract double getCtrlX1(); + + public abstract double getCtrlY1(); + + public abstract Point2D getCtrlP1(); + + public abstract double getCtrlX2(); + + public abstract double getCtrlY2(); + + public abstract Point2D getCtrlP2(); + + public abstract double getX2(); + + public abstract double getY2(); + + public abstract Point2D getP2(); + + public abstract void setCurve(double x1, double y1, double ctrlx1, double ctrly1, + double ctrlx2, double ctrly2, double x2, double y2); + + public void setCurve(double[] coords, int offset) { + setCurve(coords[offset], coords[offset + 1], coords[offset + 2], coords[offset + 3], + coords[offset + 4], coords[offset + 5], coords[offset + 6], coords[offset + 7]); + } + + public void setCurve(Point2D p1, Point2D cp1, Point2D cp2, Point2D p2) { + setCurve(p1.getX(), p1.getY(), cp1.getX(), cp1.getY(), cp2.getX(), cp2.getY(), p2.getX(), p2.getY()); + } + + public void setCurve(Point2D[] pts, int offset) { + setCurve(pts[offset].getX(), pts[offset].getY(), pts[offset + 1].getX(), pts[offset + 1].getY(), + pts[offset + 2].getX(), pts[offset + 2].getY(), pts[offset + 3].getX(), pts[offset + 3].getY()); + } + + public void setCurve(CubicCurve2D c) { + setCurve(c.getX1(), c.getY1(), c.getCtrlX1(), c.getCtrlY1(), + c.getCtrlX2(), c.getCtrlY2(), c.getX2(), c.getY2()); + } + + /// The square of the larger distance of the two control points from the + /// segment joining the end points. + public static double getFlatnessSq(double x1, double y1, double ctrlx1, double ctrly1, + double ctrlx2, double ctrly2, double x2, double y2) { + return Math.max(Line2D.ptSegDistSq(x1, y1, x2, y2, ctrlx1, ctrly1), + Line2D.ptSegDistSq(x1, y1, x2, y2, ctrlx2, ctrly2)); + } + + public static double getFlatness(double x1, double y1, double ctrlx1, double ctrly1, + double ctrlx2, double ctrly2, double x2, double y2) { + return Math.sqrt(getFlatnessSq(x1, y1, ctrlx1, ctrly1, ctrlx2, ctrly2, x2, y2)); + } + + public static double getFlatnessSq(double[] coords, int offset) { + return getFlatnessSq(coords[offset], coords[offset + 1], coords[offset + 2], coords[offset + 3], + coords[offset + 4], coords[offset + 5], coords[offset + 6], coords[offset + 7]); + } + + public static double getFlatness(double[] coords, int offset) { + return Math.sqrt(getFlatnessSq(coords, offset)); + } + + public double getFlatnessSq() { + return getFlatnessSq(getX1(), getY1(), getCtrlX1(), getCtrlY1(), getCtrlX2(), getCtrlY2(), getX2(), getY2()); + } + + public double getFlatness() { + return Math.sqrt(getFlatnessSq()); + } + + /// Splits this curve at its middle into two curves; either may be null + /// or this curve itself. + public void subdivide(CubicCurve2D left, CubicCurve2D right) { + subdivide(this, left, right); + } + + public static void subdivide(CubicCurve2D src, CubicCurve2D left, CubicCurve2D right) { + double[] c = {src.getX1(), src.getY1(), src.getCtrlX1(), src.getCtrlY1(), + src.getCtrlX2(), src.getCtrlY2(), src.getX2(), src.getY2()}; + double[] l = new double[8]; + double[] r = new double[8]; + subdivide(c, 0, l, 0, r, 0); + if (left != null) { + left.setCurve(l, 0); + } + if (right != null) { + right.setCurve(r, 0); + } + } + + /// Splits eight coordinates into two curves of eight; a destination may + /// be null, and may be the source array where the ranges allow it. + public static void subdivide(double[] src, int srcoff, double[] left, int leftoff, double[] right, int rightoff) { + double x1 = src[srcoff]; + double y1 = src[srcoff + 1]; + double ax = src[srcoff + 2]; + double ay = src[srcoff + 3]; + double bx = src[srcoff + 4]; + double by = src[srcoff + 5]; + double x2 = src[srcoff + 6]; + double y2 = src[srcoff + 7]; + double midx = (ax + bx) / 2.0; + double midy = (ay + by) / 2.0; + double lax = (x1 + ax) / 2.0; + double lay = (y1 + ay) / 2.0; + double rbx = (bx + x2) / 2.0; + double rby = (by + y2) / 2.0; + double lbx = (lax + midx) / 2.0; + double lby = (lay + midy) / 2.0; + double rax = (midx + rbx) / 2.0; + double ray = (midy + rby) / 2.0; + double mx = (lbx + rax) / 2.0; + double my = (lby + ray) / 2.0; + if (left != null) { + left[leftoff] = x1; + left[leftoff + 1] = y1; + left[leftoff + 2] = lax; + left[leftoff + 3] = lay; + left[leftoff + 4] = lbx; + left[leftoff + 5] = lby; + left[leftoff + 6] = mx; + left[leftoff + 7] = my; + } + if (right != null) { + right[rightoff] = mx; + right[rightoff + 1] = my; + right[rightoff + 2] = rax; + right[rightoff + 3] = ray; + right[rightoff + 4] = rbx; + right[rightoff + 5] = rby; + right[rightoff + 6] = x2; + right[rightoff + 7] = y2; + } + } + + @Override + public boolean contains(double x, double y) { + return Outline.of(this).contains(x, y, PathIterator.WIND_EVEN_ODD); + } + + @Override + public boolean contains(Point2D p) { + return contains(p.getX(), p.getY()); + } + + @Override + public boolean intersects(double x, double y, double w, double h) { + return Outline.of(this).intersects(x, y, w, h, PathIterator.WIND_EVEN_ODD); + } + + @Override + public boolean intersects(Rectangle2D r) { + return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public boolean contains(double x, double y, double w, double h) { + return Outline.of(this).contains(x, y, w, h, PathIterator.WIND_EVEN_ODD); + } + + @Override + public boolean contains(Rectangle2D r) { + return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public Rectangle getBounds() { + return getBounds2D().getBounds(); + } + + @Override + public PathIterator getPathIterator(AffineTransform at) { + SegmentIterator it = new SegmentIterator(PathIterator.WIND_NON_ZERO, at, 2); + it.move(getX1(), getY1()); + it.cubic(getCtrlX1(), getCtrlY1(), getCtrlX2(), getCtrlY2(), getX2(), getY2()); + return it; + } + + @Override + public PathIterator getPathIterator(AffineTransform at, double flatness) { + return new FlatteningPathIterator(getPathIterator(at), flatness); + } + + /// Returns a copy of this curve. A subclass from elsewhere that does not + /// override it gets a double precision curve back. + @Override + public Object clone() { + return new Double(getX1(), getY1(), getCtrlX1(), getCtrlY1(), getCtrlX2(), getCtrlY2(), getX2(), getY2()); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Dimension2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Dimension2D.java new file mode 100644 index 00000000000..8ea7ae1991c --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Dimension2D.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +/// A width and a height, stored by a subclass in the precision it chooses. +public abstract class Dimension2D implements Cloneable { + + /// The copy handed out for a subclass that does not override `clone()`. + private static final class Copy extends Dimension2D { + + private double width; + private double height; + + Copy(double width, double height) { + this.width = width; + this.height = height; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public void setSize(double width, double height) { + this.width = width; + this.height = height; + } + + @Override + public Object clone() { + return new Copy(width, height); + } + } + + protected Dimension2D() { + } + + public abstract double getWidth(); + + public abstract double getHeight(); + + public abstract void setSize(double width, double height); + + public void setSize(Dimension2D d) { + setSize(d.getWidth(), d.getHeight()); + } + + /// Returns a copy of this size. A subclass that does not override it gets + /// a plain double precision size back rather than its own type. + @Override + public Object clone() { + return new Copy(getWidth(), getHeight()); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Ellipse2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Ellipse2D.java new file mode 100644 index 00000000000..36c16eda292 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Ellipse2D.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +/// The ellipse inscribed in a rectangular frame. +/// +/// Its outline is four cubic Bezier curves, one per quarter, starting at the +/// middle of the right side and running through the bottom, left and top. +public abstract class Ellipse2D extends RectangularShape { + + /// How far a quarter-circle's control points sit from its ends, as a + /// fraction of the radius: four thirds of the tangent of a sixteenth of + /// a turn. + static final double KAPPA = 4.0 / 3.0 * (Math.sqrt(2.0) - 1.0); + + /// An ellipse stored in single precision. + public static class Float extends Ellipse2D { + + public float x; + public float y; + public float width; + public float height; + + public Float() { + } + + public Float(float x, float y, float w, float h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public boolean isEmpty() { + return width <= 0.0f || height <= 0.0f; + } + + public void setFrame(float x, float y, float w, float h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + } + + @Override + public void setFrame(double x, double y, double w, double h) { + this.x = (float) x; + this.y = (float) y; + this.width = (float) w; + this.height = (float) h; + } + + @Override + public Rectangle2D getBounds2D() { + return new Rectangle2D.Float(x, y, width, height); + } + + @Override + public Object clone() { + return new Float(x, y, width, height); + } + } + + /// An ellipse stored in double precision. + public static class Double extends Ellipse2D { + + public double x; + public double y; + public double width; + public double height; + + public Double() { + } + + public Double(double x, double y, double w, double h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public boolean isEmpty() { + return width <= 0.0 || height <= 0.0; + } + + @Override + public void setFrame(double x, double y, double w, double h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + } + + @Override + public Rectangle2D getBounds2D() { + return new Rectangle2D.Double(x, y, width, height); + } + + @Override + public Object clone() { + return new Double(x, y, width, height); + } + } + + protected Ellipse2D() { + } + + @Override + public boolean contains(double x, double y) { + double w = getWidth(); + double h = getHeight(); + if (w <= 0.0 || h <= 0.0) { + return false; + } + // in the frame where the ellipse is the circle of diameter one + // about the origin + double nx = (x - getX()) / w - 0.5; + double ny = (y - getY()) / h - 0.5; + return nx * nx + ny * ny < 0.25; + } + + @Override + public boolean intersects(double x, double y, double w, double h) { + double ew = getWidth(); + double eh = getHeight(); + if (w <= 0.0 || h <= 0.0 || ew <= 0.0 || eh <= 0.0) { + return false; + } + // the rectangle in the unit-circle frame, then its point nearest + // the centre + double nx0 = (x - getX()) / ew - 0.5; + double nx1 = nx0 + w / ew; + double ny0 = (y - getY()) / eh - 0.5; + double ny1 = ny0 + h / eh; + double nearX = nx0 > 0.0 ? nx0 : (nx1 < 0.0 ? nx1 : 0.0); + double nearY = ny0 > 0.0 ? ny0 : (ny1 < 0.0 ? ny1 : 0.0); + return nearX * nearX + nearY * nearY < 0.25; + } + + @Override + public boolean contains(double x, double y, double w, double h) { + // an ellipse is convex, so the corners decide + return contains(x, y) && contains(x + w, y) + && contains(x, y + h) && contains(x + w, y + h); + } + + @Override + public PathIterator getPathIterator(AffineTransform at) { + SegmentIterator it = new SegmentIterator(PathIterator.WIND_NON_ZERO, at, 6); + double x = getX(); + double y = getY(); + double w = getWidth(); + double h = getHeight(); + double near = 0.5 - KAPPA / 2.0; + double far = 0.5 + KAPPA / 2.0; + it.move(x + w, y + h * 0.5); + it.cubic(x + w, y + h * far, x + w * far, y + h, x + w * 0.5, y + h); + it.cubic(x + w * near, y + h, x, y + h * far, x, y + h * 0.5); + it.cubic(x, y + h * near, x + w * near, y, x + w * 0.5, y); + it.cubic(x + w * far, y, x + w, y + h * near, x + w, y + h * 0.5); + it.close(); + return it; + } + + @Override + public int hashCode() { + long bits = java.lang.Double.doubleToLongBits(getX()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getY()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getWidth()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getHeight()); + return (int) bits ^ (int) (bits >> 32); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Ellipse2D) { + Ellipse2D e = (Ellipse2D) obj; + return getX() == e.getX() && getY() == e.getY() + && getWidth() == e.getWidth() && getHeight() == e.getHeight(); + } + return false; + } + + /// Returns a copy of this ellipse. A subclass from elsewhere that does + /// not override it gets a double precision ellipse back. + @Override + public Object clone() { + return new Double(getX(), getY(), getWidth(), getHeight()); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/FlatteningPathIterator.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/FlatteningPathIterator.java new file mode 100644 index 00000000000..2f52062de50 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/FlatteningPathIterator.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import java.util.NoSuchElementException; + +/// Reads another path iterator and replaces every curve with straight lines +/// that stay within a given distance of it. +/// +/// A curve is halved until its control points are within the flatness of +/// the chord joining its ends, or until it has been halved `limit` times. +/// Every point returned lies on the original curve. The number of lines a +/// curve becomes is not promised to match any other implementation. +public class FlatteningPathIterator implements PathIterator { + + private final PathIterator src; + private final double flatness; + private final double flatnessSq; + private final int limit; + private final double[] segment = new double[6]; + + /// End points of the lines the current curve became, not yet returned. + private double[] pending = new double[32]; + private int pendingCount; + private int pendingIndex; + + private double curX; + private double curY; + private double moveX; + private double moveY; + private int type; + private boolean done; + + public FlatteningPathIterator(PathIterator src, double flatness) { + this(src, flatness, 10); + } + + public FlatteningPathIterator(PathIterator src, double flatness, int limit) { + if (flatness < 0.0) { + throw new IllegalArgumentException("flatness must be >= 0"); + } + if (limit < 0) { + throw new IllegalArgumentException("limit must be >= 0"); + } + this.src = src; + this.flatness = flatness; + this.flatnessSq = flatness * flatness; + this.limit = limit; + advance(); + } + + public double getFlatness() { + return flatness; + } + + public int getRecursionLimit() { + return limit; + } + + @Override + public int getWindingRule() { + return src.getWindingRule(); + } + + @Override + public boolean isDone() { + return done; + } + + @Override + public void next() { + advance(); + } + + private void advance() { + if (pendingIndex < pendingCount) { + curX = pending[pendingIndex++]; + curY = pending[pendingIndex++]; + type = SEG_LINETO; + return; + } + if (src.isDone()) { + done = true; + return; + } + int t = src.currentSegment(segment); + src.next(); + switch (t) { + case SEG_MOVETO: + curX = segment[0]; + curY = segment[1]; + moveX = curX; + moveY = curY; + type = SEG_MOVETO; + break; + case SEG_LINETO: + curX = segment[0]; + curY = segment[1]; + type = SEG_LINETO; + break; + case SEG_QUADTO: + pendingCount = 0; + pendingIndex = 0; + quad(curX, curY, segment[0], segment[1], segment[2], segment[3], 0); + advance(); + break; + case SEG_CUBICTO: + pendingCount = 0; + pendingIndex = 0; + cubic(curX, curY, segment[0], segment[1], segment[2], segment[3], segment[4], segment[5], 0); + advance(); + break; + default: + curX = moveX; + curY = moveY; + type = SEG_CLOSE; + break; + } + } + + private void push(double x, double y) { + if (pendingCount + 2 > pending.length) { + double[] p = new double[pending.length * 2]; + System.arraycopy(pending, 0, p, 0, pendingCount); + pending = p; + } + pending[pendingCount++] = x; + pending[pendingCount++] = y; + } + + private void quad(double x0, double y0, double cx, double cy, double x1, double y1, int level) { + if (level >= limit || Line2D.ptSegDistSq(x0, y0, x1, y1, cx, cy) <= flatnessSq) { + push(x1, y1); + return; + } + double lx = (x0 + cx) / 2.0; + double ly = (y0 + cy) / 2.0; + double rx = (cx + x1) / 2.0; + double ry = (cy + y1) / 2.0; + double mx = (lx + rx) / 2.0; + double my = (ly + ry) / 2.0; + quad(x0, y0, lx, ly, mx, my, level + 1); + quad(mx, my, rx, ry, x1, y1, level + 1); + } + + private void cubic(double x0, double y0, double ax, double ay, double bx, double by, + double x1, double y1, int level) { + if (level >= limit || (Line2D.ptSegDistSq(x0, y0, x1, y1, ax, ay) <= flatnessSq + && Line2D.ptSegDistSq(x0, y0, x1, y1, bx, by) <= flatnessSq)) { + push(x1, y1); + return; + } + double lax = (x0 + ax) / 2.0; + double lay = (y0 + ay) / 2.0; + double midx = (ax + bx) / 2.0; + double midy = (ay + by) / 2.0; + double rbx = (bx + x1) / 2.0; + double rby = (by + y1) / 2.0; + double lbx = (lax + midx) / 2.0; + double lby = (lay + midy) / 2.0; + double rax = (midx + rbx) / 2.0; + double ray = (midy + rby) / 2.0; + double mx = (lbx + rax) / 2.0; + double my = (lby + ray) / 2.0; + cubic(x0, y0, lax, lay, lbx, lby, mx, my, level + 1); + cubic(mx, my, rax, ray, rbx, rby, x1, y1, level + 1); + } + + @Override + public int currentSegment(float[] coords) { + if (done) { + throw new NoSuchElementException("flattening iterator out of bounds"); + } + if (type != SEG_CLOSE) { + coords[0] = (float) curX; + coords[1] = (float) curY; + } + return type; + } + + @Override + public int currentSegment(double[] coords) { + if (done) { + throw new NoSuchElementException("flattening iterator out of bounds"); + } + if (type != SEG_CLOSE) { + coords[0] = curX; + coords[1] = curY; + } + return type; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/GeneralPath.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/GeneralPath.java new file mode 100644 index 00000000000..91dd896c8ca --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/GeneralPath.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import com.codename1.desktopcompat.java.awt.Shape; + +/// The original name of the single precision path; identical to +/// [Path2D.Float] apart from being final. +public final class GeneralPath extends Path2D.Float { + + public GeneralPath() { + super(WIND_NON_ZERO, 20); + } + + public GeneralPath(int rule) { + super(rule, 20); + } + + public GeneralPath(int rule, int initialCapacity) { + super(rule, initialCapacity); + } + + public GeneralPath(Shape s) { + super(s, null); + } + + @Override + public Object clone() { + return new GeneralPath(this); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/IllegalPathStateException.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/IllegalPathStateException.java new file mode 100644 index 00000000000..8228ddebf90 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/IllegalPathStateException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +/// Thrown when a path is asked for something its current state cannot give, +/// such as a line before the first move. +public class IllegalPathStateException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public IllegalPathStateException() { + super(); + } + + public IllegalPathStateException(String s) { + super(s); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Line2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Line2D.java new file mode 100644 index 00000000000..a5c2c373667 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Line2D.java @@ -0,0 +1,397 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import com.codename1.desktopcompat.java.awt.Rectangle; +import com.codename1.desktopcompat.java.awt.Shape; + +/// A line segment between two points. It has no area, so it contains +/// nothing, but it can be tested against rectangles and other segments. +public abstract class Line2D implements Shape, Cloneable { + + /// A segment stored in single precision. + public static class Float extends Line2D { + + public float x1; + public float y1; + public float x2; + public float y2; + + public Float() { + } + + public Float(float x1, float y1, float x2, float y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + } + + public Float(Point2D p1, Point2D p2) { + this.x1 = (float) p1.getX(); + this.y1 = (float) p1.getY(); + this.x2 = (float) p2.getX(); + this.y2 = (float) p2.getY(); + } + + @Override + public double getX1() { + return x1; + } + + @Override + public double getY1() { + return y1; + } + + @Override + public Point2D getP1() { + return new Point2D.Float(x1, y1); + } + + @Override + public double getX2() { + return x2; + } + + @Override + public double getY2() { + return y2; + } + + @Override + public Point2D getP2() { + return new Point2D.Float(x2, y2); + } + + @Override + public void setLine(double x1, double y1, double x2, double y2) { + this.x1 = (float) x1; + this.y1 = (float) y1; + this.x2 = (float) x2; + this.y2 = (float) y2; + } + + public void setLine(float x1, float y1, float x2, float y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + } + + @Override + public Rectangle2D getBounds2D() { + return new Rectangle2D.Float(Math.min(x1, x2), Math.min(y1, y2), + Math.abs(x2 - x1), Math.abs(y2 - y1)); + } + + @Override + public Object clone() { + return new Float(x1, y1, x2, y2); + } + } + + /// A segment stored in double precision. + public static class Double extends Line2D { + + public double x1; + public double y1; + public double x2; + public double y2; + + public Double() { + } + + public Double(double x1, double y1, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + } + + public Double(Point2D p1, Point2D p2) { + this.x1 = p1.getX(); + this.y1 = p1.getY(); + this.x2 = p2.getX(); + this.y2 = p2.getY(); + } + + @Override + public double getX1() { + return x1; + } + + @Override + public double getY1() { + return y1; + } + + @Override + public Point2D getP1() { + return new Point2D.Double(x1, y1); + } + + @Override + public double getX2() { + return x2; + } + + @Override + public double getY2() { + return y2; + } + + @Override + public Point2D getP2() { + return new Point2D.Double(x2, y2); + } + + @Override + public void setLine(double x1, double y1, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + } + + @Override + public Rectangle2D getBounds2D() { + return new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), + Math.abs(x2 - x1), Math.abs(y2 - y1)); + } + + @Override + public Object clone() { + return new Double(x1, y1, x2, y2); + } + } + + protected Line2D() { + } + + public abstract double getX1(); + + public abstract double getY1(); + + public abstract Point2D getP1(); + + public abstract double getX2(); + + public abstract double getY2(); + + public abstract Point2D getP2(); + + public abstract void setLine(double x1, double y1, double x2, double y2); + + public void setLine(Point2D p1, Point2D p2) { + setLine(p1.getX(), p1.getY(), p2.getX(), p2.getY()); + } + + public void setLine(Line2D l) { + setLine(l.getX1(), l.getY1(), l.getX2(), l.getY2()); + } + + /// Tells which way the segment from `(x1, y1)` to `(x2, y2)` has to turn + /// about its first end to point at `(px, py)`: 1 when that is the turn + /// taking the positive x axis towards the negative y axis, -1 for the + /// opposite turn. For a point on the segment's line the answer is 0 when + /// it lies between the ends, -1 when it lies before the first end and 1 + /// when it lies past the second. + public static int relativeCCW(double x1, double y1, double x2, double y2, double px, double py) { + double dx = x2 - x1; + double dy = y2 - y1; + double rx = px - x1; + double ry = py - y1; + double side = rx * dy - ry * dx; + if (side == 0.0) { + // on the line: order the point along it instead + side = rx * dx + ry * dy; + if (side > 0.0) { + side = (rx - dx) * dx + (ry - dy) * dy; + if (side < 0.0) { + side = 0.0; + } + } + } + return side < 0.0 ? -1 : (side > 0.0 ? 1 : 0); + } + + public int relativeCCW(double px, double py) { + return relativeCCW(getX1(), getY1(), getX2(), getY2(), px, py); + } + + public int relativeCCW(Point2D p) { + return relativeCCW(getX1(), getY1(), getX2(), getY2(), p.getX(), p.getY()); + } + + /// Whether two segments share a point: each must have the other's ends + /// on opposite sides of it, or on it. + public static boolean linesIntersect(double x1, double y1, double x2, double y2, + double x3, double y3, double x4, double y4) { + return relativeCCW(x1, y1, x2, y2, x3, y3) * relativeCCW(x1, y1, x2, y2, x4, y4) <= 0 + && relativeCCW(x3, y3, x4, y4, x1, y1) * relativeCCW(x3, y3, x4, y4, x2, y2) <= 0; + } + + public boolean intersectsLine(double x1, double y1, double x2, double y2) { + return linesIntersect(x1, y1, x2, y2, getX1(), getY1(), getX2(), getY2()); + } + + public boolean intersectsLine(Line2D l) { + return linesIntersect(l.getX1(), l.getY1(), l.getX2(), l.getY2(), getX1(), getY1(), getX2(), getY2()); + } + + /// The square of the distance from a point to the nearest point of a + /// segment. + public static double ptSegDistSq(double x1, double y1, double x2, double y2, double px, double py) { + double dx = x2 - x1; + double dy = y2 - y1; + double rx = px - x1; + double ry = py - y1; + double along = rx * dx + ry * dy; + double projSq; + if (along <= 0.0) { + // nearest to the first end + projSq = 0.0; + } else { + // measure from the second end instead + rx = dx - rx; + ry = dy - ry; + along = rx * dx + ry * dy; + projSq = along <= 0.0 ? 0.0 : along * along / (dx * dx + dy * dy); + } + double distSq = rx * rx + ry * ry - projSq; + return distSq < 0.0 ? 0.0 : distSq; + } + + public static double ptSegDist(double x1, double y1, double x2, double y2, double px, double py) { + return Math.sqrt(ptSegDistSq(x1, y1, x2, y2, px, py)); + } + + public double ptSegDistSq(double px, double py) { + return ptSegDistSq(getX1(), getY1(), getX2(), getY2(), px, py); + } + + public double ptSegDistSq(Point2D pt) { + return ptSegDistSq(getX1(), getY1(), getX2(), getY2(), pt.getX(), pt.getY()); + } + + public double ptSegDist(double px, double py) { + return ptSegDist(getX1(), getY1(), getX2(), getY2(), px, py); + } + + public double ptSegDist(Point2D pt) { + return ptSegDist(getX1(), getY1(), getX2(), getY2(), pt.getX(), pt.getY()); + } + + /// The square of the distance from a point to the infinite line through + /// two points. + public static double ptLineDistSq(double x1, double y1, double x2, double y2, double px, double py) { + double dx = x2 - x1; + double dy = y2 - y1; + double rx = px - x1; + double ry = py - y1; + double along = rx * dx + ry * dy; + double distSq = rx * rx + ry * ry - along * along / (dx * dx + dy * dy); + return distSq < 0.0 ? 0.0 : distSq; + } + + public static double ptLineDist(double x1, double y1, double x2, double y2, double px, double py) { + return Math.sqrt(ptLineDistSq(x1, y1, x2, y2, px, py)); + } + + public double ptLineDistSq(double px, double py) { + return ptLineDistSq(getX1(), getY1(), getX2(), getY2(), px, py); + } + + public double ptLineDistSq(Point2D pt) { + return ptLineDistSq(getX1(), getY1(), getX2(), getY2(), pt.getX(), pt.getY()); + } + + public double ptLineDist(double px, double py) { + return ptLineDist(getX1(), getY1(), getX2(), getY2(), px, py); + } + + public double ptLineDist(Point2D pt) { + return ptLineDist(getX1(), getY1(), getX2(), getY2(), pt.getX(), pt.getY()); + } + + /// Always false: a segment has no interior. + @Override + public boolean contains(double x, double y) { + return false; + } + + /// Always false: a segment has no interior. + @Override + public boolean contains(Point2D p) { + return false; + } + + @Override + public boolean intersects(double x, double y, double w, double h) { + return new Rectangle2D.Double(x, y, w, h).intersectsLine(getX1(), getY1(), getX2(), getY2()); + } + + @Override + public boolean intersects(Rectangle2D r) { + return r.intersectsLine(getX1(), getY1(), getX2(), getY2()); + } + + /// Always false: a segment has no interior. + @Override + public boolean contains(double x, double y, double w, double h) { + return false; + } + + /// Always false: a segment has no interior. + @Override + public boolean contains(Rectangle2D r) { + return false; + } + + @Override + public Rectangle getBounds() { + return getBounds2D().getBounds(); + } + + @Override + public PathIterator getPathIterator(AffineTransform at) { + SegmentIterator it = new SegmentIterator(PathIterator.WIND_NON_ZERO, at, 2); + it.move(getX1(), getY1()); + it.line(getX2(), getY2()); + return it; + } + + /// A segment is already flat, so this is the plain outline. + @Override + public PathIterator getPathIterator(AffineTransform at, double flatness) { + return getPathIterator(at); + } + + /// Returns a copy of this segment. A subclass from elsewhere that does + /// not override it gets a double precision segment back. + @Override + public Object clone() { + return new Double(getX1(), getY1(), getX2(), getY2()); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/NoninvertibleTransformException.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/NoninvertibleTransformException.java new file mode 100644 index 00000000000..749b779957e --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/NoninvertibleTransformException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +/// Thrown when an operation needs the inverse of an [AffineTransform] whose +/// determinant is zero. +public class NoninvertibleTransformException extends Exception { + + private static final long serialVersionUID = 1L; + + public NoninvertibleTransformException(String s) { + super(s); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Outline.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Outline.java new file mode 100644 index 00000000000..65a31825e3f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Outline.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import com.codename1.desktopcompat.java.awt.Shape; + +/// The flattened outline of a shape as a list of directed edges, with every +/// subpath closed, for hit testing. +/// +/// Point tests count the edges a ray towards positive x crosses, with an +/// edge owning its upper end and not its lower one, so a point on a left or +/// top boundary is inside and one on a right or bottom boundary is not. +/// Rectangle tests are exact for polygons: the open interior of a rectangle +/// that no edge passes through lies wholly inside or wholly outside. +final class Outline { + + private double[] edges = new double[64]; + private int size; + + /// Flattens a shape to within a hundred-thousandth of its larger side. + static Outline of(Shape s) { + Rectangle2D b = s.getBounds2D(); + double flatness = Math.max(b.getWidth(), b.getHeight()) * 1e-5; + if (!(flatness > 0.0)) { + flatness = 0.0; + } + return new Outline(new FlatteningPathIterator(s.getPathIterator(null), flatness)); + } + + /// Reads an iterator that returns only moves, lines and closes. + private Outline(PathIterator pi) { + double[] c = new double[6]; + double startX = 0.0; + double startY = 0.0; + double curX = 0.0; + double curY = 0.0; + while (!pi.isDone()) { + switch (pi.currentSegment(c)) { + case PathIterator.SEG_MOVETO: + edge(curX, curY, startX, startY); + startX = c[0]; + startY = c[1]; + curX = startX; + curY = startY; + break; + case PathIterator.SEG_LINETO: + edge(curX, curY, c[0], c[1]); + curX = c[0]; + curY = c[1]; + break; + default: + edge(curX, curY, startX, startY); + curX = startX; + curY = startY; + break; + } + pi.next(); + } + edge(curX, curY, startX, startY); + } + + private void edge(double x1, double y1, double x2, double y2) { + if (x1 == x2 && y1 == y2) { + return; + } + if (size + 4 > edges.length) { + double[] e = new double[edges.length * 2]; + System.arraycopy(edges, 0, e, 0, size); + edges = e; + } + edges[size++] = x1; + edges[size++] = y1; + edges[size++] = x2; + edges[size++] = y2; + } + + /// The signed number of edges a ray from the point towards positive x + /// crosses: downward edges count one way and upward edges the other. + private int crossings(double px, double py) { + int n = 0; + for (int i = 0; i < size; i += 4) { + double x1 = edges[i]; + double y1 = edges[i + 1]; + double x2 = edges[i + 2]; + double y2 = edges[i + 3]; + boolean down = y1 < y2; + if (down ? (py < y1 || py >= y2) : (py < y2 || py >= y1)) { + continue; + } + if (px >= x1 && px >= x2) { + continue; + } + if ((px < x1 && px < x2) || px < x1 + (py - y1) * (x2 - x1) / (y2 - y1)) { + n += down ? 1 : -1; + } + } + return n; + } + + boolean contains(double x, double y, int rule) { + int n = crossings(x, y); + return rule == PathIterator.WIND_NON_ZERO ? n != 0 : (n & 1) != 0; + } + + /// Whether any edge passes through the open interior of the rectangle. + private boolean entersInterior(double rx, double ry, double rw, double rh) { + double[] range = new double[2]; + for (int i = 0; i < size; i += 4) { + double x1 = edges[i]; + double y1 = edges[i + 1]; + double dx = edges[i + 2] - x1; + double dy = edges[i + 3] - y1; + // the part of the edge inside the closed rectangle, as a + // parameter range along it + range[0] = 0.0; + range[1] = 1.0; + if (clip(-dx, x1 - rx, range) && clip(dx, rx + rw - x1, range) + && clip(-dy, y1 - ry, range) && clip(dy, ry + rh - y1, range) + && range[0] < range[1]) { + double t = (range[0] + range[1]) / 2.0; + double mx = x1 + dx * t; + double my = y1 + dy * t; + if (mx > rx && mx < rx + rw && my > ry && my < ry + rh) { + return true; + } + } + } + return false; + } + + /// Narrows a parameter range to the half plane `p * t <= q`; false when + /// nothing is left. + private static boolean clip(double p, double q, double[] range) { + if (p == 0.0) { + return q >= 0.0; + } + double t = q / p; + if (p < 0.0) { + if (t > range[1]) { + return false; + } + if (t > range[0]) { + range[0] = t; + } + } else { + if (t < range[0]) { + return false; + } + if (t < range[1]) { + range[1] = t; + } + } + return true; + } + + boolean intersects(double x, double y, double w, double h, int rule) { + if (!(w > 0.0 && h > 0.0)) { + return false; + } + return entersInterior(x, y, w, h) || contains(x + w / 2.0, y + h / 2.0, rule); + } + + boolean contains(double x, double y, double w, double h, int rule) { + if (!(w > 0.0 && h > 0.0)) { + return false; + } + return !entersInterior(x, y, w, h) && contains(x + w / 2.0, y + h / 2.0, rule); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Path2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Path2D.java new file mode 100644 index 00000000000..c4e7814574c --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Path2D.java @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import com.codename1.desktopcompat.java.awt.Rectangle; +import com.codename1.desktopcompat.java.awt.Shape; + +/// An outline built from moves, lines, quadratic and cubic curves, with a +/// winding rule that decides its interior. +/// +/// Hit tests run over the flattened outline with every subpath closed. They +/// are exact for polygons; for a rectangle test against a path that overlaps +/// itself under the non-zero rule, `contains` may answer false and +/// `intersects` true where the exact answer is the opposite. `getBounds2D` +/// answers the bounds of every stored point, control points included. +/// +/// A path is not safe to change from two threads at once. +public abstract class Path2D implements Shape, Cloneable { + + public static final int WIND_EVEN_ODD = PathIterator.WIND_EVEN_ODD; + + public static final int WIND_NON_ZERO = PathIterator.WIND_NON_ZERO; + + /// A path whose coordinates are held to single precision. + public static class Float extends Path2D { + + public Float() { + this(WIND_NON_ZERO, 20); + } + + public Float(int rule) { + this(rule, 20); + } + + public Float(int rule, int initialCapacity) { + super(rule, initialCapacity, true); + } + + public Float(Shape s) { + this(s, null); + } + + public Float(Shape s, AffineTransform at) { + super(WIND_NON_ZERO, 20, true); + copy(s, at); + } + + public final void moveTo(float x, float y) { + addMove(x, y); + } + + public final void lineTo(float x, float y) { + addLine(x, y); + } + + public final void quadTo(float x1, float y1, float x2, float y2) { + addQuad(x1, y1, x2, y2); + } + + public final void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) { + addCubic(x1, y1, x2, y2, x3, y3); + } + + @Override + public Object clone() { + return new Float(this); + } + } + + /// A path whose coordinates are held to double precision. + public static class Double extends Path2D { + + public Double() { + this(WIND_NON_ZERO, 20); + } + + public Double(int rule) { + this(rule, 20); + } + + public Double(int rule, int initialCapacity) { + super(rule, initialCapacity, false); + } + + public Double(Shape s) { + this(s, null); + } + + public Double(Shape s, AffineTransform at) { + super(WIND_NON_ZERO, 20, false); + copy(s, at); + } + + @Override + public Object clone() { + return new Double(this); + } + } + + private byte[] types; + private double[] coords; + private int typeCount; + private int coordCount; + private int windingRule; + + /// Whether every stored coordinate is rounded to a float first, which + /// is all that tells the two concrete paths apart. + private final boolean single; + + Path2D(int rule, int initialCapacity, boolean single) { + checkRule(rule); + this.windingRule = rule; + this.single = single; + this.types = new byte[Math.max(initialCapacity, 1)]; + this.coords = new double[Math.max(initialCapacity, 1) * 2]; + } + + private static void checkRule(int rule) { + if (rule != WIND_EVEN_ODD && rule != WIND_NON_ZERO) { + throw new IllegalArgumentException("winding rule must be WIND_EVEN_ODD or WIND_NON_ZERO"); + } + } + + /// Fills a new path from a shape, optionally transformed. + final void copy(Shape s, AffineTransform at) { + if (s instanceof Path2D) { + Path2D p = (Path2D) s; + windingRule = p.windingRule; + typeCount = p.typeCount; + coordCount = p.coordCount; + types = new byte[Math.max(typeCount, 1)]; + coords = new double[Math.max(coordCount, 2)]; + System.arraycopy(p.types, 0, types, 0, typeCount); + System.arraycopy(p.coords, 0, coords, 0, coordCount); + if (at != null) { + at.transform(coords, 0, coords, 0, coordCount / 2); + } + round(); + } else { + PathIterator pi = s.getPathIterator(at); + checkRule(pi.getWindingRule()); + windingRule = pi.getWindingRule(); + addAll(pi, false); + } + } + + private void round() { + if (single) { + for (int i = 0; i < coordCount; i++) { + coords[i] = (float) coords[i]; + } + } + } + + private void room(int type, boolean needsMove) { + if (needsMove && typeCount == 0) { + throw new IllegalPathStateException("missing initial moveto in path definition"); + } + if (typeCount == types.length) { + byte[] t = new byte[typeCount * 2]; + System.arraycopy(types, 0, t, 0, typeCount); + types = t; + } + if (coordCount + 6 > coords.length) { + double[] c = new double[coords.length * 2 + 6]; + System.arraycopy(coords, 0, c, 0, coordCount); + coords = c; + } + types[typeCount++] = (byte) type; + } + + private void put(double v) { + coords[coordCount++] = single ? (double) (float) v : v; + } + + final void addMove(double x, double y) { + if (typeCount > 0 && types[typeCount - 1] == PathIterator.SEG_MOVETO) { + // a move straight after a move replaces it + coordCount -= 2; + } else { + room(PathIterator.SEG_MOVETO, false); + } + put(x); + put(y); + } + + final void addLine(double x, double y) { + room(PathIterator.SEG_LINETO, true); + put(x); + put(y); + } + + final void addQuad(double x1, double y1, double x2, double y2) { + room(PathIterator.SEG_QUADTO, true); + put(x1); + put(y1); + put(x2); + put(y2); + } + + final void addCubic(double x1, double y1, double x2, double y2, double x3, double y3) { + room(PathIterator.SEG_CUBICTO, true); + put(x1); + put(y1); + put(x2); + put(y2); + put(x3); + put(y3); + } + + private void addAll(PathIterator pi, boolean connect) { + double[] c = new double[6]; + while (!pi.isDone()) { + switch (pi.currentSegment(c)) { + case PathIterator.SEG_MOVETO: + if (!connect || typeCount == 0 || coordCount == 0) { + addMove(c[0], c[1]); + } else if (types[typeCount - 1] == PathIterator.SEG_CLOSE + || coords[coordCount - 2] != stored(c[0]) || coords[coordCount - 1] != stored(c[1])) { + // joined to what is already here, unless the open + // subpath ends on this very point + addLine(c[0], c[1]); + } + break; + case PathIterator.SEG_LINETO: + addLine(c[0], c[1]); + break; + case PathIterator.SEG_QUADTO: + addQuad(c[0], c[1], c[2], c[3]); + break; + case PathIterator.SEG_CUBICTO: + addCubic(c[0], c[1], c[2], c[3], c[4], c[5]); + break; + default: + closePath(); + break; + } + pi.next(); + connect = false; + } + } + + private double stored(double v) { + return single ? (double) (float) v : v; + } + + /// Starts a new subpath. A move that directly follows another move + /// replaces it. + public void moveTo(double x, double y) { + addMove(x, y); + } + + public void lineTo(double x, double y) { + addLine(x, y); + } + + public void quadTo(double x1, double y1, double x2, double y2) { + addQuad(x1, y1, x2, y2); + } + + public void curveTo(double x1, double y1, double x2, double y2, double x3, double y3) { + addCubic(x1, y1, x2, y2, x3, y3); + } + + /// Closes the current subpath; does nothing when it is already closed. + public final void closePath() { + if (typeCount == 0 || types[typeCount - 1] != PathIterator.SEG_CLOSE) { + room(PathIterator.SEG_CLOSE, true); + } + } + + public final void append(Shape s, boolean connect) { + addAll(s.getPathIterator(null), connect); + } + + /// Adds every segment of an iterator. With `connect`, a leading move + /// becomes a line from the current point, or is dropped when the open + /// subpath already ends there. + public void append(PathIterator pi, boolean connect) { + addAll(pi, connect); + } + + public final int getWindingRule() { + return windingRule; + } + + public final void setWindingRule(int rule) { + checkRule(rule); + windingRule = rule; + } + + /// The point the next segment starts from, or null for an empty path. + /// After a close that is the start of the subpath just closed. + public final Point2D getCurrentPoint() { + if (typeCount == 0 || coordCount == 0) { + return null; + } + int index = coordCount; + if (types[typeCount - 1] == PathIterator.SEG_CLOSE) { + // walk back to the move that opened the closed subpath + for (int i = typeCount - 2; i > 0; i--) { + int type = types[i]; + if (type == PathIterator.SEG_MOVETO) { + break; + } + index -= SegmentIterator.size(type); + } + } + return point(coords[index - 2], coords[index - 1]); + } + + private Point2D point(double x, double y) { + if (single) { + return new Point2D.Float((float) x, (float) y); + } + return new Point2D.Double(x, y); + } + + /// Empties the path, keeping its winding rule. + public final void reset() { + typeCount = 0; + coordCount = 0; + } + + public void transform(AffineTransform at) { + at.transform(coords, 0, coords, 0, coordCount / 2); + round(); + } + + /// Returns a transformed copy of this path, of the same class. + public final Shape createTransformedShape(AffineTransform at) { + Object copy = clone(); + if (!(copy instanceof Path2D)) { + return null; + } + Path2D p = (Path2D) copy; + if (at != null) { + p.transform(at); + } + return p; + } + + @Override + public final Rectangle getBounds() { + return getBounds2D().getBounds(); + } + + /// The bounds of every stored point, in the precision of the path. + @Override + public Rectangle2D getBounds2D() { + double x1 = 0.0; + double y1 = 0.0; + double x2 = 0.0; + double y2 = 0.0; + for (int i = 0; i < coordCount; i += 2) { + double x = coords[i]; + double y = coords[i + 1]; + if (i == 0) { + x1 = x; + x2 = x; + y1 = y; + y2 = y; + } else { + x1 = Math.min(x1, x); + x2 = Math.max(x2, x); + y1 = Math.min(y1, y); + y2 = Math.max(y2, y); + } + } + if (single) { + return new Rectangle2D.Float((float) x1, (float) y1, (float) (x2 - x1), (float) (y2 - y1)); + } + return new Rectangle2D.Double(x1, y1, x2 - x1, y2 - y1); + } + + private static Path2D collect(PathIterator pi) { + Path2D p = new Double(pi.getWindingRule()); + p.append(pi, false); + return p; + } + + public static boolean contains(PathIterator pi, double x, double y) { + return collect(pi).contains(x, y); + } + + public static boolean contains(PathIterator pi, Point2D p) { + return collect(pi).contains(p.getX(), p.getY()); + } + + @Override + public final boolean contains(double x, double y) { + if (typeCount < 2) { + return false; + } + return Outline.of(this).contains(x, y, windingRule); + } + + @Override + public final boolean contains(Point2D p) { + return contains(p.getX(), p.getY()); + } + + public static boolean contains(PathIterator pi, double x, double y, double w, double h) { + return collect(pi).contains(x, y, w, h); + } + + public static boolean contains(PathIterator pi, Rectangle2D r) { + return collect(pi).contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public final boolean contains(double x, double y, double w, double h) { + if (typeCount < 2) { + return false; + } + return Outline.of(this).contains(x, y, w, h, windingRule); + } + + @Override + public final boolean contains(Rectangle2D r) { + return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + public static boolean intersects(PathIterator pi, double x, double y, double w, double h) { + return collect(pi).intersects(x, y, w, h); + } + + public static boolean intersects(PathIterator pi, Rectangle2D r) { + return collect(pi).intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public final boolean intersects(double x, double y, double w, double h) { + if (typeCount < 2) { + return false; + } + return Outline.of(this).intersects(x, y, w, h, windingRule); + } + + @Override + public final boolean intersects(Rectangle2D r) { + return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + /// The iterator reads this path's own storage: change the path while + /// iterating and what the iterator returns is undefined. + @Override + public PathIterator getPathIterator(AffineTransform at) { + return new SegmentIterator(windingRule, at, types, typeCount, coords); + } + + @Override + public final PathIterator getPathIterator(AffineTransform at, double flatness) { + return new FlatteningPathIterator(getPathIterator(at), flatness); + } + + @Override + public abstract Object clone(); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/PathIterator.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/PathIterator.java new file mode 100644 index 00000000000..fb06b8bc44d --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/PathIterator.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +/// Walks the outline of a shape one segment at a time. +/// +/// A segment is a move, a line, a quadratic or cubic Bezier curve, or the +/// closing of the current subpath. `currentSegment` stores the points of the +/// segment in the array it is handed and returns one of the `SEG_` constants. +public interface PathIterator { + + /// Interior rule: a point is inside when a ray from it crosses the + /// outline an odd number of times. + int WIND_EVEN_ODD = 0; + + /// Interior rule: a point is inside when the signed count of outline + /// crossings of a ray from it is not zero. + int WIND_NON_ZERO = 1; + + /// Starts a new subpath at one point. + int SEG_MOVETO = 0; + + /// A straight line to one point. + int SEG_LINETO = 1; + + /// A quadratic curve: one control point, then the end point. + int SEG_QUADTO = 2; + + /// A cubic curve: two control points, then the end point. + int SEG_CUBICTO = 3; + + /// Closes the current subpath back to its last move; stores no points. + int SEG_CLOSE = 4; + + int getWindingRule(); + + boolean isDone(); + + void next(); + + int currentSegment(float[] coords); + + int currentSegment(double[] coords); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Point2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Point2D.java new file mode 100644 index 00000000000..d867205869f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Point2D.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +/// A location in two dimensional space, stored by a subclass in the +/// precision it chooses. +public abstract class Point2D implements Cloneable { + + /// A point stored in single precision. + public static class Float extends Point2D { + + public float x; + + public float y; + + public Float() { + } + + public Float(float x, float y) { + this.x = x; + this.y = y; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public void setLocation(double x, double y) { + this.x = (float) x; + this.y = (float) y; + } + + public void setLocation(float x, float y) { + this.x = x; + this.y = y; + } + + @Override + public Object clone() { + return new Float(x, y); + } + + @Override + public String toString() { + return "Point2D.Float[" + x + ", " + y + "]"; + } + } + + /// A point stored in double precision. + public static class Double extends Point2D { + + public double x; + + public double y; + + public Double() { + } + + public Double(double x, double y) { + this.x = x; + this.y = y; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public void setLocation(double x, double y) { + this.x = x; + this.y = y; + } + + @Override + public Object clone() { + return new Double(x, y); + } + + @Override + public String toString() { + return "Point2D.Double[" + x + ", " + y + "]"; + } + } + + protected Point2D() { + } + + public abstract double getX(); + + public abstract double getY(); + + public abstract void setLocation(double x, double y); + + public void setLocation(Point2D p) { + setLocation(p.getX(), p.getY()); + } + + public static double distanceSq(double x1, double y1, double x2, double y2) { + double dx = x1 - x2; + double dy = y1 - y2; + return dx * dx + dy * dy; + } + + public static double distance(double x1, double y1, double x2, double y2) { + return Math.sqrt(distanceSq(x1, y1, x2, y2)); + } + + public double distanceSq(double px, double py) { + return distanceSq(getX(), getY(), px, py); + } + + public double distanceSq(Point2D pt) { + return distanceSq(getX(), getY(), pt.getX(), pt.getY()); + } + + public double distance(double px, double py) { + return Math.sqrt(distanceSq(getX(), getY(), px, py)); + } + + public double distance(Point2D pt) { + return Math.sqrt(distanceSq(getX(), getY(), pt.getX(), pt.getY())); + } + + /// Returns a copy of this point. A subclass outside this package that + /// does not override it gets a double precision point back. + @Override + public Object clone() { + return new Double(getX(), getY()); + } + + @Override + public int hashCode() { + long bits = java.lang.Double.doubleToLongBits(getX()) * 31 + java.lang.Double.doubleToLongBits(getY()); + return (int) bits ^ (int) (bits >> 32); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Point2D) { + Point2D p = (Point2D) obj; + return getX() == p.getX() && getY() == p.getY(); + } + return false; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/QuadCurve2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/QuadCurve2D.java new file mode 100644 index 00000000000..cac15330157 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/QuadCurve2D.java @@ -0,0 +1,441 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import com.codename1.desktopcompat.java.awt.Rectangle; +import com.codename1.desktopcompat.java.awt.Shape; + +/// A quadratic Bezier curve: two end points and one control point. +/// +/// The area tested by `contains` and `intersects` is the region between the +/// curve and the straight line joining its ends, and is evaluated over the +/// flattened curve. `getBounds2D` answers the bounds of the three defining +/// points, which enclose the curve without being tight. +public abstract class QuadCurve2D implements Shape, Cloneable { + + /// A curve stored in single precision. + public static class Float extends QuadCurve2D { + + public float x1; + public float y1; + public float ctrlx; + public float ctrly; + public float x2; + public float y2; + + public Float() { + } + + public Float(float x1, float y1, float ctrlx, float ctrly, float x2, float y2) { + this.x1 = x1; + this.y1 = y1; + this.ctrlx = ctrlx; + this.ctrly = ctrly; + this.x2 = x2; + this.y2 = y2; + } + + @Override + public double getX1() { + return x1; + } + + @Override + public double getY1() { + return y1; + } + + @Override + public Point2D getP1() { + return new Point2D.Float(x1, y1); + } + + @Override + public double getCtrlX() { + return ctrlx; + } + + @Override + public double getCtrlY() { + return ctrly; + } + + @Override + public Point2D getCtrlPt() { + return new Point2D.Float(ctrlx, ctrly); + } + + @Override + public double getX2() { + return x2; + } + + @Override + public double getY2() { + return y2; + } + + @Override + public Point2D getP2() { + return new Point2D.Float(x2, y2); + } + + @Override + public void setCurve(double x1, double y1, double ctrlx, double ctrly, double x2, double y2) { + this.x1 = (float) x1; + this.y1 = (float) y1; + this.ctrlx = (float) ctrlx; + this.ctrly = (float) ctrly; + this.x2 = (float) x2; + this.y2 = (float) y2; + } + + public void setCurve(float x1, float y1, float ctrlx, float ctrly, float x2, float y2) { + this.x1 = x1; + this.y1 = y1; + this.ctrlx = ctrlx; + this.ctrly = ctrly; + this.x2 = x2; + this.y2 = y2; + } + + @Override + public Rectangle2D getBounds2D() { + float left = Math.min(Math.min(x1, x2), ctrlx); + float top = Math.min(Math.min(y1, y2), ctrly); + float right = Math.max(Math.max(x1, x2), ctrlx); + float bottom = Math.max(Math.max(y1, y2), ctrly); + return new Rectangle2D.Float(left, top, right - left, bottom - top); + } + + @Override + public Object clone() { + return new Float(x1, y1, ctrlx, ctrly, x2, y2); + } + } + + /// A curve stored in double precision. + public static class Double extends QuadCurve2D { + + public double x1; + public double y1; + public double ctrlx; + public double ctrly; + public double x2; + public double y2; + + public Double() { + } + + public Double(double x1, double y1, double ctrlx, double ctrly, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + this.ctrlx = ctrlx; + this.ctrly = ctrly; + this.x2 = x2; + this.y2 = y2; + } + + @Override + public double getX1() { + return x1; + } + + @Override + public double getY1() { + return y1; + } + + @Override + public Point2D getP1() { + return new Point2D.Double(x1, y1); + } + + @Override + public double getCtrlX() { + return ctrlx; + } + + @Override + public double getCtrlY() { + return ctrly; + } + + @Override + public Point2D getCtrlPt() { + return new Point2D.Double(ctrlx, ctrly); + } + + @Override + public double getX2() { + return x2; + } + + @Override + public double getY2() { + return y2; + } + + @Override + public Point2D getP2() { + return new Point2D.Double(x2, y2); + } + + @Override + public void setCurve(double x1, double y1, double ctrlx, double ctrly, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + this.ctrlx = ctrlx; + this.ctrly = ctrly; + this.x2 = x2; + this.y2 = y2; + } + + @Override + public Rectangle2D getBounds2D() { + double left = Math.min(Math.min(x1, x2), ctrlx); + double top = Math.min(Math.min(y1, y2), ctrly); + double right = Math.max(Math.max(x1, x2), ctrlx); + double bottom = Math.max(Math.max(y1, y2), ctrly); + return new Rectangle2D.Double(left, top, right - left, bottom - top); + } + + @Override + public Object clone() { + return new Double(x1, y1, ctrlx, ctrly, x2, y2); + } + } + + protected QuadCurve2D() { + } + + public abstract double getX1(); + + public abstract double getY1(); + + public abstract Point2D getP1(); + + public abstract double getCtrlX(); + + public abstract double getCtrlY(); + + public abstract Point2D getCtrlPt(); + + public abstract double getX2(); + + public abstract double getY2(); + + public abstract Point2D getP2(); + + public abstract void setCurve(double x1, double y1, double ctrlx, double ctrly, double x2, double y2); + + public void setCurve(double[] coords, int offset) { + setCurve(coords[offset], coords[offset + 1], coords[offset + 2], coords[offset + 3], + coords[offset + 4], coords[offset + 5]); + } + + public void setCurve(Point2D p1, Point2D cp, Point2D p2) { + setCurve(p1.getX(), p1.getY(), cp.getX(), cp.getY(), p2.getX(), p2.getY()); + } + + public void setCurve(Point2D[] pts, int offset) { + setCurve(pts[offset].getX(), pts[offset].getY(), pts[offset + 1].getX(), pts[offset + 1].getY(), + pts[offset + 2].getX(), pts[offset + 2].getY()); + } + + public void setCurve(QuadCurve2D c) { + setCurve(c.getX1(), c.getY1(), c.getCtrlX(), c.getCtrlY(), c.getX2(), c.getY2()); + } + + /// The square of the distance of the control point from the segment + /// joining the end points. + public static double getFlatnessSq(double x1, double y1, double ctrlx, double ctrly, double x2, double y2) { + return Line2D.ptSegDistSq(x1, y1, x2, y2, ctrlx, ctrly); + } + + public static double getFlatness(double x1, double y1, double ctrlx, double ctrly, double x2, double y2) { + return Line2D.ptSegDist(x1, y1, x2, y2, ctrlx, ctrly); + } + + public static double getFlatnessSq(double[] coords, int offset) { + return Line2D.ptSegDistSq(coords[offset], coords[offset + 1], coords[offset + 4], coords[offset + 5], + coords[offset + 2], coords[offset + 3]); + } + + public static double getFlatness(double[] coords, int offset) { + return Line2D.ptSegDist(coords[offset], coords[offset + 1], coords[offset + 4], coords[offset + 5], + coords[offset + 2], coords[offset + 3]); + } + + public double getFlatnessSq() { + return Line2D.ptSegDistSq(getX1(), getY1(), getX2(), getY2(), getCtrlX(), getCtrlY()); + } + + public double getFlatness() { + return Line2D.ptSegDist(getX1(), getY1(), getX2(), getY2(), getCtrlX(), getCtrlY()); + } + + /// Splits this curve at its middle into two curves; either may be null + /// or this curve itself. + public void subdivide(QuadCurve2D left, QuadCurve2D right) { + subdivide(this, left, right); + } + + public static void subdivide(QuadCurve2D src, QuadCurve2D left, QuadCurve2D right) { + double x1 = src.getX1(); + double y1 = src.getY1(); + double x2 = src.getX2(); + double y2 = src.getY2(); + double lx = (x1 + src.getCtrlX()) / 2.0; + double ly = (y1 + src.getCtrlY()) / 2.0; + double rx = (src.getCtrlX() + x2) / 2.0; + double ry = (src.getCtrlY() + y2) / 2.0; + double mx = (lx + rx) / 2.0; + double my = (ly + ry) / 2.0; + if (left != null) { + left.setCurve(x1, y1, lx, ly, mx, my); + } + if (right != null) { + right.setCurve(mx, my, rx, ry, x2, y2); + } + } + + /// Splits six coordinates into two curves of six; a destination may be + /// null, and may be the source array where the ranges allow it. + public static void subdivide(double[] src, int srcoff, double[] left, int leftoff, double[] right, int rightoff) { + double x1 = src[srcoff]; + double y1 = src[srcoff + 1]; + double x2 = src[srcoff + 4]; + double y2 = src[srcoff + 5]; + double lx = (x1 + src[srcoff + 2]) / 2.0; + double ly = (y1 + src[srcoff + 3]) / 2.0; + double rx = (src[srcoff + 2] + x2) / 2.0; + double ry = (src[srcoff + 3] + y2) / 2.0; + double mx = (lx + rx) / 2.0; + double my = (ly + ry) / 2.0; + if (left != null) { + left[leftoff] = x1; + left[leftoff + 1] = y1; + left[leftoff + 2] = lx; + left[leftoff + 3] = ly; + left[leftoff + 4] = mx; + left[leftoff + 5] = my; + } + if (right != null) { + right[rightoff] = mx; + right[rightoff + 1] = my; + right[rightoff + 2] = rx; + right[rightoff + 3] = ry; + right[rightoff + 4] = x2; + right[rightoff + 5] = y2; + } + } + + public static int solveQuadratic(double[] eqn) { + return solveQuadratic(eqn, eqn); + } + + /// Solves `eqn[2]*x*x + eqn[1]*x + eqn[0] = 0`, storing the real roots + /// and returning how many there are, or -1 when the equation is a + /// constant. A repeated root is stored twice, except a repeated root of + /// zero, which is stored once. + public static int solveQuadratic(double[] eqn, double[] res) { + double a = eqn[2]; + double b = eqn[1]; + double c = eqn[0]; + if (a == 0.0) { + if (b == 0.0) { + return -1; + } + res[0] = -c / b; + return 1; + } + double d = b * b - 4.0 * a * c; + if (d < 0.0) { + return 0; + } + // add the root to b with matching sign so nothing cancels + double q = -(b + (b < 0.0 ? -Math.sqrt(d) : Math.sqrt(d))) / 2.0; + res[0] = q / a; + if (q == 0.0) { + return 1; + } + res[1] = c / q; + return 2; + } + + @Override + public boolean contains(double x, double y) { + return Outline.of(this).contains(x, y, PathIterator.WIND_EVEN_ODD); + } + + @Override + public boolean contains(Point2D p) { + return contains(p.getX(), p.getY()); + } + + @Override + public boolean intersects(double x, double y, double w, double h) { + return Outline.of(this).intersects(x, y, w, h, PathIterator.WIND_EVEN_ODD); + } + + @Override + public boolean intersects(Rectangle2D r) { + return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public boolean contains(double x, double y, double w, double h) { + return Outline.of(this).contains(x, y, w, h, PathIterator.WIND_EVEN_ODD); + } + + @Override + public boolean contains(Rectangle2D r) { + return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public Rectangle getBounds() { + return getBounds2D().getBounds(); + } + + @Override + public PathIterator getPathIterator(AffineTransform at) { + SegmentIterator it = new SegmentIterator(PathIterator.WIND_NON_ZERO, at, 2); + it.move(getX1(), getY1()); + it.quad(getCtrlX(), getCtrlY(), getX2(), getY2()); + return it; + } + + @Override + public PathIterator getPathIterator(AffineTransform at, double flatness) { + return new FlatteningPathIterator(getPathIterator(at), flatness); + } + + /// Returns a copy of this curve. A subclass from elsewhere that does not + /// override it gets a double precision curve back. + @Override + public Object clone() { + return new Double(getX1(), getY1(), getCtrlX(), getCtrlY(), getX2(), getY2()); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Rectangle2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Rectangle2D.java new file mode 100644 index 00000000000..b91539f71b7 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/Rectangle2D.java @@ -0,0 +1,415 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +/// A rectangle given by a corner and a size, stored by a subclass in the +/// precision it chooses. +/// +/// A point on the left or top edge is inside and one on the right or bottom +/// edge is not; a rectangle with no area contains and intersects nothing. +public abstract class Rectangle2D extends RectangularShape { + + public static final int OUT_LEFT = 1; + public static final int OUT_TOP = 2; + public static final int OUT_RIGHT = 4; + public static final int OUT_BOTTOM = 8; + + /// A rectangle stored in single precision. + public static class Float extends Rectangle2D { + + public float x; + public float y; + public float width; + public float height; + + public Float() { + } + + public Float(float x, float y, float w, float h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public boolean isEmpty() { + return width <= 0.0f || height <= 0.0f; + } + + public void setRect(float x, float y, float w, float h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + } + + @Override + public void setRect(double x, double y, double w, double h) { + this.x = (float) x; + this.y = (float) y; + this.width = (float) w; + this.height = (float) h; + } + + @Override + public void setRect(Rectangle2D r) { + setRect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public int outcode(double x, double y) { + return outcode(this.x, this.y, width, height, x, y); + } + + @Override + public Rectangle2D getBounds2D() { + return new Float(x, y, width, height); + } + + @Override + public Rectangle2D createIntersection(Rectangle2D r) { + Rectangle2D dest = r instanceof Float ? (Rectangle2D) new Float() : new Double(); + Rectangle2D.intersect(this, r, dest); + return dest; + } + + @Override + public Rectangle2D createUnion(Rectangle2D r) { + Rectangle2D dest = r instanceof Float ? (Rectangle2D) new Float() : new Double(); + Rectangle2D.union(this, r, dest); + return dest; + } + + @Override + public Object clone() { + return new Float(x, y, width, height); + } + + @Override + public String toString() { + return "java.awt.geom.Rectangle2D$Float[x=" + x + ",y=" + y + ",w=" + width + ",h=" + height + "]"; + } + } + + /// A rectangle stored in double precision. + public static class Double extends Rectangle2D { + + public double x; + public double y; + public double width; + public double height; + + public Double() { + } + + public Double(double x, double y, double w, double h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public boolean isEmpty() { + return width <= 0.0 || height <= 0.0; + } + + @Override + public void setRect(double x, double y, double w, double h) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + } + + @Override + public void setRect(Rectangle2D r) { + setRect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public int outcode(double x, double y) { + return outcode(this.x, this.y, width, height, x, y); + } + + @Override + public Rectangle2D getBounds2D() { + return new Double(x, y, width, height); + } + + @Override + public Rectangle2D createIntersection(Rectangle2D r) { + Rectangle2D dest = new Double(); + Rectangle2D.intersect(this, r, dest); + return dest; + } + + @Override + public Rectangle2D createUnion(Rectangle2D r) { + Rectangle2D dest = new Double(); + Rectangle2D.union(this, r, dest); + return dest; + } + + @Override + public Object clone() { + return new Double(x, y, width, height); + } + + @Override + public String toString() { + return "java.awt.geom.Rectangle2D$Double[x=" + x + ",y=" + y + ",w=" + width + ",h=" + height + "]"; + } + } + + protected Rectangle2D() { + } + + /// The `OUT_` flags of a point against a rectangle. A rectangle with no + /// extent on an axis puts every point outside on both sides of it. + static int outcode(double rx, double ry, double rw, double rh, double x, double y) { + int out = 0; + if (rw <= 0.0) { + out |= OUT_LEFT | OUT_RIGHT; + } else if (x < rx) { + out |= OUT_LEFT; + } else if (x > rx + rw) { + out |= OUT_RIGHT; + } + if (rh <= 0.0) { + out |= OUT_TOP | OUT_BOTTOM; + } else if (y < ry) { + out |= OUT_TOP; + } else if (y > ry + rh) { + out |= OUT_BOTTOM; + } + return out; + } + + public abstract void setRect(double x, double y, double w, double h); + + public void setRect(Rectangle2D r) { + setRect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + /// Whether a line segment touches this rectangle, edges included. + public boolean intersectsLine(double x1, double y1, double x2, double y2) { + int out2 = outcode(x2, y2); + if (out2 == 0) { + return true; + } + // walk the first end towards the second, one rectangle edge at a + // time, until it is inside or both ends are beyond the same edge + int out1 = outcode(x1, y1); + while (out1 != 0) { + if ((out1 & out2) != 0) { + return false; + } + if ((out1 & (OUT_LEFT | OUT_RIGHT)) != 0) { + double x = (out1 & OUT_RIGHT) != 0 ? getX() + getWidth() : getX(); + y1 = y1 + (x - x1) * (y2 - y1) / (x2 - x1); + x1 = x; + } else { + double y = (out1 & OUT_BOTTOM) != 0 ? getY() + getHeight() : getY(); + x1 = x1 + (y - y1) * (x2 - x1) / (y2 - y1); + y1 = y; + } + out1 = outcode(x1, y1); + } + return true; + } + + public boolean intersectsLine(Line2D l) { + return intersectsLine(l.getX1(), l.getY1(), l.getX2(), l.getY2()); + } + + public abstract int outcode(double x, double y); + + public int outcode(Point2D p) { + return outcode(p.getX(), p.getY()); + } + + @Override + public void setFrame(double x, double y, double w, double h) { + setRect(x, y, w, h); + } + + @Override + public Rectangle2D getBounds2D() { + return new Double(getX(), getY(), getWidth(), getHeight()); + } + + @Override + public boolean contains(double x, double y) { + double x0 = getX(); + double y0 = getY(); + return x >= x0 && y >= y0 && x < x0 + getWidth() && y < y0 + getHeight(); + } + + @Override + public boolean intersects(double x, double y, double w, double h) { + if (isEmpty() || w <= 0.0 || h <= 0.0) { + return false; + } + double x0 = getX(); + double y0 = getY(); + return x + w > x0 && y + h > y0 && x < x0 + getWidth() && y < y0 + getHeight(); + } + + @Override + public boolean contains(double x, double y, double w, double h) { + if (isEmpty() || w <= 0.0 || h <= 0.0) { + return false; + } + double x0 = getX(); + double y0 = getY(); + return x >= x0 && y >= y0 && x + w <= x0 + getWidth() && y + h <= y0 + getHeight(); + } + + public abstract Rectangle2D createIntersection(Rectangle2D r); + + public abstract Rectangle2D createUnion(Rectangle2D r); + + /// Stores the overlap of two rectangles. Rectangles that do not overlap + /// give a result with a negative width or height. + public static void intersect(Rectangle2D src1, Rectangle2D src2, Rectangle2D dest) { + double x1 = Math.max(src1.getMinX(), src2.getMinX()); + double y1 = Math.max(src1.getMinY(), src2.getMinY()); + double x2 = Math.min(src1.getMaxX(), src2.getMaxX()); + double y2 = Math.min(src1.getMaxY(), src2.getMaxY()); + dest.setFrame(x1, y1, x2 - x1, y2 - y1); + } + + public static void union(Rectangle2D src1, Rectangle2D src2, Rectangle2D dest) { + double x1 = Math.min(src1.getMinX(), src2.getMinX()); + double y1 = Math.min(src1.getMinY(), src2.getMinY()); + double x2 = Math.max(src1.getMaxX(), src2.getMaxX()); + double y2 = Math.max(src1.getMaxY(), src2.getMaxY()); + dest.setFrameFromDiagonal(x1, y1, x2, y2); + } + + public void add(double newx, double newy) { + double x1 = Math.min(getMinX(), newx); + double x2 = Math.max(getMaxX(), newx); + double y1 = Math.min(getMinY(), newy); + double y2 = Math.max(getMaxY(), newy); + setRect(x1, y1, x2 - x1, y2 - y1); + } + + public void add(Point2D pt) { + add(pt.getX(), pt.getY()); + } + + public void add(Rectangle2D r) { + double x1 = Math.min(getMinX(), r.getMinX()); + double x2 = Math.max(getMaxX(), r.getMaxX()); + double y1 = Math.min(getMinY(), r.getMinY()); + double y2 = Math.max(getMaxY(), r.getMaxY()); + setRect(x1, y1, x2 - x1, y2 - y1); + } + + @Override + public PathIterator getPathIterator(AffineTransform at) { + SegmentIterator it = new SegmentIterator(PathIterator.WIND_NON_ZERO, at, 6); + double x = getX(); + double y = getY(); + double w = getWidth(); + double h = getHeight(); + it.move(x, y); + it.line(x + w, y); + it.line(x + w, y + h); + it.line(x, y + h); + it.line(x, y); + it.close(); + return it; + } + + /// A rectangle is already flat, so this is the plain outline. + @Override + public PathIterator getPathIterator(AffineTransform at, double flatness) { + return getPathIterator(at); + } + + @Override + public int hashCode() { + long bits = java.lang.Double.doubleToLongBits(getX()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getY()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getWidth()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getHeight()); + return (int) bits ^ (int) (bits >> 32); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Rectangle2D) { + Rectangle2D r = (Rectangle2D) obj; + return getX() == r.getX() && getY() == r.getY() + && getWidth() == r.getWidth() && getHeight() == r.getHeight(); + } + return false; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/RectangularShape.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/RectangularShape.java new file mode 100644 index 00000000000..b91a381e2a8 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/RectangularShape.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import com.codename1.desktopcompat.java.awt.Rectangle; +import com.codename1.desktopcompat.java.awt.Shape; + +/// The base of every shape defined by a rectangular frame: rectangles, +/// rounded rectangles, ellipses and arcs. +public abstract class RectangularShape implements Shape, Cloneable { + + protected RectangularShape() { + } + + public abstract double getX(); + + public abstract double getY(); + + public abstract double getWidth(); + + public abstract double getHeight(); + + public double getMinX() { + return getX(); + } + + public double getMinY() { + return getY(); + } + + public double getMaxX() { + return getX() + getWidth(); + } + + public double getMaxY() { + return getY() + getHeight(); + } + + public double getCenterX() { + return getX() + getWidth() / 2.0; + } + + public double getCenterY() { + return getY() + getHeight() / 2.0; + } + + public Rectangle2D getFrame() { + return new Rectangle2D.Double(getX(), getY(), getWidth(), getHeight()); + } + + public abstract boolean isEmpty(); + + public abstract void setFrame(double x, double y, double w, double h); + + public void setFrame(Point2D loc, Dimension2D size) { + setFrame(loc.getX(), loc.getY(), size.getWidth(), size.getHeight()); + } + + public void setFrame(Rectangle2D r) { + setFrame(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + public void setFrameFromDiagonal(double x1, double y1, double x2, double y2) { + setFrame(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1)); + } + + public void setFrameFromDiagonal(Point2D p1, Point2D p2) { + setFrameFromDiagonal(p1.getX(), p1.getY(), p2.getX(), p2.getY()); + } + + public void setFrameFromCenter(double centerX, double centerY, double cornerX, double cornerY) { + double halfW = Math.abs(cornerX - centerX); + double halfH = Math.abs(cornerY - centerY); + setFrame(centerX - halfW, centerY - halfH, halfW * 2.0, halfH * 2.0); + } + + public void setFrameFromCenter(Point2D center, Point2D corner) { + setFrameFromCenter(center.getX(), center.getY(), corner.getX(), corner.getY()); + } + + @Override + public boolean contains(Point2D p) { + return contains(p.getX(), p.getY()); + } + + @Override + public boolean intersects(Rectangle2D r) { + return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + @Override + public boolean contains(Rectangle2D r) { + return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + + /// The smallest integer rectangle enclosing the frame, or an empty one at + /// the origin when the frame has a negative size. + @Override + public Rectangle getBounds() { + double w = getWidth(); + double h = getHeight(); + if (w < 0.0 || h < 0.0) { + return new Rectangle(); + } + double x1 = Math.floor(getX()); + double y1 = Math.floor(getY()); + double x2 = Math.ceil(getX() + w); + double y2 = Math.ceil(getY() + h); + return new Rectangle((int) x1, (int) y1, (int) (x2 - x1), (int) (y2 - y1)); + } + + @Override + public PathIterator getPathIterator(AffineTransform at, double flatness) { + return new FlatteningPathIterator(getPathIterator(at), flatness); + } + + /// Returns a copy of this shape. Every shape of this package returns its + /// own type; a subclass from elsewhere that does not override it gets a + /// rectangle with the same frame. + @Override + public Object clone() { + return new Rectangle2D.Double(getX(), getY(), getWidth(), getHeight()); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/RoundRectangle2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/RoundRectangle2D.java new file mode 100644 index 00000000000..cefe8241b5b --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/RoundRectangle2D.java @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +/// A rectangle whose corners are quarter ellipses of a given width and +/// height. +/// +/// The arc size is the full width and height of the ellipse a corner is cut +/// from, and is limited to the size of the rectangle. The outline starts on +/// the left side below the top-left corner and runs through the bottom, +/// right and top. +public abstract class RoundRectangle2D extends RectangularShape { + + /// How far a corner's control points sit from the corner of the frame, + /// as a fraction of the corner radius. + private static final double CORNER = 1.0 - Ellipse2D.KAPPA; + + /// A rounded rectangle stored in single precision. + public static class Float extends RoundRectangle2D { + + public float x; + public float y; + public float width; + public float height; + public float arcwidth; + public float archeight; + + public Float() { + } + + public Float(float x, float y, float w, float h, float arcw, float arch) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + this.arcwidth = arcw; + this.archeight = arch; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public double getArcWidth() { + return arcwidth; + } + + @Override + public double getArcHeight() { + return archeight; + } + + @Override + public boolean isEmpty() { + return width <= 0.0f || height <= 0.0f; + } + + public void setRoundRect(float x, float y, float w, float h, float arcw, float arch) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + this.arcwidth = arcw; + this.archeight = arch; + } + + @Override + public void setRoundRect(double x, double y, double w, double h, double arcw, double arch) { + this.x = (float) x; + this.y = (float) y; + this.width = (float) w; + this.height = (float) h; + this.arcwidth = (float) arcw; + this.archeight = (float) arch; + } + + @Override + public void setRoundRect(RoundRectangle2D rr) { + setRoundRect(rr.getX(), rr.getY(), rr.getWidth(), rr.getHeight(), rr.getArcWidth(), rr.getArcHeight()); + } + + @Override + public Rectangle2D getBounds2D() { + return new Rectangle2D.Float(x, y, width, height); + } + + @Override + public Object clone() { + return new Float(x, y, width, height, arcwidth, archeight); + } + } + + /// A rounded rectangle stored in double precision. + public static class Double extends RoundRectangle2D { + + public double x; + public double y; + public double width; + public double height; + public double arcwidth; + public double archeight; + + public Double() { + } + + public Double(double x, double y, double w, double h, double arcw, double arch) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + this.arcwidth = arcw; + this.archeight = arch; + } + + @Override + public double getX() { + return x; + } + + @Override + public double getY() { + return y; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public double getArcWidth() { + return arcwidth; + } + + @Override + public double getArcHeight() { + return archeight; + } + + @Override + public boolean isEmpty() { + return width <= 0.0 || height <= 0.0; + } + + @Override + public void setRoundRect(double x, double y, double w, double h, double arcw, double arch) { + this.x = x; + this.y = y; + this.width = w; + this.height = h; + this.arcwidth = arcw; + this.archeight = arch; + } + + @Override + public void setRoundRect(RoundRectangle2D rr) { + setRoundRect(rr.getX(), rr.getY(), rr.getWidth(), rr.getHeight(), rr.getArcWidth(), rr.getArcHeight()); + } + + @Override + public Rectangle2D getBounds2D() { + return new Rectangle2D.Double(x, y, width, height); + } + + @Override + public Object clone() { + return new Double(x, y, width, height, arcwidth, archeight); + } + } + + protected RoundRectangle2D() { + } + + public abstract double getArcWidth(); + + public abstract double getArcHeight(); + + public abstract void setRoundRect(double x, double y, double w, double h, double arcWidth, double arcHeight); + + public void setRoundRect(RoundRectangle2D rr) { + setRoundRect(rr.getX(), rr.getY(), rr.getWidth(), rr.getHeight(), rr.getArcWidth(), rr.getArcHeight()); + } + + /// Moves and resizes the frame, keeping the arc size. + @Override + public void setFrame(double x, double y, double w, double h) { + setRoundRect(x, y, w, h, getArcWidth(), getArcHeight()); + } + + /// The horizontal corner radius actually used. + private double radiusX() { + return Math.min(getWidth(), Math.abs(getArcWidth())) / 2.0; + } + + /// The vertical corner radius actually used. + private double radiusY() { + return Math.min(getHeight(), Math.abs(getArcHeight())) / 2.0; + } + + /// How far outside the corner ellipses the nearest point of a box lies, + /// where the box overlaps the frame: below one it reaches the shape. + /// + /// The shape is every point within the corner ellipse of the core + /// rectangle left when the frame is inset by the radii, so the measure + /// is the box's gap to that core on each axis, scaled by the radius. + private double cornerMeasure(double x0, double y0, double x1, double y1) { + double rx = radiusX(); + double ry = radiusY(); + if (rx <= 0.0 || ry <= 0.0) { + return 0.0; + } + double coreX0 = getX() + rx; + double coreX1 = getX() + getWidth() - rx; + double coreY0 = getY() + ry; + double coreY1 = getY() + getHeight() - ry; + double gapX = Math.max(0.0, Math.max(coreX0 - x1, x0 - coreX1)) / rx; + double gapY = Math.max(0.0, Math.max(coreY0 - y1, y0 - coreY1)) / ry; + return gapX * gapX + gapY * gapY; + } + + @Override + public boolean contains(double x, double y) { + if (isEmpty()) { + return false; + } + double x0 = getX(); + double y0 = getY(); + if (x < x0 || y < y0 || x >= x0 + getWidth() || y >= y0 + getHeight()) { + return false; + } + return cornerMeasure(x, y, x, y) <= 1.0; + } + + @Override + public boolean intersects(double x, double y, double w, double h) { + if (isEmpty() || w <= 0.0 || h <= 0.0) { + return false; + } + double x0 = getX(); + double y0 = getY(); + if (x + w <= x0 || y + h <= y0 || x >= x0 + getWidth() || y >= y0 + getHeight()) { + return false; + } + return cornerMeasure(x, y, x + w, y + h) < 1.0; + } + + @Override + public boolean contains(double x, double y, double w, double h) { + if (isEmpty() || w <= 0.0 || h <= 0.0) { + return false; + } + // the shape is convex, so the corners decide + return contains(x, y) && contains(x + w, y) && contains(x, y + h) && contains(x + w, y + h); + } + + @Override + public PathIterator getPathIterator(AffineTransform at) { + SegmentIterator it = new SegmentIterator(PathIterator.WIND_NON_ZERO, at, 10); + double x0 = getX(); + double y0 = getY(); + double x1 = x0 + getWidth(); + double y1 = y0 + getHeight(); + double rx = radiusX(); + double ry = radiusY(); + double cx = rx * CORNER; + double cy = ry * CORNER; + it.move(x0, y0 + ry); + it.line(x0, y1 - ry); + it.cubic(x0, y1 - cy, x0 + cx, y1, x0 + rx, y1); + it.line(x1 - rx, y1); + it.cubic(x1 - cx, y1, x1, y1 - cy, x1, y1 - ry); + it.line(x1, y0 + ry); + it.cubic(x1, y0 + cy, x1 - cx, y0, x1 - rx, y0); + it.line(x0 + rx, y0); + it.cubic(x0 + cx, y0, x0, y0 + cy, x0, y0 + ry); + it.close(); + return it; + } + + @Override + public int hashCode() { + long bits = java.lang.Double.doubleToLongBits(getX()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getY()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getWidth()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getHeight()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getArcWidth()); + bits = bits * 31 + java.lang.Double.doubleToLongBits(getArcHeight()); + return (int) bits ^ (int) (bits >> 32); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof RoundRectangle2D) { + RoundRectangle2D r = (RoundRectangle2D) obj; + return getX() == r.getX() && getY() == r.getY() + && getWidth() == r.getWidth() && getHeight() == r.getHeight() + && getArcWidth() == r.getArcWidth() && getArcHeight() == r.getArcHeight(); + } + return false; + } + + /// Returns a copy of this shape. A subclass from elsewhere that does not + /// override it gets a double precision rounded rectangle back. + @Override + public Object clone() { + return new Double(getX(), getY(), getWidth(), getHeight(), getArcWidth(), getArcHeight()); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/SegmentIterator.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/SegmentIterator.java new file mode 100644 index 00000000000..6ed247e964c --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/SegmentIterator.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import java.util.NoSuchElementException; + +/// The one path iterator every shape of the package hands out: a list of +/// segment types with their coordinates, mapped through an optional +/// transform as they are read. +/// +/// A shape either fills one through `move`, `line`, `quad`, `cubic` and +/// `close` before returning it, or wraps the arrays it already keeps. +final class SegmentIterator implements PathIterator { + + private byte[] types; + private double[] coords; + private int count; + private int coordCount; + private final int rule; + private final AffineTransform at; + private int index; + private int coordIndex; + + /// An empty iterator with room for the given number of segments. + SegmentIterator(int rule, AffineTransform at, int segments) { + this.rule = rule; + this.at = at; + this.types = new byte[segments]; + this.coords = new double[segments * 6]; + } + + /// An iterator over segments a path already holds. + SegmentIterator(int rule, AffineTransform at, byte[] types, int count, double[] coords) { + this.rule = rule; + this.at = at; + this.types = types; + this.count = count; + this.coords = coords; + } + + /// The number of coordinates a segment of the given type carries. + static int size(int type) { + switch (type) { + case SEG_MOVETO: + case SEG_LINETO: + return 2; + case SEG_QUADTO: + return 4; + case SEG_CUBICTO: + return 6; + default: + return 0; + } + } + + private void add(int type) { + if (count == types.length) { + byte[] t = new byte[count * 2 + 4]; + System.arraycopy(types, 0, t, 0, count); + types = t; + } + if (coordCount + 6 > coords.length) { + double[] c = new double[coords.length * 2 + 12]; + System.arraycopy(coords, 0, c, 0, coordCount); + coords = c; + } + types[count++] = (byte) type; + } + + void move(double x, double y) { + add(SEG_MOVETO); + coords[coordCount++] = x; + coords[coordCount++] = y; + } + + void line(double x, double y) { + add(SEG_LINETO); + coords[coordCount++] = x; + coords[coordCount++] = y; + } + + void quad(double cx, double cy, double x, double y) { + add(SEG_QUADTO); + coords[coordCount++] = cx; + coords[coordCount++] = cy; + coords[coordCount++] = x; + coords[coordCount++] = y; + } + + void cubic(double cx1, double cy1, double cx2, double cy2, double x, double y) { + add(SEG_CUBICTO); + coords[coordCount++] = cx1; + coords[coordCount++] = cy1; + coords[coordCount++] = cx2; + coords[coordCount++] = cy2; + coords[coordCount++] = x; + coords[coordCount++] = y; + } + + void close() { + add(SEG_CLOSE); + } + + @Override + public int getWindingRule() { + return rule; + } + + @Override + public boolean isDone() { + return index >= count; + } + + @Override + public void next() { + if (index < count) { + coordIndex += size(types[index]); + index++; + } + } + + @Override + public int currentSegment(float[] out) { + if (index >= count) { + throw new NoSuchElementException("path iterator out of bounds"); + } + int type = types[index]; + int n = size(type); + if (n > 0) { + if (at != null) { + at.transform(coords, coordIndex, out, 0, n / 2); + } else { + for (int i = 0; i < n; i++) { + out[i] = (float) coords[coordIndex + i]; + } + } + } + return type; + } + + @Override + public int currentSegment(double[] out) { + if (index >= count) { + throw new NoSuchElementException("path iterator out of bounds"); + } + int type = types[index]; + int n = size(type); + if (n > 0) { + if (at != null) { + at.transform(coords, coordIndex, out, 0, n / 2); + } else { + System.arraycopy(coords, coordIndex, out, 0, n); + } + } + return type; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/package-info.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/package-info.java new file mode 100644 index 00000000000..46fa0198212 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/geom/package-info.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Two dimensional geometry for the desktop compatibility layer: points, +/// rectangles, lines, ellipses, arcs, Bezier curves, general paths and the +/// affine transform that maps between coordinate spaces. +/// +/// These classes stand in for the JDK's `java.awt.geom` package once an +/// application has been relocated onto the layer. They are plain data and +/// arithmetic with no dependency on a toolkit, and are written against the +/// small class library a device has: trigonometry beyond sine, cosine and +/// tangent goes through `com.codename1.util.MathUtil`, and every `clone()` +/// builds its copy explicitly. +/// +/// Curved outlines are answered through [PathIterator] as cubic Bezier +/// segments. Hit testing of a [Path2D], a [QuadCurve2D], a [CubicCurve2D] and +/// of an [Arc2D] against a rectangle is done over the flattened outline, so +/// it is exact for polygons and accurate to a small fraction of the shape's +/// size for curves. +package com.codename1.desktopcompat.java.awt.geom; diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/BufferedImage.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/BufferedImage.java new file mode 100644 index 00000000000..2ff276e1a8f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/BufferedImage.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.image; + +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Graphics2D; +import com.codename1.desktopcompat.java.awt.Image; +import com.codename1.desktopcompat.java.awt.Transparency; +import com.codename1.desktopcompat.rt.G2D; + +/// An image whose pixels are an array of ARGB integers. +/// +/// Whatever type it is created with, it stores one `int` per pixel; the +/// type only decides whether the alpha channel is kept (`TYPE_INT_ARGB` and +/// the other alpha types) or every pixel is opaque. Drawing into it goes +/// through a Codename One mutable image that is created the first time a +/// graphics is asked for; pixels move between that image and the array only +/// when the other side has changed and is then read, so a program that only +/// calls `setRGB`, or only draws, never pays for the copy. There are no +/// rasters, color models or sample models. +public class BufferedImage extends Image implements Transparency { + + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_INT_RGB = 1; + public static final int TYPE_INT_ARGB = 2; + public static final int TYPE_INT_ARGB_PRE = 3; + public static final int TYPE_INT_BGR = 4; + public static final int TYPE_3BYTE_BGR = 5; + public static final int TYPE_4BYTE_ABGR = 6; + public static final int TYPE_4BYTE_ABGR_PRE = 7; + public static final int TYPE_USHORT_565_RGB = 8; + public static final int TYPE_USHORT_555_RGB = 9; + public static final int TYPE_BYTE_GRAY = 10; + public static final int TYPE_USHORT_GRAY = 11; + public static final int TYPE_BYTE_BINARY = 12; + public static final int TYPE_BYTE_INDEXED = 13; + + private final int width; + private final int height; + private final int type; + private final boolean alpha; + private final int[] pixels; + /// The mutable image drawing goes into, once a graphics was asked for. + private com.codename1.ui.Image surface; + /// An immutable image of [#pixels], for an image nobody draws into. + private com.codename1.ui.Image snapshot; + private boolean surfaceNewer; + private boolean pixelsNewer; + + public BufferedImage(int width, int height, int imageType) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Width (" + width + ") and height (" + height + ") cannot be <= 0"); + } + this.width = width; + this.height = height; + this.type = imageType; + this.alpha = imageType == TYPE_INT_ARGB || imageType == TYPE_INT_ARGB_PRE || imageType == TYPE_4BYTE_ABGR + || imageType == TYPE_4BYTE_ABGR_PRE || imageType == TYPE_CUSTOM; + this.pixels = new int[width * height]; + if (!alpha) { + for (int i = 0; i < pixels.length; i++) { + pixels[i] = 0xff000000; + } + } + } + + public int getType() { + return type; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + @Override + public int getWidth(ImageObserver observer) { + return width; + } + + @Override + public int getHeight(ImageObserver observer) { + return height; + } + + private void pull() { + if (surfaceNewer && surface != null) { + surfaceNewer = false; + int[] rgb = surface.getRGB(); + if (rgb != null && rgb.length == pixels.length) { + System.arraycopy(rgb, 0, pixels, 0, rgb.length); + } + if (!alpha) { + for (int i = 0; i < pixels.length; i++) { + pixels[i] |= 0xff000000; + } + } + } + } + + private void push() { + if (pixelsNewer && surface != null) { + pixelsNewer = false; + com.codename1.ui.Graphics g = surface.getGraphics(); + g.clearRect(0, 0, width, height); + g.drawImage(com.codename1.ui.Image.createImage(pixels, width, height), 0, 0); + } + } + + private void check(int x, int y) { + if (x < 0 || y < 0 || x >= width || y >= height) { + throw new ArrayIndexOutOfBoundsException("Coordinate out of bounds!"); + } + } + + public int getRGB(int x, int y) { + check(x, y); + pull(); + return pixels[y * width + x]; + } + + public int[] getRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) { + int[] out = rgbArray == null ? new int[offset + h * scansize] : rgbArray; + if (w <= 0 || h <= 0) { + return out; + } + check(startX, startY); + check(startX + w - 1, startY + h - 1); + pull(); + for (int row = 0; row < h; row++) { + System.arraycopy(pixels, (startY + row) * width + startX, out, offset + row * scansize, w); + } + return out; + } + + public void setRGB(int x, int y, int rgb) { + check(x, y); + pull(); + pixels[y * width + x] = alpha ? rgb : rgb | 0xff000000; + pixelsNewer = true; + snapshot = null; + } + + public void setRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) { + if (w <= 0 || h <= 0) { + return; + } + check(startX, startY); + check(startX + w - 1, startY + h - 1); + pull(); + for (int row = 0; row < h; row++) { + int to = (startY + row) * width + startX; + int from = offset + row * scansize; + for (int col = 0; col < w; col++) { + pixels[to + col] = alpha ? rgbArray[from + col] : rgbArray[from + col] | 0xff000000; + } + } + pixelsNewer = true; + snapshot = null; + } + + @Override + public Graphics getGraphics() { + return createGraphics(); + } + + public Graphics2D createGraphics() { + if (surface == null) { + surface = com.codename1.ui.Image.createImage(width, height, alpha ? 0 : 0xff000000); + pixelsNewer = true; + snapshot = null; + } + push(); + return G2D.forImage(this, surface.getGraphics(), width, height); + } + + /// Called by a graphics of this image before it draws: the surface now + /// holds pixels the array does not. + public void cn1Drawing() { + push(); + surfaceNewer = true; + } + + @Override + public com.codename1.ui.Image cn1Image() { + if (surface != null) { + push(); + return surface; + } + if (snapshot == null) { + snapshot = com.codename1.ui.Image.createImage(pixels, width, height); + } + return snapshot; + } + + /// A copy of the region. The JDK's subimage shares pixels with its + /// parent; this one does not. + public BufferedImage getSubimage(int x, int y, int w, int h) { + BufferedImage out = new BufferedImage(w, h, type); + getRGB(x, y, w, h, out.pixels, 0, w); + out.pixelsNewer = true; + return out; + } + + public boolean isAlphaPremultiplied() { + return false; + } + + @Override + public int getTransparency() { + return alpha ? TRANSLUCENT : OPAQUE; + } + + @Override + public void flush() { + snapshot = null; + } + + @Override + public String toString() { + return "BufferedImage@" + Integer.toHexString(hashCode()) + ": type = " + type + " " + width + "x" + height; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/ImageObserver.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/ImageObserver.java new file mode 100644 index 00000000000..2bd855f8d35 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/ImageObserver.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.image; + +import com.codename1.desktopcompat.java.awt.Image; + +/// Told as an image loads. Images of the layer are complete as soon as they +/// exist, so an observer is accepted wherever the API takes one and never +/// called. +public interface ImageObserver { + int WIDTH = 1; + int HEIGHT = 2; + int PROPERTIES = 4; + int SOMEBITS = 8; + int FRAMEBITS = 16; + int ALLBITS = 32; + int ERROR = 64; + int ABORT = 128; + + boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/package-info.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/package-info.java new file mode 100644 index 00000000000..32cd8c377d2 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/image/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Images whose pixels a program can read and write. `BufferedImage` keeps +/// its pixels as ARGB integers and hands them to Codename One when drawn. +package com.codename1.desktopcompat.java.awt.image; diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/package-info.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/package-info.java new file mode 100644 index 00000000000..512cca33da9 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/awt/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The AWT core: components and containers, graphics, colors, fonts, +/// layout managers and windows, written over Codename One. +/// +/// Coordinates are logical pixels of 1/96 inch, whatever the density of the +/// display. Every component is shown through one Codename One component, +/// its peer; see [com.codename1.desktopcompat.java.awt.Component]. +package com.codename1.desktopcompat.java.awt; diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/IndexedPropertyChangeEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/IndexedPropertyChangeEvent.java new file mode 100644 index 00000000000..70df5676d5b --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/IndexedPropertyChangeEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.beans; + +/// A bound property changed at one index of an indexed property. +public class IndexedPropertyChangeEvent extends PropertyChangeEvent { + + private final int index; + + public IndexedPropertyChangeEvent(Object source, String propertyName, Object oldValue, Object newValue, + int index) { + super(source, propertyName, oldValue, newValue); + this.index = index; + } + + public int getIndex() { + return index; + } + + @Override + void appendTo(StringBuilder sb) { + sb.append("; index=").append(index); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeEvent.java new file mode 100644 index 00000000000..495403acf79 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeEvent.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.beans; + +import java.util.EventObject; + +/// A bound property changed. +/// +/// A `null` property name means that several properties changed, and a +/// `null` old or new value means that the value is unknown. +public class PropertyChangeEvent extends EventObject { + + private final String propertyName; + + private final Object newValue; + + private final Object oldValue; + + private Object propagationId; + + public PropertyChangeEvent(Object source, String propertyName, Object oldValue, Object newValue) { + super(source); + this.propertyName = propertyName; + this.newValue = newValue; + this.oldValue = oldValue; + } + + public String getPropertyName() { + return propertyName; + } + + public Object getNewValue() { + return newValue; + } + + public Object getOldValue() { + return oldValue; + } + + public void setPropagationId(Object propagationId) { + this.propagationId = propagationId; + } + + public Object getPropagationId() { + return propagationId; + } + + /// Lets a subclass add its own fields to the text of `toString()`. + void appendTo(StringBuilder sb) { + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getName()); + sb.append("[propertyName=").append(propertyName); + appendTo(sb); + sb.append("; oldValue=").append(oldValue); + sb.append("; newValue=").append(newValue); + sb.append("; propagationId=").append(propagationId); + sb.append("; source=").append(getSource()); + return sb.append(']').toString(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeListener.java new file mode 100644 index 00000000000..fcd508884e9 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.beans; + +import java.util.EventListener; + +/// Receives the changes of a bound property. +public interface PropertyChangeListener extends EventListener { + + void propertyChange(PropertyChangeEvent evt); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeListenerProxy.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeListenerProxy.java new file mode 100644 index 00000000000..ea5fc99d930 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeListenerProxy.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.beans; + +import java.util.EventListenerProxy; + +/// A listener bound to one property name, wrapping the listener that +/// receives that property's changes. +/// +/// The device class library's `EventListenerProxy` is not generic, so this +/// extends it as a raw type and `getListener()` answers the wrapped listener +/// as a plain `EventListener`. +public class PropertyChangeListenerProxy extends EventListenerProxy implements PropertyChangeListener { + + private final String propertyName; + + public PropertyChangeListenerProxy(String propertyName, PropertyChangeListener listener) { + super(listener); + this.propertyName = propertyName; + } + + public void propertyChange(PropertyChangeEvent evt) { + PropertyChangeListener l = (PropertyChangeListener) getListener(); + l.propertyChange(evt); + } + + public String getPropertyName() { + return propertyName; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeSupport.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeSupport.java new file mode 100644 index 00000000000..b72ba1f2aee --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyChangeSupport.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.beans; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/// Keeps the listeners of a bean's bound properties and delivers events to +/// them. +/// +/// Listeners added with a property name receive only that property's events; +/// the others receive all. A `PropertyChangeListenerProxy` passed to +/// `addPropertyChangeListener(PropertyChangeListener)` is registered under +/// the name it carries. No event is fired when the old and new values are +/// both non-null and equal. The user interface is single threaded, so unlike +/// the JDK class this does not synchronize. +public class PropertyChangeSupport { + + private final Object source; + + private final ArrayList common = new ArrayList(); + + private final HashMap> named = + new HashMap>(); + + public PropertyChangeSupport(Object sourceBean) { + if (sourceBean == null) { + throw new NullPointerException(); + } + source = sourceBean; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + if (listener == null) { + return; + } + if (listener instanceof PropertyChangeListenerProxy) { + PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) listener; + Object wrapped = proxy.getListener(); + if (wrapped instanceof PropertyChangeListener) { + addPropertyChangeListener(proxy.getPropertyName(), (PropertyChangeListener) wrapped); + } + } else { + common.add(listener); + } + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + if (listener == null) { + return; + } + if (listener instanceof PropertyChangeListenerProxy) { + PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) listener; + Object wrapped = proxy.getListener(); + if (wrapped instanceof PropertyChangeListener) { + removePropertyChangeListener(proxy.getPropertyName(), (PropertyChangeListener) wrapped); + } + } else { + common.remove(listener); + } + } + + public PropertyChangeListener[] getPropertyChangeListeners() { + ArrayList all = new ArrayList(common); + for (Map.Entry> e : named.entrySet()) { + for (PropertyChangeListener l : e.getValue()) { + all.add(new PropertyChangeListenerProxy(e.getKey(), l)); + } + } + return all.toArray(new PropertyChangeListener[all.size()]); + } + + public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { + if (listener == null || propertyName == null) { + return; + } + if (listener instanceof PropertyChangeListenerProxy) { + addPropertyChangeListener(listener); + return; + } + ArrayList list = named.get(propertyName); + if (list == null) { + list = new ArrayList(); + named.put(propertyName, list); + } + list.add(listener); + } + + public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { + if (listener == null || propertyName == null) { + return; + } + if (listener instanceof PropertyChangeListenerProxy) { + removePropertyChangeListener(listener); + return; + } + ArrayList list = named.get(propertyName); + if (list != null) { + list.remove(listener); + if (list.isEmpty()) { + named.remove(propertyName); + } + } + } + + public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) { + ArrayList list = propertyName == null ? null : named.get(propertyName); + if (list == null) { + return new PropertyChangeListener[0]; + } + return list.toArray(new PropertyChangeListener[list.size()]); + } + + public void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + if (oldValue == null || newValue == null || !oldValue.equals(newValue)) { + firePropertyChange(new PropertyChangeEvent(source, propertyName, oldValue, newValue)); + } + } + + public void firePropertyChange(String propertyName, int oldValue, int newValue) { + if (oldValue != newValue) { + firePropertyChange(propertyName, Integer.valueOf(oldValue), Integer.valueOf(newValue)); + } + } + + public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { + if (oldValue != newValue) { + firePropertyChange(propertyName, Boolean.valueOf(oldValue), Boolean.valueOf(newValue)); + } + } + + public void firePropertyChange(PropertyChangeEvent event) { + Object oldValue = event.getOldValue(); + Object newValue = event.getNewValue(); + if (oldValue != null && newValue != null && oldValue.equals(newValue)) { + return; + } + String name = event.getPropertyName(); + PropertyChangeListener[] first = common.toArray(new PropertyChangeListener[common.size()]); + for (int i = 0; i < first.length; i++) { + first[i].propertyChange(event); + } + if (name != null) { + PropertyChangeListener[] second = getPropertyChangeListeners(name); + for (int i = 0; i < second.length; i++) { + second[i].propertyChange(event); + } + } + } + + public void fireIndexedPropertyChange(String propertyName, int index, Object oldValue, Object newValue) { + if (oldValue == null || newValue == null || !oldValue.equals(newValue)) { + firePropertyChange(new IndexedPropertyChangeEvent(source, propertyName, oldValue, newValue, index)); + } + } + + public void fireIndexedPropertyChange(String propertyName, int index, int oldValue, int newValue) { + if (oldValue != newValue) { + fireIndexedPropertyChange(propertyName, index, Integer.valueOf(oldValue), Integer.valueOf(newValue)); + } + } + + public void fireIndexedPropertyChange(String propertyName, int index, boolean oldValue, + boolean newValue) { + if (oldValue != newValue) { + fireIndexedPropertyChange(propertyName, index, Boolean.valueOf(oldValue), + Boolean.valueOf(newValue)); + } + } + + public boolean hasListeners(String propertyName) { + if (!common.isEmpty()) { + return true; + } + if (propertyName == null) { + return false; + } + ArrayList list = named.get(propertyName); + return list != null && !list.isEmpty(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyVetoException.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyVetoException.java new file mode 100644 index 00000000000..219692c1920 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/PropertyVetoException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.beans; + +/// Thrown when a listener vetoes a change of a constrained property. +public class PropertyVetoException extends Exception { + + private final PropertyChangeEvent evt; + + public PropertyVetoException(String mess, PropertyChangeEvent evt) { + super(mess); + this.evt = evt; + } + + public PropertyChangeEvent getPropertyChangeEvent() { + return evt; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/VetoableChangeListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/VetoableChangeListener.java new file mode 100644 index 00000000000..ec7e50ab259 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/VetoableChangeListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.beans; + +import java.util.EventListener; + +/// Receives the changes of a constrained property, and may veto them. +public interface VetoableChangeListener extends EventListener { + + void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException; +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/package-info.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/package-info.java new file mode 100644 index 00000000000..5ed3a9b1184 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/java/beans/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The bound-property subset of `java.beans`: property change events, their +/// listeners, the proxy that binds a listener to one property name, the +/// support class that fires them, and the veto exception. +/// +/// Introspection (`Introspector`, `BeanInfo`) and the persistence classes are +/// not provided, and the support class does not synchronize. +package com.codename1.desktopcompat.java.beans; diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/AbstractAction.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/AbstractAction.java new file mode 100644 index 00000000000..879cbcdee7d --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/AbstractAction.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.beans.PropertyChangeListener; +import com.codename1.desktopcompat.javax.swing.event.SwingPropertyChangeSupport; + +import java.util.HashMap; + +/// A base for `Action` implementations that keeps the property map, the +/// enabled state and the property change listeners. +/// +/// Changing a property or the enabled state fires a property change event +/// named after the key (`"enabled"` for the enabled state). `clone()` of the +/// JDK class is not provided, because cloning does not work on the device. +public abstract class AbstractAction implements Action { + + /// Whether the action is enabled. + protected boolean enabled = true; + + /// The listeners of the action, created on demand. + protected SwingPropertyChangeSupport changeSupport; + + private final HashMap values = new HashMap(); + + public AbstractAction() { + } + + public AbstractAction(String name) { + putValue(Action.NAME, name); + } + + public AbstractAction(String name, Icon icon) { + this(name); + putValue(Action.SMALL_ICON, icon); + } + + public Object getValue(String key) { + return values.get(key); + } + + public void putValue(String key, Object newValue) { + Object oldValue = values.get(key); + if (newValue == null) { + values.remove(key); + } else { + values.put(key, newValue); + } + firePropertyChange(key, oldValue, newValue); + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean newValue) { + boolean oldValue = enabled; + if (oldValue != newValue) { + enabled = newValue; + firePropertyChange("enabled", Boolean.valueOf(oldValue), Boolean.valueOf(newValue)); + } + } + + /// The keys with a value, or `null` when there are none. + public Object[] getKeys() { + if (values.isEmpty()) { + return null; + } + return values.keySet().toArray(); + } + + protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + if (changeSupport == null || oldValue != null && newValue != null && oldValue.equals(newValue)) { + return; + } + changeSupport.firePropertyChange(propertyName, oldValue, newValue); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + if (changeSupport == null) { + changeSupport = new SwingPropertyChangeSupport(this); + } + changeSupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + if (changeSupport == null) { + return; + } + changeSupport.removePropertyChangeListener(listener); + } + + public PropertyChangeListener[] getPropertyChangeListeners() { + if (changeSupport == null) { + return new PropertyChangeListener[0]; + } + return changeSupport.getPropertyChangeListeners(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/AbstractButton.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/AbstractButton.java new file mode 100644 index 00000000000..f86197c75cf --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/AbstractButton.java @@ -0,0 +1,426 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Insets; +import com.codename1.desktopcompat.java.awt.ItemSelectable; +import com.codename1.desktopcompat.java.awt.event.ActionEvent; +import com.codename1.desktopcompat.java.awt.event.ActionListener; +import com.codename1.desktopcompat.java.awt.event.ItemEvent; +import com.codename1.desktopcompat.java.awt.event.ItemListener; +import com.codename1.desktopcompat.javax.swing.event.ChangeEvent; +import com.codename1.desktopcompat.javax.swing.event.ChangeListener; +import com.codename1.desktopcompat.rt.Icons; + +/// The base of the buttons: text, icon, a button model and the action, +/// item and change listeners. +/// +/// The peer is a Codename One button of some kind. A click on it runs +/// `doClick()`, which drives the model exactly as a mouse click drives it +/// on the desktop, so listeners see the same events in the same order. The +/// margin, the mnemonic, the alignment and text position properties and the +/// rollover, pressed and selected icons are recorded only; components +/// added to a button are not shown. +public abstract class AbstractButton extends JComponent implements ItemSelectable, SwingConstants { + + protected ButtonModel model; + protected ChangeEvent changeEvent; + + private final Handler handler = new Handler(); + private String text = ""; + private Icon icon; + private Insets margin; + private Action action; + private int mnemonic; + private int horizontalAlignment = CENTER; + private int verticalAlignment = CENTER; + private int horizontalTextPosition = TRAILING; + private int verticalTextPosition = CENTER; + private boolean borderPainted = true; + private boolean focusPainted = true; + private boolean contentAreaFilled = true; + private boolean rolloverEnabled; + + public AbstractButton() { + } + + protected void init(String text, Icon icon) { + if (text != null) { + setText(text); + } + if (icon != null) { + setIcon(icon); + } + setAlignmentX(LEFT_ALIGNMENT); + setAlignmentY(CENTER_ALIGNMENT); + } + + @Override + protected void cn1PeerCreated() { + super.cn1PeerCreated(); + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof com.codename1.ui.Button) { + ((com.codename1.ui.Button) p).addActionListener( + new com.codename1.ui.events.ActionListener() { + @Override + public void actionPerformed(com.codename1.ui.events.ActionEvent evt) { + doClick(); + } + }); + } + sync(); + } + + private void sync() { + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof com.codename1.ui.Button) { + com.codename1.ui.Button b = (com.codename1.ui.Button) p; + b.setText(text == null ? "" : text); + b.setIcon(Icons.toNative(icon, this)); + boolean selected = model != null && model.isSelected(); + if (p instanceof com.codename1.ui.CheckBox) { + ((com.codename1.ui.CheckBox) p).setSelected(selected); + } else if (p instanceof com.codename1.ui.RadioButton) { + ((com.codename1.ui.RadioButton) p).setSelected(selected); + } + } + } + + private void changed() { + sync(); + revalidate(); + repaint(); + } + + public String getText() { + return text; + } + + public void setText(String text) { + String old = this.text; + this.text = text; + firePropertyChange("text", old, text); + if (old == null ? text != null : !old.equals(text)) { + changed(); + } + } + + public Icon getIcon() { + return icon; + } + + public void setIcon(Icon defaultIcon) { + Icon old = icon; + icon = defaultIcon; + firePropertyChange("icon", old, defaultIcon); + if (old != defaultIcon) { + changed(); + } + } + + public ButtonModel getModel() { + return model; + } + + public void setModel(ButtonModel newModel) { + ButtonModel old = model; + if (old != null) { + old.removeChangeListener(handler); + old.removeActionListener(handler); + old.removeItemListener(handler); + } + model = newModel; + if (newModel != null) { + newModel.addChangeListener(handler); + newModel.addActionListener(handler); + newModel.addItemListener(handler); + } + firePropertyChange("model", old, newModel); + if (newModel != old) { + changed(); + } + } + + public boolean isSelected() { + return model.isSelected(); + } + + public void setSelected(boolean b) { + model.setSelected(b); + } + + /// Clicks the button: arms and presses the model, then releases it, + /// which notifies the action listeners. + public void doClick() { + doClick(68); + } + + /// As `doClick()`; the button is not shown pressed for `pressTime`. + public void doClick(int pressTime) { + model.setArmed(true); + model.setPressed(true); + model.setPressed(false); + model.setArmed(false); + } + + @Override + public void setEnabled(boolean b) { + super.setEnabled(b); + model.setEnabled(b); + } + + public String getActionCommand() { + String ac = model.getActionCommand(); + return ac == null ? getText() : ac; + } + + public void setActionCommand(String actionCommand) { + model.setActionCommand(actionCommand); + } + + public Action getAction() { + return action; + } + + /// Takes the action's name as the text, its enabled state, and makes + /// it an action listener. Later changes of the action's properties are + /// not followed. + public void setAction(Action a) { + Action old = action; + if (old == a) { + return; + } + if (old != null) { + removeActionListener(old); + } + action = a; + if (a != null) { + Object name = a.getValue(Action.NAME); + if (name instanceof String) { + setText((String) name); + } + Object cmd = a.getValue(Action.ACTION_COMMAND_KEY); + if (cmd instanceof String) { + setActionCommand((String) cmd); + } + setEnabled(a.isEnabled()); + addActionListener(a); + } + firePropertyChange("action", old, a); + } + + public void addActionListener(ActionListener l) { + listenerList.add(ActionListener.class, l); + } + + public void removeActionListener(ActionListener l) { + listenerList.remove(ActionListener.class, l); + } + + public ActionListener[] getActionListeners() { + return listenerList.getListeners(ActionListener.class); + } + + public void addChangeListener(ChangeListener l) { + listenerList.add(ChangeListener.class, l); + } + + public void removeChangeListener(ChangeListener l) { + listenerList.remove(ChangeListener.class, l); + } + + public ChangeListener[] getChangeListeners() { + return listenerList.getListeners(ChangeListener.class); + } + + @Override + public void addItemListener(ItemListener l) { + listenerList.add(ItemListener.class, l); + } + + @Override + public void removeItemListener(ItemListener l) { + listenerList.remove(ItemListener.class, l); + } + + public ItemListener[] getItemListeners() { + return listenerList.getListeners(ItemListener.class); + } + + @Override + public Object[] getSelectedObjects() { + if (!isSelected()) { + return null; + } + return new Object[]{getText()}; + } + + protected void fireActionPerformed(ActionEvent event) { + ActionListener[] ls = getActionListeners(); + ActionEvent e = null; + for (int i = ls.length - 1; i >= 0; i--) { + if (e == null) { + String cmd = event.getActionCommand(); + e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, cmd == null ? getActionCommand() : cmd, + event.getWhen(), event.getModifiers()); + } + ls[i].actionPerformed(e); + } + } + + protected void fireStateChanged() { + ChangeListener[] ls = getChangeListeners(); + for (int i = ls.length - 1; i >= 0; i--) { + if (changeEvent == null) { + changeEvent = new ChangeEvent(this); + } + ls[i].stateChanged(changeEvent); + } + } + + protected void fireItemStateChanged(ItemEvent event) { + ItemListener[] ls = getItemListeners(); + ItemEvent e = null; + for (int i = ls.length - 1; i >= 0; i--) { + if (e == null) { + e = new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, this, event.getStateChange()); + } + ls[i].itemStateChanged(e); + } + } + + public int getMnemonic() { + return mnemonic; + } + + public void setMnemonic(int mnemonic) { + this.mnemonic = mnemonic; + model.setMnemonic(mnemonic); + } + + public void setMnemonic(char mnemonic) { + int vk = mnemonic; + if (vk >= 'a' && vk <= 'z') { + vk -= 'a' - 'A'; + } + setMnemonic(vk); + } + + public Insets getMargin() { + return margin == null ? null : new Insets(margin.top, margin.left, margin.bottom, margin.right); + } + + public void setMargin(Insets m) { + margin = m == null ? null : new Insets(m.top, m.left, m.bottom, m.right); + } + + public boolean isBorderPainted() { + return borderPainted; + } + + public void setBorderPainted(boolean b) { + borderPainted = b; + } + + public boolean isFocusPainted() { + return focusPainted; + } + + public void setFocusPainted(boolean b) { + focusPainted = b; + } + + public boolean isContentAreaFilled() { + return contentAreaFilled; + } + + public void setContentAreaFilled(boolean b) { + contentAreaFilled = b; + } + + public boolean isRolloverEnabled() { + return rolloverEnabled; + } + + public void setRolloverEnabled(boolean b) { + rolloverEnabled = b; + } + + public int getHorizontalAlignment() { + return horizontalAlignment; + } + + public void setHorizontalAlignment(int alignment) { + horizontalAlignment = alignment; + } + + public int getVerticalAlignment() { + return verticalAlignment; + } + + public void setVerticalAlignment(int alignment) { + verticalAlignment = alignment; + } + + public int getHorizontalTextPosition() { + return horizontalTextPosition; + } + + public void setHorizontalTextPosition(int textPosition) { + horizontalTextPosition = textPosition; + } + + public int getVerticalTextPosition() { + return verticalTextPosition; + } + + public void setVerticalTextPosition(int textPosition) { + verticalTextPosition = textPosition; + } + + @Override + protected String paramString() { + return super.paramString() + ",text=" + text; + } + + /// Forwards the model's events as this button's. + private final class Handler implements ChangeListener, ActionListener, ItemListener { + + @Override + public void stateChanged(ChangeEvent e) { + sync(); + fireStateChanged(); + repaint(); + } + + @Override + public void actionPerformed(ActionEvent e) { + fireActionPerformed(e); + } + + @Override + public void itemStateChanged(ItemEvent e) { + fireItemStateChanged(e); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Action.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Action.java new file mode 100644 index 00000000000..1dab2306333 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Action.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.event.ActionListener; +import com.codename1.desktopcompat.java.beans.PropertyChangeListener; + +/// An `ActionListener` that also carries named properties and an enabled +/// state, so that one action can drive several buttons and menu items. +public interface Action extends ActionListener { + + String DEFAULT = "Default"; + + String NAME = "Name"; + + String SHORT_DESCRIPTION = "ShortDescription"; + + String LONG_DESCRIPTION = "LongDescription"; + + String SMALL_ICON = "SmallIcon"; + + String ACTION_COMMAND_KEY = "ActionCommandKey"; + + String ACCELERATOR_KEY = "AcceleratorKey"; + + String MNEMONIC_KEY = "MnemonicKey"; + + String SELECTED_KEY = "SwingSelectedKey"; + + String DISPLAYED_MNEMONIC_INDEX_KEY = "SwingDisplayedMnemonicIndexKey"; + + String LARGE_ICON_KEY = "SwingLargeIconKey"; + + Object getValue(String key); + + void putValue(String key, Object value); + + void setEnabled(boolean b); + + boolean isEnabled(); + + void addPropertyChangeListener(PropertyChangeListener listener); + + void removePropertyChangeListener(PropertyChangeListener listener); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/BorderFactory.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/BorderFactory.java new file mode 100644 index 00000000000..095375ee752 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/BorderFactory.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Font; +import com.codename1.desktopcompat.javax.swing.border.BevelBorder; +import com.codename1.desktopcompat.javax.swing.border.Border; +import com.codename1.desktopcompat.javax.swing.border.CompoundBorder; +import com.codename1.desktopcompat.javax.swing.border.EmptyBorder; +import com.codename1.desktopcompat.javax.swing.border.EtchedBorder; +import com.codename1.desktopcompat.javax.swing.border.LineBorder; +import com.codename1.desktopcompat.javax.swing.border.MatteBorder; +import com.codename1.desktopcompat.javax.swing.border.TitledBorder; + +/// Factory methods for the standard borders. +/// +/// The soft bevel, stroke and dashed borders are not provided. +public class BorderFactory { + + private static final Border EMPTY = new EmptyBorder(0, 0, 0, 0); + private static final Border RAISED_BEVEL = new BevelBorder(BevelBorder.RAISED); + private static final Border LOWERED_BEVEL = new BevelBorder(BevelBorder.LOWERED); + private static final Border ETCHED = new EtchedBorder(); + + private BorderFactory() { + } + + public static Border createLineBorder(Color color) { + return new LineBorder(color, 1); + } + + public static Border createLineBorder(Color color, int thickness) { + return new LineBorder(color, thickness); + } + + public static Border createLineBorder(Color color, int thickness, boolean rounded) { + return new LineBorder(color, thickness, rounded); + } + + public static Border createRaisedBevelBorder() { + return RAISED_BEVEL; + } + + public static Border createLoweredBevelBorder() { + return LOWERED_BEVEL; + } + + public static Border createBevelBorder(int type) { + if (type == BevelBorder.RAISED) { + return RAISED_BEVEL; + } + if (type == BevelBorder.LOWERED) { + return LOWERED_BEVEL; + } + return null; + } + + public static Border createBevelBorder(int type, Color highlight, Color shadow) { + return new BevelBorder(type, highlight, shadow); + } + + public static Border createBevelBorder(int type, Color highlightOuter, Color highlightInner, Color shadowOuter, + Color shadowInner) { + return new BevelBorder(type, highlightOuter, highlightInner, shadowOuter, shadowInner); + } + + public static Border createEtchedBorder() { + return ETCHED; + } + + public static Border createEtchedBorder(Color highlight, Color shadow) { + return new EtchedBorder(highlight, shadow); + } + + public static Border createEtchedBorder(int type) { + if (type != EtchedBorder.RAISED && type != EtchedBorder.LOWERED) { + throw new IllegalArgumentException("type must be one of EtchedBorder.RAISED or EtchedBorder.LOWERED"); + } + return type == EtchedBorder.LOWERED ? ETCHED : new EtchedBorder(EtchedBorder.RAISED); + } + + public static Border createEtchedBorder(int type, Color highlight, Color shadow) { + return new EtchedBorder(type, highlight, shadow); + } + + public static TitledBorder createTitledBorder(String title) { + return new TitledBorder(title); + } + + public static TitledBorder createTitledBorder(Border border) { + return new TitledBorder(border); + } + + public static TitledBorder createTitledBorder(Border border, String title) { + return new TitledBorder(border, title); + } + + public static TitledBorder createTitledBorder(Border border, String title, int titleJustification, + int titlePosition) { + return new TitledBorder(border, title, titleJustification, titlePosition); + } + + public static TitledBorder createTitledBorder(Border border, String title, int titleJustification, + int titlePosition, Font titleFont) { + return new TitledBorder(border, title, titleJustification, titlePosition, titleFont); + } + + public static TitledBorder createTitledBorder(Border border, String title, int titleJustification, + int titlePosition, Font titleFont, Color titleColor) { + return new TitledBorder(border, title, titleJustification, titlePosition, titleFont, titleColor); + } + + public static Border createEmptyBorder() { + return EMPTY; + } + + public static Border createEmptyBorder(int top, int left, int bottom, int right) { + return new EmptyBorder(top, left, bottom, right); + } + + public static CompoundBorder createCompoundBorder() { + return new CompoundBorder(); + } + + public static CompoundBorder createCompoundBorder(Border outsideBorder, Border insideBorder) { + return new CompoundBorder(outsideBorder, insideBorder); + } + + public static MatteBorder createMatteBorder(int top, int left, int bottom, int right, Color color) { + return new MatteBorder(top, left, bottom, right, color); + } + + public static MatteBorder createMatteBorder(int top, int left, int bottom, int right, Icon tileIcon) { + return new MatteBorder(top, left, bottom, right, tileIcon); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Box.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Box.java new file mode 100644 index 00000000000..f9dd0b94f8f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Box.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.LayoutManager; + +/// A container that lays its children out with a [BoxLayout], and the +/// factory of the invisible struts, rigid areas and glue that space them. +public class Box extends JComponent { + + public Box(int axis) { + super(); + super.setLayout(new BoxLayout(this, axis)); + } + + public static Box createHorizontalBox() { + return new Box(BoxLayout.X_AXIS); + } + + public static Box createVerticalBox() { + return new Box(BoxLayout.Y_AXIS); + } + + public static Component createRigidArea(Dimension d) { + return new Filler(d, d, d); + } + + public static Component createHorizontalStrut(int width) { + return new Filler(new Dimension(width, 0), new Dimension(width, 0), new Dimension(width, Short.MAX_VALUE)); + } + + public static Component createVerticalStrut(int height) { + return new Filler(new Dimension(0, height), new Dimension(0, height), new Dimension(Short.MAX_VALUE, height)); + } + + public static Component createGlue() { + return new Filler(new Dimension(0, 0), new Dimension(0, 0), new Dimension(Short.MAX_VALUE, Short.MAX_VALUE)); + } + + public static Component createHorizontalGlue() { + return new Filler(new Dimension(0, 0), new Dimension(0, 0), new Dimension(Short.MAX_VALUE, 0)); + } + + public static Component createVerticalGlue() { + return new Filler(new Dimension(0, 0), new Dimension(0, 0), new Dimension(0, Short.MAX_VALUE)); + } + + /// A box keeps the layout it was built with. The JDK throws `AWTError` + /// here; this throws an `IllegalArgumentException`. + @Override + public void setLayout(LayoutManager l) { + throw new IllegalArgumentException("Illegal request"); + } + + @Override + protected void paintComponent(Graphics g) { + if (isOpaque()) { + Color bg = getBackground(); + if (bg != null) { + g.setColor(bg); + g.fillRect(0, 0, getWidth(), getHeight()); + } + } + } + + /// An invisible component with fixed minimum, preferred and maximum + /// sizes, used to take up space in a layout. + public static class Filler extends JComponent { + + public Filler(Dimension min, Dimension pref, Dimension max) { + setMinimumSize(min); + setPreferredSize(pref); + setMaximumSize(max); + } + + public void changeShape(Dimension min, Dimension pref, Dimension max) { + setMinimumSize(min); + setPreferredSize(pref); + setMaximumSize(max); + revalidate(); + } + + @Override + protected void paintComponent(Graphics g) { + if (isOpaque()) { + Color bg = getBackground(); + if (bg != null) { + g.setColor(bg); + g.fillRect(0, 0, getWidth(), getHeight()); + } + } + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/BoxLayout.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/BoxLayout.java new file mode 100644 index 00000000000..75a2f1f5275 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/BoxLayout.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.Insets; +import com.codename1.desktopcompat.java.awt.LayoutManager2; + +/// Places components in a single row or a single column, honouring each +/// one's minimum, preferred and maximum size and its alignment across the +/// row. +/// +/// There is no component orientation in this layer: `LINE_AXIS` is the same +/// as `X_AXIS` and `PAGE_AXIS` the same as `Y_AXIS`. Where the JDK throws +/// `AWTError` -- an axis that is none of the four, or a layout handed a +/// container other than its own -- this throws an `IllegalArgumentException`. +public class BoxLayout implements LayoutManager2 { + + public static final int X_AXIS = 0; + + public static final int Y_AXIS = 1; + + public static final int LINE_AXIS = 2; + + public static final int PAGE_AXIS = 3; + + private final int axis; + private final Container target; + + public BoxLayout(Container target, int axis) { + if (axis != X_AXIS && axis != Y_AXIS && axis != LINE_AXIS && axis != PAGE_AXIS) { + throw new IllegalArgumentException("Invalid axis"); + } + this.axis = axis; + this.target = target; + } + + public final Container getTarget() { + return target; + } + + public final int getAxis() { + return axis; + } + + @Override + public void invalidateLayout(Container target) { + check(target); + } + + @Override + public void addLayoutComponent(String name, Component comp) { + } + + @Override + public void removeLayoutComponent(Component comp) { + } + + @Override + public void addLayoutComponent(Component comp, Object constraints) { + } + + private void check(Container c) { + if (target != c) { + throw new IllegalArgumentException("BoxLayout can't be shared"); + } + } + + private boolean horizontal() { + return axis == X_AXIS || axis == LINE_AXIS; + } + + /// The requirements of every child along one axis; a hidden child asks + /// for nothing but keeps its alignment. + private SizeRequirements[] requests(boolean x) { + int n = target.getComponentCount(); + SizeRequirements[] out = new SizeRequirements[n]; + for (int i = 0; i < n; i++) { + Component c = target.getComponent(i); + if (!c.isVisible()) { + out[i] = new SizeRequirements(0, 0, 0, x ? c.getAlignmentX() : c.getAlignmentY()); + continue; + } + Dimension min = c.getMinimumSize(); + Dimension pref = c.getPreferredSize(); + Dimension max = c.getMaximumSize(); + out[i] = x ? new SizeRequirements(min.width, pref.width, max.width, c.getAlignmentX()) + : new SizeRequirements(min.height, pref.height, max.height, c.getAlignmentY()); + } + return out; + } + + private SizeRequirements total(SizeRequirements[] children, boolean x) { + return x == horizontal() ? SizeRequirements.getTiledSizeRequirements(children) + : SizeRequirements.getAlignedSizeRequirements(children); + } + + private static int clamp(long v) { + return v > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) v; + } + + private Dimension size(Container c, int which) { + check(c); + SizeRequirements xt = total(requests(true), true); + SizeRequirements yt = total(requests(false), false); + int w = which == 0 ? xt.minimum : which == 1 ? xt.preferred : xt.maximum; + int h = which == 0 ? yt.minimum : which == 1 ? yt.preferred : yt.maximum; + Insets in = c.getInsets(); + return new Dimension(clamp((long) w + (long) in.left + (long) in.right), + clamp((long) h + (long) in.top + (long) in.bottom)); + } + + @Override + public Dimension preferredLayoutSize(Container target) { + return size(target, 1); + } + + @Override + public Dimension minimumLayoutSize(Container target) { + return size(target, 0); + } + + @Override + public Dimension maximumLayoutSize(Container target) { + return size(target, 2); + } + + @Override + public float getLayoutAlignmentX(Container target) { + check(target); + return total(requests(true), true).alignment; + } + + @Override + public float getLayoutAlignmentY(Container target) { + check(target); + return total(requests(false), false).alignment; + } + + @Override + public void layoutContainer(Container target) { + check(target); + int n = target.getComponentCount(); + int[] xo = new int[n]; + int[] xs = new int[n]; + int[] yo = new int[n]; + int[] ys = new int[n]; + SizeRequirements[] xc = requests(true); + SizeRequirements[] yc = requests(false); + SizeRequirements xt = total(xc, true); + SizeRequirements yt = total(yc, false); + Insets in = target.getInsets(); + int w = target.getWidth() - in.left - in.right; + int h = target.getHeight() - in.top - in.bottom; + if (horizontal()) { + SizeRequirements.calculateTiledPositions(w, xt, xc, xo, xs, true); + SizeRequirements.calculateAlignedPositions(h, yt, yc, yo, ys); + } else { + SizeRequirements.calculateAlignedPositions(w, xt, xc, xo, xs, true); + SizeRequirements.calculateTiledPositions(h, yt, yc, yo, ys); + } + for (int i = 0; i < n; i++) { + target.getComponent(i).setBounds(clamp((long) in.left + (long) xo[i]), + clamp((long) in.top + (long) yo[i]), xs[i], ys[i]); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ButtonGroup.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ButtonGroup.java new file mode 100644 index 00000000000..e3236fd2fba --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ButtonGroup.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import java.util.Enumeration; +import java.util.Vector; + +/// Makes a set of buttons mutually exclusive: at most one of them is +/// selected, and selecting another deselects it. +/// +/// Once a button of the group is selected the group cannot be left without a +/// selection by deselecting that button, only by `clearSelection()`. +public class ButtonGroup { + + /// The buttons in the group. + protected Vector buttons = new Vector(); + + private ButtonModel selection; + + public ButtonGroup() { + } + + public void add(AbstractButton b) { + if (b == null) { + return; + } + buttons.addElement(b); + if (b.isSelected()) { + if (selection == null) { + selection = b.getModel(); + } else { + b.setSelected(false); + } + } + b.getModel().setGroup(this); + } + + public void remove(AbstractButton b) { + if (b == null) { + return; + } + buttons.removeElement(b); + if (b.getModel() == selection) { + selection = null; + } + b.getModel().setGroup(null); + } + + public void clearSelection() { + if (selection != null) { + ButtonModel oldSelection = selection; + selection = null; + oldSelection.setSelected(false); + } + } + + public Enumeration getElements() { + return buttons.elements(); + } + + public ButtonModel getSelection() { + return selection; + } + + public void setSelected(ButtonModel m, boolean b) { + if (b && m != null && m != selection) { + ButtonModel oldSelection = selection; + selection = m; + if (oldSelection != null) { + oldSelection.setSelected(false); + } + m.setSelected(true); + } + } + + public boolean isSelected(ButtonModel m) { + return m == selection; + } + + public int getButtonCount() { + return buttons.size(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ButtonModel.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ButtonModel.java new file mode 100644 index 00000000000..fcd762febfb --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ButtonModel.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.ItemSelectable; +import com.codename1.desktopcompat.java.awt.event.ActionListener; +import com.codename1.desktopcompat.java.awt.event.ItemListener; +import com.codename1.desktopcompat.javax.swing.event.ChangeListener; + +/// The state of a button: armed, selected, pressed, enabled and rolled over. +public interface ButtonModel extends ItemSelectable { + + boolean isArmed(); + + boolean isSelected(); + + boolean isEnabled(); + + boolean isPressed(); + + boolean isRollover(); + + void setArmed(boolean b); + + void setSelected(boolean b); + + void setEnabled(boolean b); + + void setPressed(boolean b); + + void setRollover(boolean b); + + void setMnemonic(int key); + + int getMnemonic(); + + void setActionCommand(String s); + + String getActionCommand(); + + void setGroup(ButtonGroup group); + + void addActionListener(ActionListener l); + + void removeActionListener(ActionListener l); + + void addItemListener(ItemListener l); + + void removeItemListener(ItemListener l); + + void addChangeListener(ChangeListener l); + + void removeChangeListener(ChangeListener l); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/DefaultButtonModel.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/DefaultButtonModel.java new file mode 100644 index 00000000000..26f5fdedd91 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/DefaultButtonModel.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.event.ActionEvent; +import com.codename1.desktopcompat.java.awt.event.ActionListener; +import com.codename1.desktopcompat.java.awt.event.ItemEvent; +import com.codename1.desktopcompat.java.awt.event.ItemListener; +import com.codename1.desktopcompat.javax.swing.event.ChangeEvent; +import com.codename1.desktopcompat.javax.swing.event.ChangeListener; +import com.codename1.desktopcompat.javax.swing.event.EventListenerList; + +import java.util.EventListener; + +/// The default state holder of a button. +/// +/// Every change of state fires a `ChangeEvent`, a change of selection also +/// fires an `ItemEvent`, and releasing the button while it is armed fires an +/// `ActionEvent`. The modifiers of the action event are always zero, because +/// there is no current event to take them from. A model in a `ButtonGroup` +/// lets the group decide whether it can be selected. +public class DefaultButtonModel implements ButtonModel { + + public static final int ARMED = 1 << 0; + + public static final int SELECTED = 1 << 1; + + public static final int PRESSED = 1 << 2; + + public static final int ENABLED = 1 << 3; + + public static final int ROLLOVER = 1 << 4; + + /// The bit set of the state constants. + protected int stateMask; + + protected String actionCommand; + + protected ButtonGroup group; + + protected int mnemonic; + + protected ChangeEvent changeEvent; + + protected EventListenerList listenerList = new EventListenerList(); + + public DefaultButtonModel() { + stateMask = 0; + mnemonic = 0; + actionCommand = null; + setEnabled(true); + } + + public void setActionCommand(String actionCommand) { + this.actionCommand = actionCommand; + } + + public String getActionCommand() { + return actionCommand; + } + + public boolean isArmed() { + return (stateMask & ARMED) != 0; + } + + public boolean isSelected() { + return (stateMask & SELECTED) != 0; + } + + public boolean isEnabled() { + return (stateMask & ENABLED) != 0; + } + + public boolean isPressed() { + return (stateMask & PRESSED) != 0; + } + + public boolean isRollover() { + return (stateMask & ROLLOVER) != 0; + } + + public void setArmed(boolean b) { + if (isArmed() == b || !isEnabled()) { + return; + } + if (b) { + stateMask |= ARMED; + } else { + stateMask &= ~ARMED; + } + fireStateChanged(); + } + + public void setEnabled(boolean b) { + if (isEnabled() == b) { + return; + } + if (b) { + stateMask |= ENABLED; + } else { + stateMask &= ~(ENABLED | ARMED | PRESSED); + } + fireStateChanged(); + } + + public void setSelected(boolean b) { + boolean selected = b; + if (group != null) { + group.setSelected(this, selected); + selected = group.isSelected(this); + } + if (isSelected() == selected) { + return; + } + if (selected) { + stateMask |= SELECTED; + } else { + stateMask &= ~SELECTED; + } + fireItemStateChanged(new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, this, + selected ? ItemEvent.SELECTED : ItemEvent.DESELECTED)); + fireStateChanged(); + } + + public void setPressed(boolean b) { + if (isPressed() == b || !isEnabled()) { + return; + } + if (b) { + stateMask |= PRESSED; + } else { + stateMask &= ~PRESSED; + } + if (!isPressed() && isArmed()) { + fireActionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, getActionCommand(), + System.currentTimeMillis(), 0)); + } + fireStateChanged(); + } + + public void setRollover(boolean b) { + if (isRollover() == b || !isEnabled()) { + return; + } + if (b) { + stateMask |= ROLLOVER; + } else { + stateMask &= ~ROLLOVER; + } + fireStateChanged(); + } + + public void setMnemonic(int key) { + mnemonic = key; + fireStateChanged(); + } + + public int getMnemonic() { + return mnemonic; + } + + public void addChangeListener(ChangeListener l) { + listenerList.add(ChangeListener.class, l); + } + + public void removeChangeListener(ChangeListener l) { + listenerList.remove(ChangeListener.class, l); + } + + public ChangeListener[] getChangeListeners() { + return listenerList.getListeners(ChangeListener.class); + } + + protected void fireStateChanged() { + Object[] listeners = listenerList.getListenerList(); + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == ChangeListener.class) { + if (changeEvent == null) { + changeEvent = new ChangeEvent(this); + } + ((ChangeListener) listeners[i + 1]).stateChanged(changeEvent); + } + } + } + + public void addActionListener(ActionListener l) { + listenerList.add(ActionListener.class, l); + } + + public void removeActionListener(ActionListener l) { + listenerList.remove(ActionListener.class, l); + } + + public ActionListener[] getActionListeners() { + return listenerList.getListeners(ActionListener.class); + } + + protected void fireActionPerformed(ActionEvent e) { + Object[] listeners = listenerList.getListenerList(); + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == ActionListener.class) { + ((ActionListener) listeners[i + 1]).actionPerformed(e); + } + } + } + + public void addItemListener(ItemListener l) { + listenerList.add(ItemListener.class, l); + } + + public void removeItemListener(ItemListener l) { + listenerList.remove(ItemListener.class, l); + } + + public ItemListener[] getItemListeners() { + return listenerList.getListeners(ItemListener.class); + } + + protected void fireItemStateChanged(ItemEvent e) { + Object[] listeners = listenerList.getListenerList(); + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == ItemListener.class) { + ((ItemListener) listeners[i + 1]).itemStateChanged(e); + } + } + } + + public T[] getListeners(Class listenerType) { + return listenerList.getListeners(listenerType); + } + + public Object[] getSelectedObjects() { + return null; + } + + public void setGroup(ButtonGroup group) { + this.group = group; + } + + public ButtonGroup getGroup() { + return group; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Icon.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Icon.java new file mode 100644 index 00000000000..3deec4caea6 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Icon.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; + +/// A small fixed-size picture. +public interface Icon { + + void paintIcon(Component c, Graphics g, int x, int y); + + int getIconWidth(); + + int getIconHeight(); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ImageIcon.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ImageIcon.java new file mode 100644 index 00000000000..118633f69af --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/ImageIcon.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Image; +import com.codename1.desktopcompat.java.awt.Toolkit; +import com.codename1.desktopcompat.java.awt.image.ImageObserver; +import com.codename1.desktopcompat.rt.NativeImage; +import com.codename1.ui.Display; +import java.io.IOException; +import java.io.InputStream; + +/// An icon that paints an image, drawn at one logical pixel per image +/// pixel. +/// +/// A file name is looked up among the application's resources by its last +/// path element, since an application has no file tree of its own. +public class ImageIcon implements Icon { + + private Image image; + private String description; + private int width = -1; + private int height = -1; + private ImageObserver imageObserver; + + public ImageIcon() { + } + + public ImageIcon(Image image) { + setImage(image); + } + + public ImageIcon(Image image, String description) { + setImage(image); + this.description = description; + } + + public ImageIcon(byte[] imageData) { + setImage(Toolkit.getDefaultToolkit().createImage(imageData)); + } + + public ImageIcon(byte[] imageData, String description) { + this(imageData); + this.description = description; + } + + public ImageIcon(String filename) { + this(filename, filename); + } + + public ImageIcon(String filename, String description) { + this.description = description; + if (filename != null && Display.isInitialized()) { + String name = filename; + int cut = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')); + if (cut >= 0) { + name = name.substring(cut + 1); + } + InputStream in = Display.getInstance().getResourceAsStream(null, "/" + name); + if (in != null) { + try { + try { + setImage(new NativeImage(com.codename1.ui.Image.createImage(in))); + } finally { + in.close(); + } + } catch (IOException e) { + image = null; + } + } + } + } + + public Image getImage() { + return image; + } + + public void setImage(Image image) { + this.image = image; + width = image == null ? -1 : image.getWidth(imageObserver); + height = image == null ? -1 : image.getHeight(imageObserver); + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + if (image != null) { + g.drawImage(image, x, y, imageObserver != null ? imageObserver : c); + } + } + + @Override + public int getIconWidth() { + return width; + } + + @Override + public int getIconHeight() { + return height; + } + + public void setImageObserver(ImageObserver observer) { + imageObserver = observer; + } + + public ImageObserver getImageObserver() { + return imageObserver; + } + + @Override + public String toString() { + return description != null ? description : super.toString(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JButton.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JButton.java new file mode 100644 index 00000000000..8a1f0e07de5 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JButton.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.rt.ButtonPeer; + +/// A push button, shown by a Codename One button. +public class JButton extends AbstractButton { + + public JButton() { + this(null, null); + } + + public JButton(Icon icon) { + this(null, icon); + } + + public JButton(String text) { + this(text, null); + } + + public JButton(Action a) { + this(); + setAction(a); + } + + public JButton(String text, Icon icon) { + setModel(new DefaultButtonModel()); + init(text, icon); + } + + @Override + protected com.codename1.ui.Component cn1CreatePeer() { + return new ButtonPeer(this); + } + + public boolean isDefaultButton() { + JRootPane root = SwingUtilities.getRootPane(this); + return root != null && root.getDefaultButton() == this; + } + + public boolean isDefaultCapable() { + return true; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JCheckBox.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JCheckBox.java new file mode 100644 index 00000000000..6935f9d6351 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JCheckBox.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.rt.CheckBoxPeer; + +/// A check box, shown by a Codename One check box. +public class JCheckBox extends JToggleButton { + + public JCheckBox() { + this(null, null, false); + } + + public JCheckBox(Icon icon) { + this(null, icon, false); + } + + public JCheckBox(Icon icon, boolean selected) { + this(null, icon, selected); + } + + public JCheckBox(String text) { + this(text, null, false); + } + + public JCheckBox(Action a) { + this(); + setAction(a); + } + + public JCheckBox(String text, boolean selected) { + this(text, null, selected); + } + + public JCheckBox(String text, Icon icon) { + this(text, icon, false); + } + + public JCheckBox(String text, Icon icon, boolean selected) { + super(text, icon, selected); + } + + @Override + protected com.codename1.ui.Component cn1CreatePeer() { + return new CheckBoxPeer(this); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JComponent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JComponent.java new file mode 100644 index 00000000000..97ca8969e03 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JComponent.java @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Insets; +import com.codename1.desktopcompat.java.awt.Rectangle; +import com.codename1.desktopcompat.javax.swing.border.Border; +import com.codename1.desktopcompat.javax.swing.event.EventListenerList; +import java.util.HashMap; + +/// The base of the Swing components. +/// +/// Painting follows Swing: `paint` calls `paintComponent`, `paintBorder` +/// and `paintChildren`. The default `paintComponent` draws the Codename +/// One widget behind a component that has one (a button, a label) and +/// otherwise fills the background of an opaque component; an override that +/// does not call `super.paintComponent` therefore starts from what its +/// parent painted. There are no UI delegates: `updateUI` does nothing, and +/// key bindings (input and action maps), tool tips and client side +/// scrolling are absent. +public abstract class JComponent extends Container { + + public static final int WHEN_FOCUSED = 0; + public static final int WHEN_ANCESTOR_OF_FOCUSED_COMPONENT = 1; + public static final int WHEN_IN_FOCUSED_WINDOW = 2; + public static final int UNDEFINED_CONDITION = -1; + public static final String TOOL_TIP_TEXT_KEY = "ToolTipText"; + + protected EventListenerList listenerList = new EventListenerList(); + + private Border border; + private boolean opaque; + private float alignmentX = -1; + private float alignmentY = -1; + private String toolTipText; + private HashMap clientProperties; + + public JComponent() { + } + + /// Does nothing: there are no UI delegates to refresh. + public void updateUI() { + } + + // ------------------------------------------------------------ paint + + @Override + public void paint(Graphics g) { + if (getWidth() <= 0 || getHeight() <= 0) { + return; + } + Graphics co = g.create(); + try { + paintComponent(co); + } finally { + co.dispose(); + } + co = g.create(); + try { + paintBorder(co); + } finally { + co.dispose(); + } + paintChildren(g); + } + + @Override + public void update(Graphics g) { + paint(g); + } + + /// Draws the Codename One widget behind this component if it has one; + /// otherwise fills the background when the component is opaque. + protected void paintComponent(Graphics g) { + if (cn1PaintNative(g)) { + return; + } + if (isOpaque()) { + Color bg = getBackground(); + if (bg != null) { + g.setColor(bg); + g.fillRect(0, 0, getWidth(), getHeight()); + } + } + } + + protected void paintBorder(Graphics g) { + if (border != null) { + border.paintBorder(this, g, 0, 0, getWidth(), getHeight()); + } + } + + protected void paintChildren(Graphics g) { + cn1PaintChildren(g); + } + + public void repaint(Rectangle r) { + repaint(0, r.x, r.y, r.width, r.height); + } + + /// Asks for a repaint; nothing is painted before this returns. + public void paintImmediately(int x, int y, int w, int h) { + repaint(0, x, y, w, h); + } + + public void paintImmediately(Rectangle r) { + repaint(0, r.x, r.y, r.width, r.height); + } + + @Override + public boolean isOpaque() { + return opaque; + } + + public void setOpaque(boolean isOpaque) { + boolean old = opaque; + opaque = isOpaque; + firePropertyChange("opaque", old, isOpaque); + repaint(); + } + + @Override + public boolean isDoubleBuffered() { + return true; + } + + public void setDoubleBuffered(boolean aFlag) { + } + + public Rectangle getVisibleRect() { + return new Rectangle(0, 0, getWidth(), getHeight()); + } + + // ------------------------------------------------------------ border + + public void setBorder(Border border) { + Border old = this.border; + this.border = border; + firePropertyChange("border", old, border); + if (border != old) { + revalidate(); + repaint(); + } + } + + public Border getBorder() { + return border; + } + + @Override + public Insets getInsets() { + if (border != null) { + return border.getBorderInsets(this); + } + return super.getInsets(); + } + + public Insets getInsets(Insets insets) { + Insets i = getInsets(); + if (insets == null) { + return i; + } + insets.top = i.top; + insets.left = i.left; + insets.bottom = i.bottom; + insets.right = i.right; + return insets; + } + + // ------------------------------------------------------------ sizes + + @Override + public Dimension getMaximumSize() { + if (!isMaximumSizeSet()) { + Dimension d = cn1NativePreferredSize(); + if (d != null) { + return d; + } + } + return super.getMaximumSize(); + } + + @Override + public float getAlignmentX() { + return alignmentX >= 0 ? alignmentX : super.getAlignmentX(); + } + + public void setAlignmentX(float alignmentX) { + this.alignmentX = alignmentX > 1.0f ? 1.0f : alignmentX < 0.0f ? 0.0f : alignmentX; + } + + @Override + public float getAlignmentY() { + return alignmentY >= 0 ? alignmentY : super.getAlignmentY(); + } + + public void setAlignmentY(float alignmentY) { + this.alignmentY = alignmentY > 1.0f ? 1.0f : alignmentY < 0.0f ? 0.0f : alignmentY; + } + + // ------------------------------------------------------------ misc + + /// Recorded only; no tool tip is shown. + public void setToolTipText(String text) { + String old = toolTipText; + toolTipText = text; + firePropertyChange(TOOL_TIP_TEXT_KEY, old, text); + } + + public String getToolTipText() { + return toolTipText; + } + + public final Object getClientProperty(Object key) { + return clientProperties == null ? null : clientProperties.get(key); + } + + public final void putClientProperty(Object key, Object value) { + if (clientProperties == null) { + if (value == null) { + return; + } + clientProperties = new HashMap(); + } + Object old = clientProperties.get(key); + if (value == null) { + clientProperties.remove(key); + } else { + clientProperties.put(key, value); + } + firePropertyChange(String.valueOf(key), old, value); + } + + public void grabFocus() { + requestFocus(); + } + + public void setRequestFocusEnabled(boolean requestFocusEnabled) { + } + + public JRootPane getRootPane() { + return SwingUtilities.getRootPane(this); + } + + public Container getTopLevelAncestor() { + for (Container p = this; p != null; p = p.getParent()) { + if (p instanceof com.codename1.desktopcompat.java.awt.Window) { + return p; + } + } + return null; + } + + @Override + public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { + super.firePropertyChange(propertyName, oldValue, newValue); + } + + @Override + public void firePropertyChange(String propertyName, int oldValue, int newValue) { + super.firePropertyChange(propertyName, oldValue, newValue); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JDialog.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JDialog.java new file mode 100644 index 00000000000..3da37602197 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JDialog.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.BorderLayout; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.desktopcompat.java.awt.Dialog; +import com.codename1.desktopcompat.java.awt.Frame; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.LayoutManager; +import com.codename1.desktopcompat.java.awt.event.WindowEvent; + +/// The Swing dialog, a skeleton: a root pane and a content pane like a +/// frame's, shown as a form. A modal dialog does not block its caller. +public class JDialog extends Dialog implements WindowConstants { + + protected JRootPane rootPane; + protected boolean rootPaneCheckingEnabled; + + private int defaultCloseOperation = HIDE_ON_CLOSE; + + public JDialog() { + this((Frame) null, "", false); + } + + public JDialog(Frame owner) { + this(owner, "", false); + } + + public JDialog(Frame owner, boolean modal) { + this(owner, "", modal); + } + + public JDialog(Frame owner, String title) { + this(owner, title, false); + } + + public JDialog(Frame owner, String title, boolean modal) { + super(owner, title, modal); + dialogInit(); + } + + protected void dialogInit() { + setRootPane(createRootPane()); + setRootPaneCheckingEnabled(true); + } + + protected JRootPane createRootPane() { + return new JRootPane(); + } + + public JRootPane getRootPane() { + return rootPane; + } + + protected void setRootPane(JRootPane root) { + boolean checking = rootPaneCheckingEnabled; + rootPaneCheckingEnabled = false; + try { + if (rootPane != null) { + remove(rootPane); + } + rootPane = root; + if (root != null) { + add(root, BorderLayout.CENTER); + } + } finally { + rootPaneCheckingEnabled = checking; + } + } + + public Container getContentPane() { + return rootPane.getContentPane(); + } + + public void setContentPane(Container contentPane) { + rootPane.setContentPane(contentPane); + } + + protected boolean isRootPaneCheckingEnabled() { + return rootPaneCheckingEnabled; + } + + protected void setRootPaneCheckingEnabled(boolean enabled) { + rootPaneCheckingEnabled = enabled; + } + + @Override + protected void addImpl(Component comp, Object constraints, int index) { + if (rootPaneCheckingEnabled) { + getContentPane().add(comp, constraints, index); + } else { + super.addImpl(comp, constraints, index); + } + } + + @Override + public void remove(Component comp) { + if (comp == rootPane || !rootPaneCheckingEnabled) { + super.remove(comp); + } else { + getContentPane().remove(comp); + } + } + + @Override + public void setLayout(LayoutManager manager) { + if (rootPaneCheckingEnabled) { + getContentPane().setLayout(manager); + } else { + super.setLayout(manager); + } + } + + @Override + public void update(Graphics g) { + paint(g); + } + + public int getDefaultCloseOperation() { + return defaultCloseOperation; + } + + public void setDefaultCloseOperation(int operation) { + if (operation != DO_NOTHING_ON_CLOSE && operation != HIDE_ON_CLOSE && operation != DISPOSE_ON_CLOSE) { + throw new IllegalArgumentException("defaultCloseOperation must be one of: DO_NOTHING_ON_CLOSE, " + + "HIDE_ON_CLOSE, or DISPOSE_ON_CLOSE"); + } + int old = defaultCloseOperation; + defaultCloseOperation = operation; + firePropertyChange("defaultCloseOperation", old, operation); + } + + @Override + protected void processWindowEvent(WindowEvent e) { + super.processWindowEvent(e); + if (e.getID() == WindowEvent.WINDOW_CLOSING) { + if (defaultCloseOperation == HIDE_ON_CLOSE) { + setVisible(false); + } else if (defaultCloseOperation == DISPOSE_ON_CLOSE) { + dispose(); + } + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JFrame.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JFrame.java new file mode 100644 index 00000000000..27c32db4f6f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JFrame.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.BorderLayout; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.desktopcompat.java.awt.Frame; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.LayoutManager; +import com.codename1.desktopcompat.java.awt.event.WindowEvent; +import com.codename1.ui.Display; + +/// The Swing frame: a frame whose single child is a root pane, so that +/// adding to the frame adds to its content pane. +/// +/// A request to close the frame -- the back command of its form -- runs +/// the default close operation after the window listeners. `EXIT_ON_CLOSE` +/// exits the application. +public class JFrame extends Frame implements WindowConstants { + + public static final int EXIT_ON_CLOSE = 3; + + protected JRootPane rootPane; + protected boolean rootPaneCheckingEnabled; + + private int defaultCloseOperation = HIDE_ON_CLOSE; + + public JFrame() { + this(""); + } + + public JFrame(String title) { + super(title); + frameInit(); + } + + protected void frameInit() { + setRootPane(createRootPane()); + setRootPaneCheckingEnabled(true); + } + + protected JRootPane createRootPane() { + return new JRootPane(); + } + + public JRootPane getRootPane() { + return rootPane; + } + + protected void setRootPane(JRootPane root) { + boolean checking = rootPaneCheckingEnabled; + rootPaneCheckingEnabled = false; + try { + if (rootPane != null) { + remove(rootPane); + } + rootPane = root; + if (root != null) { + add(root, BorderLayout.CENTER); + } + } finally { + rootPaneCheckingEnabled = checking; + } + } + + public Container getContentPane() { + return rootPane.getContentPane(); + } + + public void setContentPane(Container contentPane) { + rootPane.setContentPane(contentPane); + } + + protected boolean isRootPaneCheckingEnabled() { + return rootPaneCheckingEnabled; + } + + protected void setRootPaneCheckingEnabled(boolean enabled) { + rootPaneCheckingEnabled = enabled; + } + + @Override + protected void addImpl(Component comp, Object constraints, int index) { + if (rootPaneCheckingEnabled) { + getContentPane().add(comp, constraints, index); + } else { + super.addImpl(comp, constraints, index); + } + } + + @Override + public void remove(Component comp) { + if (comp == rootPane || !rootPaneCheckingEnabled) { + super.remove(comp); + } else { + getContentPane().remove(comp); + } + } + + @Override + public void setLayout(LayoutManager manager) { + if (rootPaneCheckingEnabled) { + getContentPane().setLayout(manager); + } else { + super.setLayout(manager); + } + } + + @Override + public void update(Graphics g) { + paint(g); + } + + public int getDefaultCloseOperation() { + return defaultCloseOperation; + } + + public void setDefaultCloseOperation(int operation) { + if (operation != DO_NOTHING_ON_CLOSE && operation != HIDE_ON_CLOSE && operation != DISPOSE_ON_CLOSE + && operation != EXIT_ON_CLOSE) { + throw new IllegalArgumentException("defaultCloseOperation must be one of: DO_NOTHING_ON_CLOSE, " + + "HIDE_ON_CLOSE, DISPOSE_ON_CLOSE, or EXIT_ON_CLOSE"); + } + int old = defaultCloseOperation; + defaultCloseOperation = operation; + firePropertyChange("defaultCloseOperation", old, operation); + } + + @Override + protected void processWindowEvent(WindowEvent e) { + super.processWindowEvent(e); + if (e.getID() == WindowEvent.WINDOW_CLOSING) { + switch (defaultCloseOperation) { + case HIDE_ON_CLOSE: + setVisible(false); + break; + case DISPOSE_ON_CLOSE: + dispose(); + break; + case EXIT_ON_CLOSE: + if (Display.isInitialized()) { + Display.getInstance().exitApplication(); + } + break; + default: + break; + } + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JLabel.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JLabel.java new file mode 100644 index 00000000000..f81599cc37f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JLabel.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.rt.Icons; +import com.codename1.desktopcompat.rt.LabelPeer; + +/// A line of text, an icon, or both, shown by a Codename One label. +/// +/// The horizontal alignment is honoured; the vertical alignment and the +/// text position relative to the icon are recorded and the label keeps +/// Codename One's arrangement (icon before the text, centered vertically). +/// HTML text is shown as it is written. +public class JLabel extends JComponent implements SwingConstants { + + private String text; + private Icon icon; + private Icon disabledIcon; + private int horizontalAlignment; + private int verticalAlignment = CENTER; + private int horizontalTextPosition = TRAILING; + private int verticalTextPosition = CENTER; + private int iconTextGap = 4; + private Component labelFor; + + public JLabel(String text, Icon icon, int horizontalAlignment) { + this.text = text; + this.icon = icon; + setHorizontalAlignment(horizontalAlignment); + setAlignmentX(LEFT_ALIGNMENT); + } + + public JLabel(String text, int horizontalAlignment) { + this(text, null, horizontalAlignment); + } + + public JLabel(String text) { + this(text, null, LEADING); + } + + public JLabel(Icon image, int horizontalAlignment) { + this(null, image, horizontalAlignment); + } + + public JLabel(Icon image) { + this(null, image, CENTER); + } + + public JLabel() { + this("", null, LEADING); + } + + @Override + protected com.codename1.ui.Component cn1CreatePeer() { + return new LabelPeer(this); + } + + @Override + protected void cn1PeerCreated() { + super.cn1PeerCreated(); + sync(); + } + + private void sync() { + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof com.codename1.ui.Label) { + com.codename1.ui.Label l = (com.codename1.ui.Label) p; + l.setText(text == null ? "" : text); + l.setIcon(Icons.toNative(icon, this)); + l.setAlignment(nativeAlignment(horizontalAlignment)); + l.setGap(com.codename1.desktopcompat.rt.Units.toDevice(iconTextGap)); + } + } + + static int nativeAlignment(int alignment) { + switch (alignment) { + case CENTER: + return com.codename1.ui.Component.CENTER; + case RIGHT: + case TRAILING: + return com.codename1.ui.Component.RIGHT; + default: + return com.codename1.ui.Component.LEFT; + } + } + + private void changed() { + sync(); + revalidate(); + repaint(); + } + + public String getText() { + return text; + } + + public void setText(String text) { + String old = this.text; + this.text = text; + firePropertyChange("text", old, text); + if (old == null ? text != null : !old.equals(text)) { + changed(); + } + } + + public Icon getIcon() { + return icon; + } + + public void setIcon(Icon icon) { + Icon old = this.icon; + this.icon = icon; + firePropertyChange("icon", old, icon); + if (old != icon) { + changed(); + } + } + + public Icon getDisabledIcon() { + return disabledIcon; + } + + /// Recorded only. + public void setDisabledIcon(Icon disabledIcon) { + this.disabledIcon = disabledIcon; + } + + public int getHorizontalAlignment() { + return horizontalAlignment; + } + + public void setHorizontalAlignment(int alignment) { + if (alignment != LEFT && alignment != CENTER && alignment != RIGHT && alignment != LEADING + && alignment != TRAILING) { + throw new IllegalArgumentException("horizontalAlignment"); + } + if (alignment != horizontalAlignment) { + int old = horizontalAlignment; + horizontalAlignment = alignment; + firePropertyChange("horizontalAlignment", old, alignment); + sync(); + repaint(); + } + } + + public int getVerticalAlignment() { + return verticalAlignment; + } + + public void setVerticalAlignment(int alignment) { + if (alignment != TOP && alignment != CENTER && alignment != BOTTOM) { + throw new IllegalArgumentException("verticalAlignment"); + } + verticalAlignment = alignment; + } + + public int getHorizontalTextPosition() { + return horizontalTextPosition; + } + + public void setHorizontalTextPosition(int textPosition) { + horizontalTextPosition = textPosition; + } + + public int getVerticalTextPosition() { + return verticalTextPosition; + } + + public void setVerticalTextPosition(int textPosition) { + verticalTextPosition = textPosition; + } + + public int getIconTextGap() { + return iconTextGap; + } + + public void setIconTextGap(int iconTextGap) { + if (iconTextGap != this.iconTextGap) { + this.iconTextGap = iconTextGap; + changed(); + } + } + + public Component getLabelFor() { + return labelFor; + } + + public void setLabelFor(Component c) { + labelFor = c; + } + + @Override + protected String paramString() { + return super.paramString() + ",text=" + text; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JPanel.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JPanel.java new file mode 100644 index 00000000000..583154a2e3c --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JPanel.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.FlowLayout; +import com.codename1.desktopcompat.java.awt.LayoutManager; + +/// The plain Swing container: opaque, with a flow layout unless told +/// otherwise. Subclasses paint in `paintComponent`. +public class JPanel extends JComponent { + + public JPanel(LayoutManager layout, boolean isDoubleBuffered) { + setLayout(layout); + setOpaque(true); + } + + public JPanel(LayoutManager layout) { + this(layout, true); + } + + public JPanel(boolean isDoubleBuffered) { + this(new FlowLayout(), isDoubleBuffered); + } + + public JPanel() { + this(true); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JRadioButton.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JRadioButton.java new file mode 100644 index 00000000000..824f0d3b05f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JRadioButton.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.rt.RadioButtonPeer; + +/// A radio button, shown by a Codename One radio button; put several in a +/// `ButtonGroup` to make them exclusive. +public class JRadioButton extends JToggleButton { + + public JRadioButton() { + this(null, null, false); + } + + public JRadioButton(Icon icon) { + this(null, icon, false); + } + + public JRadioButton(Action a) { + this(); + setAction(a); + } + + public JRadioButton(Icon icon, boolean selected) { + this(null, icon, selected); + } + + public JRadioButton(String text) { + this(text, null, false); + } + + public JRadioButton(String text, boolean selected) { + this(text, null, selected); + } + + public JRadioButton(String text, Icon icon) { + this(text, icon, false); + } + + public JRadioButton(String text, Icon icon, boolean selected) { + super(text, icon, selected); + } + + @Override + protected com.codename1.ui.Component cn1CreatePeer() { + return new RadioButtonPeer(this); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JRootPane.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JRootPane.java new file mode 100644 index 00000000000..c83d2848e63 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JRootPane.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.BorderLayout; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.Insets; +import com.codename1.desktopcompat.java.awt.LayoutManager; + +/// The single child of a frame or dialog; it holds the content pane, which +/// fills it. There is no layered pane, glass pane or menu bar. +public class JRootPane extends JComponent { + + protected Container contentPane; + protected JButton defaultButton; + + public JRootPane() { + setLayout(createRootLayout()); + setContentPane(createContentPane()); + } + + protected Container createContentPane() { + JPanel p = new JPanel(new BorderLayout()); + p.setName(getName() + ".contentPane"); + return p; + } + + protected LayoutManager createRootLayout() { + return new RootLayout(); + } + + public Container getContentPane() { + return contentPane; + } + + public void setContentPane(Container content) { + if (content == null) { + throw new IllegalArgumentException("contentPane cannot be set to null."); + } + if (contentPane != null && contentPane.getParent() == this) { + remove(contentPane); + } + contentPane = content; + add(content); + } + + public JButton getDefaultButton() { + return defaultButton; + } + + /// Recorded only: no key activates the default button. + public void setDefaultButton(JButton defaultButton) { + this.defaultButton = defaultButton; + } + + @Override + public boolean isValidateRoot() { + return true; + } + + /// Gives every child the whole of the root pane. + private static final class RootLayout implements LayoutManager { + + @Override + public void addLayoutComponent(String name, Component comp) { + } + + @Override + public void removeLayoutComponent(Component comp) { + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + return size(parent, true); + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return size(parent, false); + } + + private static Dimension size(Container parent, boolean preferred) { + Insets in = parent.getInsets(); + int w = 0; + int h = 0; + for (int i = 0; i < parent.getComponentCount(); i++) { + Component c = parent.getComponent(i); + Dimension d = preferred ? c.getPreferredSize() : c.getMinimumSize(); + w = Math.max(w, d.width); + h = Math.max(h, d.height); + } + return new Dimension(w + in.left + in.right, h + in.top + in.bottom); + } + + @Override + public void layoutContainer(Container parent) { + Insets in = parent.getInsets(); + int w = parent.getWidth() - in.left - in.right; + int h = parent.getHeight() - in.top - in.bottom; + for (int i = 0; i < parent.getComponentCount(); i++) { + parent.getComponent(i).setBounds(in.left, in.top, w, h); + } + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JTextField.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JTextField.java new file mode 100644 index 00000000000..a357a5f526a --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JTextField.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.event.ActionEvent; +import com.codename1.desktopcompat.java.awt.event.ActionListener; +import com.codename1.desktopcompat.javax.swing.text.JTextComponent; +import com.codename1.desktopcompat.rt.TextFieldPeer; + +/// A single line of editable text, shown and edited by a Codename One +/// text field. Finishing the edit notifies the action listeners. +public class JTextField extends JTextComponent implements SwingConstants { + + public static final String notifyAction = "notify-field-accept"; + + private int columns; + private int horizontalAlignment = LEADING; + private String command; + + public JTextField() { + this(null, 0); + } + + public JTextField(String text) { + this(text, 0); + } + + public JTextField(int columns) { + this(null, columns); + } + + public JTextField(String text, int columns) { + if (columns < 0) { + throw new IllegalArgumentException("columns less than zero."); + } + this.columns = columns; + if (text != null) { + setText(text); + } + } + + @Override + protected com.codename1.ui.Component cn1CreatePeer() { + TextFieldPeer p = new TextFieldPeer(this); + p.setSingleLineTextArea(true); + if (columns > 0) { + p.setColumns(columns); + } + p.addActionListener(new com.codename1.ui.events.ActionListener() { + @Override + public void actionPerformed(com.codename1.ui.events.ActionEvent evt) { + postActionEvent(); + } + }); + return p; + } + + @Override + public Dimension getMaximumSize() { + if (isMaximumSizeSet()) { + return super.getMaximumSize(); + } + return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + public int getColumns() { + return columns; + } + + public void setColumns(int columns) { + if (columns < 0) { + throw new IllegalArgumentException("columns less than zero."); + } + if (columns != this.columns) { + this.columns = columns; + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof com.codename1.ui.TextArea && columns > 0) { + ((com.codename1.ui.TextArea) p).setColumns(columns); + } + revalidate(); + } + } + + public int getHorizontalAlignment() { + return horizontalAlignment; + } + + /// Recorded only. + public void setHorizontalAlignment(int alignment) { + horizontalAlignment = alignment; + } + + public void addActionListener(ActionListener l) { + listenerList.add(ActionListener.class, l); + } + + public void removeActionListener(ActionListener l) { + listenerList.remove(ActionListener.class, l); + } + + public ActionListener[] getActionListeners() { + return listenerList.getListeners(ActionListener.class); + } + + public void setActionCommand(String command) { + this.command = command; + } + + protected void fireActionPerformed() { + ActionListener[] ls = getActionListeners(); + if (ls.length == 0) { + return; + } + ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command != null ? command : getText(), + System.currentTimeMillis(), 0); + for (int i = ls.length - 1; i >= 0; i--) { + ls[i].actionPerformed(e); + } + } + + public void postActionEvent() { + fireActionPerformed(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JToggleButton.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JToggleButton.java new file mode 100644 index 00000000000..e1ae34803bb --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/JToggleButton.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.rt.CheckBoxPeer; + +/// A button that stays selected until clicked again, shown by a Codename +/// One toggle button. +public class JToggleButton extends AbstractButton { + + public JToggleButton() { + this(null, null, false); + } + + public JToggleButton(Icon icon) { + this(null, icon, false); + } + + public JToggleButton(Icon icon, boolean selected) { + this(null, icon, selected); + } + + public JToggleButton(String text) { + this(text, null, false); + } + + public JToggleButton(String text, boolean selected) { + this(text, null, selected); + } + + public JToggleButton(Action a) { + this(); + setAction(a); + } + + public JToggleButton(String text, Icon icon) { + this(text, icon, false); + } + + public JToggleButton(String text, Icon icon, boolean selected) { + setModel(new ToggleButtonModel()); + model.setSelected(selected); + init(text, icon); + } + + @Override + protected com.codename1.ui.Component cn1CreatePeer() { + CheckBoxPeer p = new CheckBoxPeer(this); + p.setToggle(true); + return p; + } + + /// The model of the toggle buttons: a click flips the selection, and + /// a button group may refuse to deselect. + public static class ToggleButtonModel extends DefaultButtonModel { + + public ToggleButtonModel() { + } + + @Override + public void setPressed(boolean b) { + if (isPressed() != b && isEnabled() && !b && isArmed()) { + setSelected(!isSelected()); + } + super.setPressed(b); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/OverlayLayout.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/OverlayLayout.java new file mode 100644 index 00000000000..b7238a1a49a --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/OverlayLayout.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.Insets; +import com.codename1.desktopcompat.java.awt.LayoutManager2; + +/// Stacks components over one another, lining up the alignment point of +/// each on both axes. +/// +/// A layout handed a container other than its own throws an +/// `IllegalArgumentException` where the JDK throws `AWTError`. +public class OverlayLayout implements LayoutManager2 { + + private final Container target; + + public OverlayLayout(Container target) { + this.target = target; + } + + public final Container getTarget() { + return target; + } + + @Override + public void invalidateLayout(Container target) { + check(target); + } + + @Override + public void addLayoutComponent(String name, Component comp) { + } + + @Override + public void removeLayoutComponent(Component comp) { + } + + @Override + public void addLayoutComponent(Component comp, Object constraints) { + } + + private void check(Container c) { + if (target != c) { + throw new IllegalArgumentException("OverlayLayout can't be shared"); + } + } + + private SizeRequirements[] requests(boolean x) { + int n = target.getComponentCount(); + SizeRequirements[] out = new SizeRequirements[n]; + for (int i = 0; i < n; i++) { + Component c = target.getComponent(i); + Dimension min = c.getMinimumSize(); + Dimension pref = c.getPreferredSize(); + Dimension max = c.getMaximumSize(); + out[i] = x ? new SizeRequirements(min.width, pref.width, max.width, c.getAlignmentX()) + : new SizeRequirements(min.height, pref.height, max.height, c.getAlignmentY()); + } + return out; + } + + private Dimension size(Container c, int which) { + check(c); + SizeRequirements xt = SizeRequirements.getAlignedSizeRequirements(requests(true)); + SizeRequirements yt = SizeRequirements.getAlignedSizeRequirements(requests(false)); + int w = which == 0 ? xt.minimum : which == 1 ? xt.preferred : xt.maximum; + int h = which == 0 ? yt.minimum : which == 1 ? yt.preferred : yt.maximum; + Insets in = c.getInsets(); + return new Dimension(w + in.left + in.right, h + in.top + in.bottom); + } + + @Override + public Dimension preferredLayoutSize(Container target) { + return size(target, 1); + } + + @Override + public Dimension minimumLayoutSize(Container target) { + return size(target, 0); + } + + @Override + public Dimension maximumLayoutSize(Container target) { + return size(target, 2); + } + + @Override + public float getLayoutAlignmentX(Container target) { + check(target); + return SizeRequirements.getAlignedSizeRequirements(requests(true)).alignment; + } + + @Override + public float getLayoutAlignmentY(Container target) { + check(target); + return SizeRequirements.getAlignedSizeRequirements(requests(false)).alignment; + } + + @Override + public void layoutContainer(Container target) { + check(target); + int n = target.getComponentCount(); + int[] xo = new int[n]; + int[] xs = new int[n]; + int[] yo = new int[n]; + int[] ys = new int[n]; + SizeRequirements[] xc = requests(true); + SizeRequirements[] yc = requests(false); + Insets in = target.getInsets(); + SizeRequirements.calculateAlignedPositions(target.getWidth() - in.left - in.right, + SizeRequirements.getAlignedSizeRequirements(xc), xc, xo, xs); + SizeRequirements.calculateAlignedPositions(target.getHeight() - in.top - in.bottom, + SizeRequirements.getAlignedSizeRequirements(yc), yc, yo, ys); + for (int i = 0; i < n; i++) { + target.getComponent(i).setBounds(in.left + xo[i], in.top + yo[i], xs[i], ys[i]); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SizeRequirements.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SizeRequirements.java new file mode 100644 index 00000000000..556c16399a4 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SizeRequirements.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +/// The minimum, preferred and maximum extent of a component along one axis +/// together with its alignment, and the arithmetic layouts use to place a +/// row of such components end to end or over one another. +public class SizeRequirements { + + public int minimum; + + public int preferred; + + public int maximum; + + public float alignment; + + public SizeRequirements() { + minimum = 0; + preferred = 0; + maximum = 0; + alignment = 0.5f; + } + + public SizeRequirements(int min, int pref, int max, float a) { + minimum = min; + preferred = pref; + maximum = max; + alignment = a > 1.0f ? 1.0f : a < 0.0f ? 0.0f : a; + } + + @Override + public String toString() { + return "[" + minimum + "," + preferred + "," + maximum + "]@" + alignment; + } + + private static int clamp(long v) { + return v > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) v; + } + + public static SizeRequirements getTiledSizeRequirements(SizeRequirements[] children) { + SizeRequirements total = new SizeRequirements(); + long min = 0; + long pref = 0; + long max = 0; + for (int i = 0; i < children.length; i++) { + SizeRequirements req = children[i]; + min += req.minimum; + pref += req.preferred; + max += req.maximum; + } + total.minimum = clamp(min); + total.preferred = clamp(pref); + total.maximum = clamp(max); + return total; + } + + public static SizeRequirements getAlignedSizeRequirements(SizeRequirements[] children) { + int minAbove = 0; + int minBelow = 0; + int prefAbove = 0; + int prefBelow = 0; + int maxAbove = 0; + int maxBelow = 0; + for (int i = 0; i < children.length; i++) { + SizeRequirements req = children[i]; + int above = (int) (req.alignment * req.minimum); + int below = req.minimum - above; + minAbove = Math.max(above, minAbove); + minBelow = Math.max(below, minBelow); + above = (int) (req.alignment * req.preferred); + below = req.preferred - above; + prefAbove = Math.max(above, prefAbove); + prefBelow = Math.max(below, prefBelow); + above = (int) (req.alignment * req.maximum); + below = req.maximum - above; + maxAbove = Math.max(above, maxAbove); + maxBelow = Math.max(below, maxBelow); + } + int min = clamp((long) minAbove + (long) minBelow); + int pref = clamp((long) prefAbove + (long) prefBelow); + int max = clamp((long) maxAbove + (long) maxBelow); + float alignment = 0.0f; + if (min > 0) { + alignment = (float) minAbove / min; + alignment = alignment > 1.0f ? 1.0f : alignment < 0.0f ? 0.0f : alignment; + } + return new SizeRequirements(min, pref, max, alignment); + } + + public static void calculateTiledPositions(int allocated, SizeRequirements total, SizeRequirements[] children, + int[] offsets, int[] spans) { + calculateTiledPositions(allocated, total, children, offsets, spans, true); + } + + public static void calculateTiledPositions(int allocated, SizeRequirements total, SizeRequirements[] children, + int[] offsets, int[] spans, boolean forward) { + long min = 0; + long pref = 0; + long max = 0; + for (int i = 0; i < children.length; i++) { + min += children[i].minimum; + pref += children[i].preferred; + max += children[i].maximum; + } + boolean grow = allocated >= pref; + // The fraction of each child's slack (towards its maximum when + // growing, towards its minimum when shrinking) that is taken up. + float play; + float range; + if (grow) { + play = (float) Math.min(allocated - pref, max - pref); + range = (float) (max - pref); + } else { + play = (float) Math.min(pref - allocated, pref - min); + range = (float) (pref - min); + } + float factor = (grow ? max - pref : pref - min) == 0 ? 0.0f : play / range; + int at = forward ? 0 : allocated; + for (int i = 0; i < children.length; i++) { + SizeRequirements req = children[i]; + if (grow) { + int extra = (int) (factor * (req.maximum - req.preferred)); + spans[i] = clamp((long) req.preferred + (long) extra); + } else { + float less = factor * (req.preferred - req.minimum); + spans[i] = (int) (req.preferred - less); + } + if (forward) { + offsets[i] = at; + at = clamp((long) at + (long) spans[i]); + } else { + offsets[i] = at - spans[i]; + at = (int) Math.max((long) at - (long) spans[i], 0L); + } + } + } + + public static void calculateAlignedPositions(int allocated, SizeRequirements total, SizeRequirements[] children, + int[] offsets, int[] spans) { + calculateAlignedPositions(allocated, total, children, offsets, spans, true); + } + + public static void calculateAlignedPositions(int allocated, SizeRequirements total, SizeRequirements[] children, + int[] offsets, int[] spans, boolean normal) { + float align = normal ? total.alignment : 1.0f - total.alignment; + int above = (int) (allocated * align); + int below = allocated - above; + for (int i = 0; i < children.length; i++) { + SizeRequirements req = children[i]; + float a = normal ? req.alignment : 1.0f - req.alignment; + int maxAbove = (int) (req.maximum * a); + int maxBelow = req.maximum - maxAbove; + int up = Math.min(above, maxAbove); + int down = Math.min(below, maxBelow); + offsets[i] = above - up; + spans[i] = clamp((long) up + (long) down); + } + } + + public static int[] adjustSizes(int delta, SizeRequirements[] children) { + return new int[0]; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SwingConstants.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SwingConstants.java new file mode 100644 index 00000000000..b543a0b8471 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SwingConstants.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +/// Positions, orientations and directions shared by Swing components. +public interface SwingConstants { + + int CENTER = 0; + + int TOP = 1; + + int LEFT = 2; + + int BOTTOM = 3; + + int RIGHT = 4; + + int NORTH = 1; + + int NORTH_EAST = 2; + + int EAST = 3; + + int SOUTH_EAST = 4; + + int SOUTH = 5; + + int SOUTH_WEST = 6; + + int WEST = 7; + + int NORTH_WEST = 8; + + int HORIZONTAL = 0; + + int VERTICAL = 1; + + int LEADING = 10; + + int TRAILING = 11; + + int NEXT = 12; + + int PREVIOUS = 13; +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SwingUtilities.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SwingUtilities.java new file mode 100644 index 00000000000..8c8c15974d0 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/SwingUtilities.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.desktopcompat.java.awt.EventQueue; +import com.codename1.desktopcompat.java.awt.FontMetrics; +import com.codename1.desktopcompat.java.awt.Point; +import com.codename1.desktopcompat.java.awt.Window; +import com.codename1.desktopcompat.java.awt.event.MouseEvent; + +/// The Swing helpers for the event dispatch thread and the component +/// tree. +public class SwingUtilities implements SwingConstants { + + private SwingUtilities() { + } + + public static void invokeLater(Runnable doRun) { + EventQueue.invokeLater(doRun); + } + + /// See `EventQueue.invokeAndWait`: on the event dispatch thread the + /// runnable simply runs. + public static void invokeAndWait(Runnable doRun) throws InterruptedException { + EventQueue.invokeAndWait(doRun); + } + + public static boolean isEventDispatchThread() { + return EventQueue.isDispatchThread(); + } + + public static Window getWindowAncestor(Component c) { + for (Container p = c.getParent(); p != null; p = p.getParent()) { + if (p instanceof Window) { + return (Window) p; + } + } + return null; + } + + public static Window windowForComponent(Component c) { + return getWindowAncestor(c); + } + + public static Component getRoot(Component c) { + for (Component p = c; p != null; p = p.getParent()) { + if (p instanceof Window) { + return p; + } + } + return null; + } + + public static JRootPane getRootPane(Component c) { + for (Component p = c; p != null; p = p.getParent()) { + if (p instanceof JRootPane) { + return (JRootPane) p; + } + if (p instanceof JFrame) { + return ((JFrame) p).getRootPane(); + } + if (p instanceof JDialog) { + return ((JDialog) p).getRootPane(); + } + } + return null; + } + + public static Container getAncestorOfClass(Class c, Component comp) { + if (comp == null || c == null) { + return null; + } + Container parent = comp.getParent(); + while (parent != null && !c.isInstance(parent)) { + parent = parent.getParent(); + } + return parent; + } + + public static boolean isDescendingFrom(Component a, Component b) { + if (a == b) { + return true; + } + for (Container p = a.getParent(); p != null; p = p.getParent()) { + if (p == b) { + return true; + } + } + return false; + } + + public static Component getDeepestComponentAt(Component parent, int x, int y) { + if (!parent.contains(x, y)) { + return null; + } + if (parent instanceof Container) { + return ((Container) parent).findComponentAt(x, y); + } + return parent; + } + + public static void convertPointToScreen(Point p, Component c) { + Point s = c.getLocationOnScreen(); + p.x += s.x; + p.y += s.y; + } + + public static void convertPointFromScreen(Point p, Component c) { + Point s = c.getLocationOnScreen(); + p.x -= s.x; + p.y -= s.y; + } + + public static Point convertPoint(Component source, int x, int y, Component destination) { + Point p = new Point(x, y); + if (source != null) { + convertPointToScreen(p, source); + } + if (destination != null) { + convertPointFromScreen(p, destination); + } + return p; + } + + public static Point convertPoint(Component source, Point aPoint, Component destination) { + return convertPoint(source, aPoint.x, aPoint.y, destination); + } + + public static boolean isLeftMouseButton(MouseEvent anEvent) { + return anEvent.getButton() == MouseEvent.BUTTON1; + } + + public static boolean isMiddleMouseButton(MouseEvent anEvent) { + return anEvent.getButton() == MouseEvent.BUTTON2; + } + + public static boolean isRightMouseButton(MouseEvent anEvent) { + return anEvent.getButton() == MouseEvent.BUTTON3; + } + + public static int computeStringWidth(FontMetrics fm, String str) { + return str == null ? 0 : fm.stringWidth(str); + } + + /// Does nothing: there are no UI delegates to refresh. + public static void updateComponentTreeUI(Component c) { + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Timer.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Timer.java new file mode 100644 index 00000000000..bff3c40c5a0 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/Timer.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.EventQueue; +import com.codename1.desktopcompat.java.awt.event.ActionEvent; +import com.codename1.desktopcompat.java.awt.event.ActionListener; +import com.codename1.desktopcompat.javax.swing.event.EventListenerList; +import java.util.TimerTask; + +/// Fires action events on the event dispatch thread after a delay, once +/// or repeatedly. Start and stop it from the event dispatch thread. +public class Timer { + + protected EventListenerList listenerList = new EventListenerList(); + + private int delay; + private int initialDelay; + private boolean repeats = true; + private boolean coalesce = true; + private String actionCommand; + private java.util.Timer timer; + private int run; + + public Timer(int delay, ActionListener listener) { + this.delay = delay; + this.initialDelay = delay; + if (listener != null) { + addActionListener(listener); + } + } + + public void addActionListener(ActionListener listener) { + listenerList.add(ActionListener.class, listener); + } + + public void removeActionListener(ActionListener listener) { + listenerList.remove(ActionListener.class, listener); + } + + public ActionListener[] getActionListeners() { + return listenerList.getListeners(ActionListener.class); + } + + protected void fireActionPerformed(ActionEvent e) { + ActionListener[] ls = getActionListeners(); + for (int i = ls.length - 1; i >= 0; i--) { + ls[i].actionPerformed(e); + } + } + + public int getDelay() { + return delay; + } + + /// Takes effect the next time the timer is started. + public void setDelay(int delay) { + if (delay < 0) { + throw new IllegalArgumentException("Invalid delay: " + delay); + } + this.delay = delay; + } + + public int getInitialDelay() { + return initialDelay; + } + + public void setInitialDelay(int initialDelay) { + if (initialDelay < 0) { + throw new IllegalArgumentException("Invalid initial delay: " + initialDelay); + } + this.initialDelay = initialDelay; + } + + public boolean isRepeats() { + return repeats; + } + + public void setRepeats(boolean flag) { + repeats = flag; + } + + public boolean isCoalesce() { + return coalesce; + } + + public void setCoalesce(boolean flag) { + coalesce = flag; + } + + public String getActionCommand() { + return actionCommand; + } + + public void setActionCommand(String command) { + actionCommand = command; + } + + public boolean isRunning() { + return timer != null; + } + + public void start() { + if (timer != null) { + return; + } + run++; + final int mine = run; + final Runnable tick = new Runnable() { + @Override + public void run() { + tick(mine); + } + }; + TimerTask task = new Hop(tick); + timer = new java.util.Timer(); + if (repeats) { + timer.schedule(task, initialDelay, Math.max(1, delay)); + } else { + timer.schedule(task, initialDelay); + } + } + + /// Runs on the event dispatch thread; a tick queued before the timer + /// was stopped or restarted is dropped. + private void tick(int ticket) { + if (timer == null || ticket != run) { + return; + } + if (!repeats) { + stop(); + } + fireActionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, actionCommand, + System.currentTimeMillis(), 0)); + } + + public void stop() { + if (timer != null) { + timer.cancel(); + timer = null; + } + } + + public void restart() { + stop(); + start(); + } + + /// Carries a tick from the timer's thread to the event dispatch thread. + private static final class Hop extends TimerTask { + private final Runnable tick; + + Hop(Runnable tick) { + this.tick = tick; + } + + @Override + public void run() { + EventQueue.invokeLater(tick); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/UIManager.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/UIManager.java new file mode 100644 index 00000000000..5d32a4fe0f2 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/UIManager.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Font; +import com.codename1.desktopcompat.rt.EventBridge; +import com.codename1.desktopcompat.rt.Fonts; +import java.util.HashMap; + +/// A table of look and feel defaults. +/// +/// There is one look, the Codename One theme, so setting a look and feel +/// does nothing. A key that was never `put` answers from the theme by its +/// ending: `.background`, `.foreground` and `.font` give the window +/// background, the label color and the default font; any other key gives +/// `null`. +public class UIManager { + + private static final HashMap VALUES = new HashMap(); + + public UIManager() { + } + + public static Object get(Object key) { + Object v = VALUES.get(key); + if (v != null || !(key instanceof String)) { + return v; + } + String k = (String) key; + if (k.endsWith(".background")) { + return EventBridge.defaultBackground(); + } + if (k.endsWith(".foreground")) { + return EventBridge.defaultForeground(); + } + if (k.endsWith(".font")) { + return Fonts.defaultFont(); + } + return null; + } + + public static Object put(Object key, Object value) { + return value == null ? VALUES.remove(key) : VALUES.put(key, value); + } + + public static Color getColor(Object key) { + Object v = get(key); + return v instanceof Color ? (Color) v : null; + } + + public static Font getFont(Object key) { + Object v = get(key); + return v instanceof Font ? (Font) v : null; + } + + public static String getString(Object key) { + Object v = get(key); + return v instanceof String ? (String) v : null; + } + + public static int getInt(Object key) { + Object v = get(key); + return v instanceof Integer ? ((Integer) v).intValue() : 0; + } + + public static boolean getBoolean(Object key) { + Object v = get(key); + return v instanceof Boolean && ((Boolean) v).booleanValue(); + } + + public static Icon getIcon(Object key) { + Object v = get(key); + return v instanceof Icon ? (Icon) v : null; + } + + /// Does nothing; see the class description. + public static void setLookAndFeel(String className) { + } + + public static String getSystemLookAndFeelClassName() { + return "javax.swing.plaf.metal.MetalLookAndFeel"; + } + + public static String getCrossPlatformLookAndFeelClassName() { + return "javax.swing.plaf.metal.MetalLookAndFeel"; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/UnsupportedLookAndFeelException.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/UnsupportedLookAndFeelException.java new file mode 100644 index 00000000000..39fdb7a5897 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/UnsupportedLookAndFeelException.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +/// Declared so that code catching it links; nothing throws it. +public class UnsupportedLookAndFeelException extends Exception { + + public UnsupportedLookAndFeelException(String s) { + super(s); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/WindowConstants.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/WindowConstants.java new file mode 100644 index 00000000000..b377359add6 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/WindowConstants.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +/// The operations a window can perform when the user closes it. +public interface WindowConstants { + + int DO_NOTHING_ON_CLOSE = 0; + + int HIDE_ON_CLOSE = 1; + + int DISPOSE_ON_CLOSE = 2; +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/AbstractBorder.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/AbstractBorder.java new file mode 100644 index 00000000000..20a7b0a035b --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/AbstractBorder.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.border; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Insets; +import com.codename1.desktopcompat.java.awt.Rectangle; + +/// A border that takes no space and paints nothing, as the base the other +/// borders build on. +public abstract class AbstractBorder implements Border { + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + } + + @Override + public Insets getBorderInsets(Component c) { + return getBorderInsets(c, new Insets(0, 0, 0, 0)); + } + + public Insets getBorderInsets(Component c, Insets insets) { + insets.left = 0; + insets.top = 0; + insets.right = 0; + insets.bottom = 0; + return insets; + } + + @Override + public boolean isBorderOpaque() { + return false; + } + + public Rectangle getInteriorRectangle(Component c, int x, int y, int width, int height) { + return getInteriorRectangle(c, this, x, y, width, height); + } + + public static Rectangle getInteriorRectangle(Component c, Border b, int x, int y, int width, int height) { + Insets in = b != null ? b.getBorderInsets(c) : new Insets(0, 0, 0, 0); + return new Rectangle(x + in.left, y + in.top, width - in.right - in.left, height - in.top - in.bottom); + } + + public int getBaseline(Component c, int width, int height) { + if (width < 0 || height < 0) { + throw new IllegalArgumentException("Width and height must be >= 0"); + } + return -1; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/BevelBorder.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/BevelBorder.java new file mode 100644 index 00000000000..a393d521a88 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/BevelBorder.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.border; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Insets; + +/// A two pixel bevel that makes its component look raised or sunken. +/// +/// Colours not given are derived from the background of the component the +/// border is painted on, or from light gray when it has none. +public class BevelBorder extends AbstractBorder { + + public static final int RAISED = 0; + + public static final int LOWERED = 1; + + protected int bevelType; + + protected Color highlightOuter; + + protected Color highlightInner; + + protected Color shadowInner; + + protected Color shadowOuter; + + public BevelBorder(int bevelType) { + this.bevelType = bevelType; + } + + public BevelBorder(int bevelType, Color highlight, Color shadow) { + this(bevelType, highlight.brighter(), highlight, shadow, shadow.brighter()); + } + + public BevelBorder(int bevelType, Color highlightOuterColor, Color highlightInnerColor, Color shadowOuterColor, + Color shadowInnerColor) { + this(bevelType); + this.highlightOuter = highlightOuterColor; + this.highlightInner = highlightInnerColor; + this.shadowOuter = shadowOuterColor; + this.shadowInner = shadowInnerColor; + } + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + if (bevelType == RAISED) { + paintRaisedBevel(c, g, x, y, width, height); + } else if (bevelType == LOWERED) { + paintLoweredBevel(c, g, x, y, width, height); + } + } + + @Override + public Insets getBorderInsets(Component c, Insets insets) { + insets.set(2, 2, 2, 2); + return insets; + } + + public Color getHighlightOuterColor(Component c) { + return highlightOuter != null ? highlightOuter : EtchedBorder.base(c).brighter().brighter(); + } + + public Color getHighlightInnerColor(Component c) { + return highlightInner != null ? highlightInner : EtchedBorder.base(c).brighter(); + } + + public Color getShadowInnerColor(Component c) { + return shadowInner != null ? shadowInner : EtchedBorder.base(c).darker(); + } + + public Color getShadowOuterColor(Component c) { + return shadowOuter != null ? shadowOuter : EtchedBorder.base(c).darker().darker(); + } + + public Color getHighlightOuterColor() { + return highlightOuter; + } + + public Color getHighlightInnerColor() { + return highlightInner; + } + + public Color getShadowInnerColor() { + return shadowInner; + } + + public Color getShadowOuterColor() { + return shadowOuter; + } + + public int getBevelType() { + return bevelType; + } + + @Override + public boolean isBorderOpaque() { + return true; + } + + /// Two nested L shapes on the top left and two on the bottom right. + private static void bevel(Graphics g, int x, int y, int w, int h, Color topOuter, Color topInner, + Color bottomOuter, Color bottomInner) { + Color old = g.getColor(); + g.setColor(topOuter); + g.drawLine(x, y, x, y + h - 2); + g.drawLine(x + 1, y, x + w - 2, y); + g.setColor(topInner); + g.drawLine(x + 1, y + 1, x + 1, y + h - 3); + g.drawLine(x + 2, y + 1, x + w - 3, y + 1); + g.setColor(bottomOuter); + g.drawLine(x, y + h - 1, x + w - 1, y + h - 1); + g.drawLine(x + w - 1, y, x + w - 1, y + h - 2); + g.setColor(bottomInner); + g.drawLine(x + 1, y + h - 2, x + w - 2, y + h - 2); + g.drawLine(x + w - 2, y + 1, x + w - 2, y + h - 3); + g.setColor(old); + } + + protected void paintRaisedBevel(Component c, Graphics g, int x, int y, int width, int height) { + bevel(g, x, y, width, height, getHighlightOuterColor(c), getHighlightInnerColor(c), getShadowOuterColor(c), + getShadowInnerColor(c)); + } + + protected void paintLoweredBevel(Component c, Graphics g, int x, int y, int width, int height) { + bevel(g, x, y, width, height, getShadowInnerColor(c), getShadowOuterColor(c), getHighlightOuterColor(c), + getHighlightInnerColor(c)); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/Border.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/Border.java new file mode 100644 index 00000000000..e7b71eb0ffa --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/Border.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.border; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Insets; + +/// The decoration drawn around the edge of a component, and the space it +/// takes from the component's interior. +public interface Border { + + void paintBorder(Component c, Graphics g, int x, int y, int width, int height); + + Insets getBorderInsets(Component c); + + boolean isBorderOpaque(); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/CompoundBorder.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/CompoundBorder.java new file mode 100644 index 00000000000..4836c5deaaf --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/CompoundBorder.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.border; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Insets; + +/// Two borders nested, the inside one drawn within the insets of the +/// outside one. +public class CompoundBorder extends AbstractBorder { + + protected Border outsideBorder; + + protected Border insideBorder; + + public CompoundBorder() { + this.outsideBorder = null; + this.insideBorder = null; + } + + public CompoundBorder(Border outsideBorder, Border insideBorder) { + this.outsideBorder = outsideBorder; + this.insideBorder = insideBorder; + } + + @Override + public boolean isBorderOpaque() { + return (outsideBorder == null || outsideBorder.isBorderOpaque()) + && (insideBorder == null || insideBorder.isBorderOpaque()); + } + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + int px = x; + int py = y; + int pw = width; + int ph = height; + if (outsideBorder != null) { + outsideBorder.paintBorder(c, g, px, py, pw, ph); + Insets in = outsideBorder.getBorderInsets(c); + px += in.left; + py += in.top; + pw = pw - in.right - in.left; + ph = ph - in.bottom - in.top; + } + if (insideBorder != null) { + insideBorder.paintBorder(c, g, px, py, pw, ph); + } + } + + @Override + public Insets getBorderInsets(Component c, Insets insets) { + insets.top = 0; + insets.left = 0; + insets.right = 0; + insets.bottom = 0; + if (outsideBorder != null) { + Insets in = outsideBorder.getBorderInsets(c); + insets.top += in.top; + insets.left += in.left; + insets.right += in.right; + insets.bottom += in.bottom; + } + if (insideBorder != null) { + Insets in = insideBorder.getBorderInsets(c); + insets.top += in.top; + insets.left += in.left; + insets.right += in.right; + insets.bottom += in.bottom; + } + return insets; + } + + public Border getOutsideBorder() { + return outsideBorder; + } + + public Border getInsideBorder() { + return insideBorder; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/EmptyBorder.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/EmptyBorder.java new file mode 100644 index 00000000000..4b22e286cab --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/EmptyBorder.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.border; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Insets; + +/// A border that takes space and paints nothing. +public class EmptyBorder extends AbstractBorder { + + protected int left; + + protected int right; + + protected int top; + + protected int bottom; + + public EmptyBorder(int top, int left, int bottom, int right) { + this.top = top; + this.right = right; + this.bottom = bottom; + this.left = left; + } + + public EmptyBorder(Insets borderInsets) { + this.top = borderInsets.top; + this.right = borderInsets.right; + this.bottom = borderInsets.bottom; + this.left = borderInsets.left; + } + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + } + + @Override + public Insets getBorderInsets(Component c, Insets insets) { + insets.left = left; + insets.top = top; + insets.right = right; + insets.bottom = bottom; + return insets; + } + + public Insets getBorderInsets() { + return new Insets(top, left, bottom, right); + } + + @Override + public boolean isBorderOpaque() { + return false; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/EtchedBorder.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/EtchedBorder.java new file mode 100644 index 00000000000..31b6f4e8410 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/EtchedBorder.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.border; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Insets; + +/// A two pixel groove (or ridge) drawn in a highlight and a shadow colour. +/// +/// Colours not given are derived from the background of the component the +/// border is painted on, or from light gray when it has none. +public class EtchedBorder extends AbstractBorder { + + public static final int RAISED = 0; + + public static final int LOWERED = 1; + + protected int etchType; + + protected Color highlight; + + protected Color shadow; + + public EtchedBorder() { + this(LOWERED); + } + + public EtchedBorder(int etchType) { + this(etchType, null, null); + } + + public EtchedBorder(Color highlight, Color shadow) { + this(LOWERED, highlight, shadow); + } + + public EtchedBorder(int etchType, Color highlight, Color shadow) { + this.etchType = etchType; + this.highlight = highlight; + this.shadow = shadow; + } + + /// The colour highlights and shadows are derived from. + static Color base(Component c) { + Color bg = c == null ? null : c.getBackground(); + return bg == null ? Color.lightGray : bg; + } + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + Color old = g.getColor(); + Color light = getHighlightColor(c); + Color dark = getShadowColor(c); + int w = width; + int h = height; + g.setColor(etchType == LOWERED ? dark : light); + g.drawRect(x, y, w - 2, h - 2); + g.setColor(etchType == LOWERED ? light : dark); + g.drawLine(x + 1, y + h - 3, x + 1, y + 1); + g.drawLine(x + 1, y + 1, x + w - 3, y + 1); + g.drawLine(x, y + h - 1, x + w - 1, y + h - 1); + g.drawLine(x + w - 1, y + h - 1, x + w - 1, y); + g.setColor(old); + } + + @Override + public Insets getBorderInsets(Component c, Insets insets) { + insets.set(2, 2, 2, 2); + return insets; + } + + @Override + public boolean isBorderOpaque() { + return true; + } + + public int getEtchType() { + return etchType; + } + + public Color getHighlightColor(Component c) { + return highlight != null ? highlight : base(c).brighter(); + } + + public Color getHighlightColor() { + return highlight; + } + + public Color getShadowColor(Component c) { + return shadow != null ? shadow : base(c).darker(); + } + + public Color getShadowColor() { + return shadow; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/LineBorder.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/LineBorder.java new file mode 100644 index 00000000000..bebbd6b55b0 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/LineBorder.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.border; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Insets; + +/// A border of one colour and a fixed thickness, with square or rounded +/// corners. +/// +/// The line is drawn as filled bands, or for rounded corners as nested one +/// pixel outlines, so it is not antialiased. +public class LineBorder extends AbstractBorder { + + private static final Border BLACK_LINE = new LineBorder(Color.black, 1); + private static final Border GRAY_LINE = new LineBorder(Color.gray, 1); + + protected int thickness; + + protected Color lineColor; + + protected boolean roundedCorners; + + public static Border createBlackLineBorder() { + return BLACK_LINE; + } + + public static Border createGrayLineBorder() { + return GRAY_LINE; + } + + public LineBorder(Color color) { + this(color, 1, false); + } + + public LineBorder(Color color, int thickness) { + this(color, thickness, false); + } + + public LineBorder(Color color, int thickness, boolean roundedCorners) { + lineColor = color; + this.thickness = thickness; + this.roundedCorners = roundedCorners; + } + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + if (thickness <= 0 || lineColor == null) { + return; + } + Color old = g.getColor(); + g.setColor(lineColor); + if (roundedCorners) { + for (int i = 0; i < thickness; i++) { + int arc = (thickness - i) * 2; + g.drawRoundRect(x + i, y + i, width - i - i - 1, height - i - i - 1, arc, arc); + } + } else if (thickness * 2 >= Math.min(width, height)) { + g.fillRect(x, y, width, height); + } else { + g.fillRect(x, y, width, thickness); + g.fillRect(x, y + height - thickness, width, thickness); + g.fillRect(x, y + thickness, thickness, height - thickness * 2); + g.fillRect(x + width - thickness, y + thickness, thickness, height - thickness * 2); + } + g.setColor(old); + } + + @Override + public Insets getBorderInsets(Component c, Insets insets) { + insets.set(thickness, thickness, thickness, thickness); + return insets; + } + + public Color getLineColor() { + return lineColor; + } + + public int getThickness() { + return thickness; + } + + public boolean getRoundedCorners() { + return roundedCorners; + } + + @Override + public boolean isBorderOpaque() { + return !roundedCorners; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/MatteBorder.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/MatteBorder.java new file mode 100644 index 00000000000..9294240a9f1 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/MatteBorder.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.border; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Insets; +import com.codename1.desktopcompat.javax.swing.Icon; + +/// A border filled with a solid colour or tiled with an icon. +/// +/// The icon variant paints by repeating `Icon.paintIcon` across each edge, +/// clipped to it; the tiles start at the border's top left corner. +public class MatteBorder extends EmptyBorder { + + protected Color color; + + protected Icon tileIcon; + + public MatteBorder(int top, int left, int bottom, int right, Color matteColor) { + super(top, left, bottom, right); + this.color = matteColor; + } + + public MatteBorder(Insets borderInsets, Color matteColor) { + super(borderInsets); + this.color = matteColor; + } + + public MatteBorder(int top, int left, int bottom, int right, Icon tileIcon) { + super(top, left, bottom, right); + this.tileIcon = tileIcon; + } + + public MatteBorder(Insets borderInsets, Icon tileIcon) { + super(borderInsets); + this.tileIcon = tileIcon; + } + + public MatteBorder(Icon tileIcon) { + this(-1, -1, -1, -1, tileIcon); + } + + /// The insets in force: those given, or the icon's size on every side + /// for a border built from an icon alone. + private Insets current() { + if (tileIcon != null && top == -1 && bottom == -1 && left == -1 && right == -1) { + int w = tileIcon.getIconWidth(); + int h = tileIcon.getIconHeight(); + return new Insets(h, w, h, w); + } + return new Insets(top, left, bottom, right); + } + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + Insets in = current(); + if (color != null) { + Color old = g.getColor(); + g.setColor(color); + g.fillRect(x, y, width - in.right, in.top); + g.fillRect(x, y + in.top, in.left, height - in.top); + g.fillRect(x + in.left, y + height - in.bottom, width - in.left, in.bottom); + g.fillRect(x + width - in.right, y, in.right, height - in.bottom); + g.setColor(old); + } else if (tileIcon != null) { + tile(c, g, x, y, x, y, width - in.right, in.top); + tile(c, g, x, y, x, y + in.top, in.left, height - in.top); + tile(c, g, x, y, x + in.left, y + height - in.bottom, width - in.left, in.bottom); + tile(c, g, x, y, x + width - in.right, y, in.right, height - in.bottom); + } + } + + /// Paints the tiles that touch one edge rectangle, on the grid that + /// starts at the border's origin. + private void tile(Component c, Graphics g, int x, int y, int rx, int ry, int rw, int rh) { + int tw = tileIcon.getIconWidth(); + int th = tileIcon.getIconHeight(); + if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) { + return; + } + Graphics clipped = g.create(); + clipped.clipRect(rx, ry, rw, rh); + int firstX = x + ((rx - x) / tw) * tw; + int firstY = y + ((ry - y) / th) * th; + for (int ty = firstY; ty < ry + rh; ty += th) { + for (int tx = firstX; tx < rx + rw; tx += tw) { + tileIcon.paintIcon(c, clipped, tx, ty); + } + } + clipped.dispose(); + } + + @Override + public Insets getBorderInsets(Component c, Insets insets) { + Insets in = current(); + insets.set(in.top, in.left, in.bottom, in.right); + return insets; + } + + @Override + public Insets getBorderInsets() { + return current(); + } + + public Color getMatteColor() { + return color; + } + + public Icon getTileIcon() { + return tileIcon; + } + + @Override + public boolean isBorderOpaque() { + return color != null; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/TitledBorder.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/TitledBorder.java new file mode 100644 index 00000000000..9b4dfc84572 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/TitledBorder.java @@ -0,0 +1,404 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.border; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.Font; +import com.codename1.desktopcompat.java.awt.FontMetrics; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Insets; + +/// A border with a title drawn on or beside its top or bottom edge, the +/// edge broken where the text crosses it. +/// +/// There is no look and feel here, so the defaults are fixed: a border +/// without one of its own draws a lowered [EtchedBorder], and a title +/// without a font or colour takes those of the component it is painted on. +/// Every container is laid out left to right, so `LEADING` is the left and +/// `TRAILING` the right. A title too long for the border is clipped rather +/// than shortened with an ellipsis. +public class TitledBorder extends AbstractBorder { + + public static final int DEFAULT_POSITION = 0; + + public static final int ABOVE_TOP = 1; + + public static final int TOP = 2; + + public static final int BELOW_TOP = 3; + + public static final int ABOVE_BOTTOM = 4; + + public static final int BOTTOM = 5; + + public static final int BELOW_BOTTOM = 6; + + public static final int DEFAULT_JUSTIFICATION = 0; + + public static final int LEFT = 1; + + public static final int CENTER = 2; + + public static final int RIGHT = 3; + + public static final int LEADING = 4; + + public static final int TRAILING = 5; + + protected static final int EDGE_SPACING = 2; + + protected static final int TEXT_SPACING = 2; + + protected static final int TEXT_INSET_H = 5; + + private static final Border FALLBACK = new EtchedBorder(EtchedBorder.LOWERED); + + protected String title; + + protected Border border; + + protected int titlePosition; + + protected int titleJustification; + + protected Font titleFont; + + protected Color titleColor; + + public TitledBorder(String title) { + this(null, title, LEADING, DEFAULT_POSITION, null, null); + } + + public TitledBorder(Border border) { + this(border, "", LEADING, DEFAULT_POSITION, null, null); + } + + public TitledBorder(Border border, String title) { + this(border, title, LEADING, DEFAULT_POSITION, null, null); + } + + public TitledBorder(Border border, String title, int titleJustification, int titlePosition) { + this(border, title, titleJustification, titlePosition, null, null); + } + + public TitledBorder(Border border, String title, int titleJustification, int titlePosition, Font titleFont) { + this(border, title, titleJustification, titlePosition, titleFont, null); + } + + public TitledBorder(Border border, String title, int titleJustification, int titlePosition, Font titleFont, + Color titleColor) { + this.title = title; + this.border = border; + this.titleFont = titleFont; + this.titleColor = titleColor; + checkJustification(titleJustification); + checkPosition(titlePosition); + this.titleJustification = titleJustification; + this.titlePosition = titlePosition; + } + + private static void checkPosition(int titlePosition) { + if (titlePosition < DEFAULT_POSITION || titlePosition > BELOW_BOTTOM) { + throw new IllegalArgumentException(titlePosition + " is not a valid title position."); + } + } + + private static void checkJustification(int titleJustification) { + if (titleJustification < DEFAULT_JUSTIFICATION || titleJustification > TRAILING) { + throw new IllegalArgumentException(titleJustification + " is not a valid title justification."); + } + } + + private boolean hasTitle() { + return title != null && title.length() > 0; + } + + private int position() { + return titlePosition == DEFAULT_POSITION ? TOP : titlePosition; + } + + private Border frame() { + return border != null ? border : FALLBACK; + } + + private static FontMetrics metrics(Component c, Font f) { + return c == null || f == null ? null : c.getFontMetrics(f); + } + + /// The height of a line of the title, estimated from the font size when + /// the component cannot measure it. + private static int lineHeight(FontMetrics fm, Font f) { + if (fm != null) { + return fm.getHeight(); + } + return f != null ? f.getSize() + f.getSize() / 4 : 15; + } + + private int textWidth(FontMetrics fm, Font f) { + if (fm != null) { + return fm.stringWidth(title); + } + return title.length() * (f != null ? f.getSize() : 12) / 2; + } + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + Border frame = frame(); + if (!hasTitle()) { + frame.paintBorder(c, g, x, y, width, height); + return; + } + Font font = getFont(c); + FontMetrics fm = metrics(c, font); + if (fm == null) { + fm = g.getFontMetrics(font); + } + int th = lineHeight(fm, font); + int ascent = fm != null ? fm.getAscent() : th * 4 / 5; + Insets bi = frame.getBorderInsets(c); + + // The frame, inset from the component's edge, and the top of the + // line of text. + int bx = x + EDGE_SPACING; + int by = y + EDGE_SPACING; + int bw = width - EDGE_SPACING * 2; + int bh = height - EDGE_SPACING * 2; + int textTop; + int pos = position(); + switch (pos) { + case ABOVE_TOP: + textTop = by; + by += th; + bh -= th; + break; + case BELOW_TOP: + textTop = by + bi.top + TEXT_SPACING; + break; + case ABOVE_BOTTOM: + textTop = by + bh - bi.bottom - TEXT_SPACING - th; + break; + case BOTTOM: { + int drop = th > bi.bottom ? (th - bi.bottom) / 2 : 0; + bh -= drop; + textTop = by + bh - bi.bottom + (bi.bottom - th) / 2; + break; + } + case BELOW_BOTTOM: + bh -= th; + textTop = by + bh; + break; + default: { + int drop = th > bi.top ? (th - bi.top) / 2 : 0; + by += drop; + bh -= drop; + textTop = by + (bi.top - th) / 2; + break; + } + } + + int room = bw - bi.left - bi.right - TEXT_INSET_H * 2; + int tw = Math.min(textWidth(fm, font), Math.max(room, 0)); + int tx; + switch (titleJustification) { + case CENTER: + tx = bx + (bw - tw) / 2; + break; + case RIGHT: + case TRAILING: + tx = bx + bw - bi.right - TEXT_INSET_H - tw; + break; + default: + tx = bx + bi.left + TEXT_INSET_H; + break; + } + + if (pos == TOP || pos == BOTTOM) { + // The frame in three pieces, leaving out the stretch of edge + // the text sits on. + int gapLeft = tx - TEXT_SPACING; + int gapRight = tx + tw + TEXT_SPACING; + paintClipped(frame, c, g, bx, by, bw, bh, x, y, gapLeft - x, height); + paintClipped(frame, c, g, bx, by, bw, bh, gapRight, y, x + width - gapRight, height); + if (pos == TOP) { + int below = textTop + th; + paintClipped(frame, c, g, bx, by, bw, bh, gapLeft, below, gapRight - gapLeft, y + height - below); + } else { + paintClipped(frame, c, g, bx, by, bw, bh, gapLeft, y, gapRight - gapLeft, textTop - y); + } + } else { + frame.paintBorder(c, g, bx, by, bw, bh); + } + + Color color = getColor(c); + Graphics text = g.create(); + text.clipRect(tx, textTop, tw, th); + text.setFont(font); + if (color != null) { + text.setColor(color); + } + text.drawString(title, tx, textTop + ascent); + text.dispose(); + } + + private static void paintClipped(Border frame, Component c, Graphics g, int bx, int by, int bw, int bh, int cx, + int cy, int cw, int ch) { + if (cw <= 0 || ch <= 0) { + return; + } + Graphics part = g.create(); + part.clipRect(cx, cy, cw, ch); + frame.paintBorder(c, part, bx, by, bw, bh); + part.dispose(); + } + + @Override + public Insets getBorderInsets(Component c, Insets insets) { + Insets bi = frame().getBorderInsets(c); + int top = bi.top; + int bottom = bi.bottom; + if (hasTitle()) { + Font font = getFont(c); + int th = lineHeight(metrics(c, font), font); + switch (position()) { + case ABOVE_TOP: + top += th; + break; + case BELOW_TOP: + top += th + TEXT_SPACING; + break; + case ABOVE_BOTTOM: + bottom += th + TEXT_SPACING; + break; + case BOTTOM: + bottom = Math.max(bottom, th); + break; + case BELOW_BOTTOM: + bottom += th; + break; + default: + top = Math.max(top, th); + break; + } + } + int pad = EDGE_SPACING + TEXT_SPACING; + insets.set(top + pad, bi.left + pad, bottom + pad, bi.right + pad); + return insets; + } + + @Override + public boolean isBorderOpaque() { + return false; + } + + public String getTitle() { + return title; + } + + /// The border drawn around the title. Without a look and feel there is + /// no default to hand out, so this is null until one is set; painting + /// then uses a lowered etched border. + public Border getBorder() { + return border; + } + + public int getTitlePosition() { + return titlePosition; + } + + public int getTitleJustification() { + return titleJustification; + } + + /// The font set on this border, or null when the title takes the font + /// of the component it is painted on. + public Font getTitleFont() { + return titleFont; + } + + /// The colour set on this border, or null when the title takes the + /// foreground of the component it is painted on. + public Color getTitleColor() { + return titleColor; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setBorder(Border border) { + this.border = border; + } + + public void setTitlePosition(int titlePosition) { + checkPosition(titlePosition); + this.titlePosition = titlePosition; + } + + public void setTitleJustification(int titleJustification) { + checkJustification(titleJustification); + this.titleJustification = titleJustification; + } + + public void setTitleFont(Font titleFont) { + this.titleFont = titleFont; + } + + public void setTitleColor(Color titleColor) { + this.titleColor = titleColor; + } + + public Dimension getMinimumSize(Component c) { + Insets in = getBorderInsets(c); + Dimension min = new Dimension(in.right + in.left, in.top + in.bottom); + if (hasTitle()) { + Font font = getFont(c); + int tw = textWidth(metrics(c, font), font); + int pos = position(); + if (pos == ABOVE_TOP || pos == BELOW_BOTTOM) { + if (min.width < tw) { + min.width = tw; + } + } else { + min.width += tw; + } + } + return min; + } + + protected Font getFont(Component c) { + if (titleFont != null) { + return titleFont; + } + Font f = c == null ? null : c.getFont(); + return f != null ? f : new Font("Dialog", Font.PLAIN, 12); + } + + private Color getColor(Component c) { + if (titleColor != null) { + return titleColor; + } + return c == null ? null : c.getForeground(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/package-info.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/package-info.java new file mode 100644 index 00000000000..cad7cca41d2 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/border/package-info.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The borders a Swing component can be given: empty space, lines, bevels, +/// etched grooves, matte edges, titled frames and compounds of those. +/// +/// These classes stand in for the JDK's `javax.swing.border` package once an +/// application has been relocated onto the layer. A [Border] answers the +/// insets it takes from its component and paints itself into the edge of +/// the component's bounds; all coordinates are the same plain pixels the +/// desktop uses. There is no look and feel to supply defaults, so a +/// [TitledBorder] without a border of its own draws an etched one, and its +/// title takes the font and foreground of the component it decorates. +package com.codename1.desktopcompat.javax.swing.border; diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ChangeEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ChangeEvent.java new file mode 100644 index 00000000000..b6ba46b1333 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ChangeEvent.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.event; + +import java.util.EventObject; + +/// A model's state changed. +public class ChangeEvent extends EventObject { + + public ChangeEvent(Object source) { + super(source); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ChangeListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ChangeListener.java new file mode 100644 index 00000000000..73c9995b9b9 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ChangeListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.event; + +import java.util.EventListener; + +/// Receives the changes of a model's state. +public interface ChangeListener extends EventListener { + + void stateChanged(ChangeEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/EventListenerList.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/EventListenerList.java new file mode 100644 index 00000000000..542f02bdb5d --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/EventListenerList.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.event; + +import java.lang.reflect.Array; +import java.util.EventListener; + +/// A list of listeners of mixed types, stored as alternating class and +/// listener entries. +/// +/// The array behind `getListenerList()` is handed out as it is, as in the +/// JDK, so that firing code can walk it without copying; an add or remove +/// replaces it rather than editing it in place. The JDK class synchronizes +/// and serializes; this one does neither. +public class EventListenerList { + + private static final Object[] NULL_ARRAY = new Object[0]; + + protected Object[] listenerList = NULL_ARRAY; + + public Object[] getListenerList() { + return listenerList; + } + + @SuppressWarnings("unchecked") + public T[] getListeners(Class t) { + int count = getListenerCount(t); + T[] result = (T[]) Array.newInstance(t, count); + int j = 0; + for (int i = listenerList.length - 2; i >= 0; i -= 2) { + if (listenerList[i] == t) { + result[j++] = (T) listenerList[i + 1]; + } + } + return result; + } + + public int getListenerCount() { + return listenerList.length / 2; + } + + public int getListenerCount(Class t) { + int count = 0; + for (int i = 0; i < listenerList.length; i += 2) { + if (t == listenerList[i]) { + count++; + } + } + return count; + } + + public void add(Class t, T l) { + if (l == null) { + return; + } + if (!t.isInstance(l)) { + throw new IllegalArgumentException("Listener " + l + " is not of type " + t); + } + int length = listenerList.length; + Object[] tmp = new Object[length + 2]; + System.arraycopy(listenerList, 0, tmp, 0, length); + tmp[length] = t; + tmp[length + 1] = l; + listenerList = tmp; + } + + public void remove(Class t, T l) { + if (l == null) { + return; + } + if (!t.isInstance(l)) { + throw new IllegalArgumentException("Listener " + l + " is not of type " + t); + } + int index = -1; + for (int i = listenerList.length - 2; i >= 0; i -= 2) { + if (listenerList[i] == t && listenerList[i + 1].equals(l)) { + index = i; + break; + } + } + if (index == -1) { + return; + } + Object[] tmp = new Object[listenerList.length - 2]; + System.arraycopy(listenerList, 0, tmp, 0, index); + if (index < tmp.length) { + System.arraycopy(listenerList, index + 2, tmp, index, tmp.length - index); + } + listenerList = tmp.length == 0 ? NULL_ARRAY : tmp; + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder("EventListenerList: "); + s.append(listenerList.length / 2).append(" listeners: "); + for (int i = 0; i <= listenerList.length - 2; i += 2) { + s.append(" type ").append(((Class) listenerList[i]).getName()); + s.append(" listener ").append(listenerList[i + 1]); + } + return s.toString(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ListSelectionEvent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ListSelectionEvent.java new file mode 100644 index 00000000000..d70f00fcf19 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ListSelectionEvent.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.event; + +import java.util.EventObject; + +/// The selection of a list changed over a range of indexes. +public class ListSelectionEvent extends EventObject { + + private final int firstIndex; + + private final int lastIndex; + + private final boolean isAdjusting; + + public ListSelectionEvent(Object source, int firstIndex, int lastIndex, boolean isAdjusting) { + super(source); + this.firstIndex = firstIndex; + this.lastIndex = lastIndex; + this.isAdjusting = isAdjusting; + } + + public int getFirstIndex() { + return firstIndex; + } + + public int getLastIndex() { + return lastIndex; + } + + public boolean getValueIsAdjusting() { + return isAdjusting; + } + + @Override + public String toString() { + return getClass().getName() + " source=" + getSource() + " firstIndex= " + firstIndex + + " lastIndex= " + lastIndex + " isAdjusting= " + isAdjusting + " "; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ListSelectionListener.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ListSelectionListener.java new file mode 100644 index 00000000000..fcbb604ed0f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/ListSelectionListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.event; + +import java.util.EventListener; + +/// Receives the changes of a list selection. +public interface ListSelectionListener extends EventListener { + + void valueChanged(ListSelectionEvent e); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/SwingPropertyChangeSupport.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/SwingPropertyChangeSupport.java new file mode 100644 index 00000000000..26b5c09f7a1 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/SwingPropertyChangeSupport.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.event; + +import com.codename1.desktopcompat.java.beans.PropertyChangeEvent; +import com.codename1.desktopcompat.java.beans.PropertyChangeSupport; + +/// A `PropertyChangeSupport` that is meant to fire on the event thread. +/// +/// The user interface has a single thread here, so events are always +/// delivered synchronously on the caller, whatever the `notifyOnEDT` flag +/// says. +public final class SwingPropertyChangeSupport extends PropertyChangeSupport { + + public SwingPropertyChangeSupport(Object sourceBean) { + super(sourceBean); + } + + public SwingPropertyChangeSupport(Object sourceBean, boolean notifyOnEDT) { + super(sourceBean); + } + + @Override + public void firePropertyChange(PropertyChangeEvent evt) { + super.firePropertyChange(evt); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/package-info.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/package-info.java new file mode 100644 index 00000000000..87737a449aa --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/event/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The `javax.swing.event` change, selection and listener-list classes shared +/// by the model and component classes of the Swing layer. +package com.codename1.desktopcompat.javax.swing.event; diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/package-info.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/package-info.java new file mode 100644 index 00000000000..7e4058f1884 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The Swing kernel: the component base class, the panel, label, button +/// and text field widgets backed by their Codename One counterparts, the +/// frame, borders, timers and the event dispatch thread helpers. +/// +/// A widget with a counterpart is drawn by it; see +/// [com.codename1.desktopcompat.javax.swing.JComponent] for how custom +/// painting combines with that. +package com.codename1.desktopcompat.javax.swing; diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/text/JTextComponent.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/text/JTextComponent.java new file mode 100644 index 00000000000..3d0d9bae330 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/text/JTextComponent.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing.text; + +import com.codename1.desktopcompat.javax.swing.JComponent; + +/// The base of the text components. The text lives in the Codename One +/// text widget once that exists; there is no document model, no caret and +/// no selection. +public abstract class JTextComponent extends JComponent { + + private String text = ""; + private boolean editable = true; + + public JTextComponent() { + } + + @Override + protected void cn1PeerCreated() { + super.cn1PeerCreated(); + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof com.codename1.ui.TextArea) { + com.codename1.ui.TextArea t = (com.codename1.ui.TextArea) p; + t.setText(text); + t.setEditable(editable); + } + } + + public String getText() { + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof com.codename1.ui.TextArea) { + String s = ((com.codename1.ui.TextArea) p).getText(); + text = s == null ? "" : s; + } + return text; + } + + public void setText(String t) { + text = t == null ? "" : t; + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof com.codename1.ui.TextArea) { + ((com.codename1.ui.TextArea) p).setText(text); + } + repaint(); + } + + public boolean isEditable() { + return editable; + } + + public void setEditable(boolean b) { + boolean old = editable; + editable = b; + com.codename1.ui.Component p = cn1PeerOrNull(); + if (p instanceof com.codename1.ui.TextArea) { + ((com.codename1.ui.TextArea) p).setEditable(b); + } + firePropertyChange("editable", old, b); + } + + /// Does nothing: there is no selection. + public void selectAll() { + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/text/package-info.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/text/package-info.java new file mode 100644 index 00000000000..5d44c29501f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/javax/swing/text/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The base class of the Swing text components. Documents, carets, +/// highlighters and editor kits are not part of this layer. +package com.codename1.desktopcompat.javax.swing.text; diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/ButtonPeer.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/ButtonPeer.java new file mode 100644 index 00000000000..400ea00f76f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/ButtonPeer.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.ui.Graphics; + +/// The peer of a push button: a Codename One button. +public class ButtonPeer extends com.codename1.ui.Button implements Peer { + + private final PeerSupport support; + + public ButtonPeer(Component owner) { + support = new PeerSupport(owner, this); + support.trackFocus(); + } + + @Override + public PeerSupport support() { + return support; + } + + @Override + public boolean nativeLook() { + return true; + } + + @Override + public void paintNativeLook(Graphics g) { + support.paintStyleBackground(g); + super.paint(g); + super.paintBorder(g); + } + + @Override + public void paintNativeChildren(Graphics g) { + } + + @Override + public void paint(Graphics g) { + support.paintOwner(g); + } + + @Override + protected void paintBorder(Graphics g) { + } + + @Override + public void repaint() { + if (support == null || !support.routeRepaint()) { + super.repaint(); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/CanvasPeer.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/CanvasPeer.java new file mode 100644 index 00000000000..b877cfc6836 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/CanvasPeer.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.ui.Graphics; + +/// The peer of a component that draws everything itself. +public class CanvasPeer extends com.codename1.ui.Component implements Peer { + + private final PeerSupport support; + + public CanvasPeer(Component owner) { + PeerSupport.strip(this); + setFocusable(false); + support = new PeerSupport(owner, this); + } + + @Override + public PeerSupport support() { + return support; + } + + @Override + public boolean nativeLook() { + return false; + } + + @Override + public void paintNativeLook(Graphics g) { + } + + @Override + public void paintNativeChildren(Graphics g) { + } + + @Override + public void paint(Graphics g) { + support.paintOwner(g); + } + + @Override + protected void paintBorder(Graphics g) { + } + + @Override + public void repaint() { + if (support == null || !support.routeRepaint()) { + super.repaint(); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/CheckBoxPeer.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/CheckBoxPeer.java new file mode 100644 index 00000000000..e5a0560d0a1 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/CheckBoxPeer.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.ui.Graphics; + +/// The peer of a check box, and of a toggle button when made a toggle: a +/// Codename One check box. +public class CheckBoxPeer extends com.codename1.ui.CheckBox implements Peer { + + private final PeerSupport support; + + public CheckBoxPeer(Component owner) { + support = new PeerSupport(owner, this); + support.trackFocus(); + } + + @Override + public PeerSupport support() { + return support; + } + + @Override + public boolean nativeLook() { + return true; + } + + @Override + public void paintNativeLook(Graphics g) { + support.paintStyleBackground(g); + super.paint(g); + super.paintBorder(g); + } + + @Override + public void paintNativeChildren(Graphics g) { + } + + @Override + public void paint(Graphics g) { + support.paintOwner(g); + } + + @Override + protected void paintBorder(Graphics g) { + } + + @Override + public void repaint() { + if (support == null || !support.routeRepaint()) { + super.repaint(); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/ContainerPeer.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/ContainerPeer.java new file mode 100644 index 00000000000..d48df68752f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/ContainerPeer.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.ui.Graphics; + +/// The peer of a container: its children are the peers of the container's +/// children, placed by a [LayoutBridge]. +public class ContainerPeer extends com.codename1.ui.Container implements Peer { + + private final PeerSupport support; + + public ContainerPeer(Container owner) { + super(new LayoutBridge(owner)); + PeerSupport.strip(this); + setScrollableX(false); + setScrollableY(false); + support = new PeerSupport(owner, this); + } + + @Override + public PeerSupport support() { + return support; + } + + @Override + public boolean nativeLook() { + return false; + } + + @Override + public void paintNativeLook(Graphics g) { + } + + @Override + public void paintNativeChildren(Graphics g) { + super.paint(g); + } + + @Override + public void paint(Graphics g) { + support.paintOwner(g); + } + + @Override + protected void paintBorder(Graphics g) { + } + + @Override + public void repaint() { + if (support == null || !support.routeRepaint()) { + super.repaint(); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/DesktopLifecycle.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/DesktopLifecycle.java new file mode 100644 index 00000000000..08b1db6e438 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/DesktopLifecycle.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.io.Log; +import com.codename1.system.Lifecycle; + +/// The Codename One lifecycle of a desktop application: when the +/// application starts it runs the application's `main` on the event +/// dispatch thread, which is where a Swing application builds its frames. +/// +/// The build generates the subclass that names the main class. +public abstract class DesktopLifecycle extends Lifecycle { + + /// Calls the application's `main`. + protected abstract void runMain() throws Exception; + + @Override + public void runApp() { + try { + runMain(); + } catch (Exception e) { + Log.e(e); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/DesktopToolkit.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/DesktopToolkit.java new file mode 100644 index 00000000000..0b9fa2c91d0 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/DesktopToolkit.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.EventQueue; +import com.codename1.desktopcompat.java.awt.Font; +import com.codename1.desktopcompat.java.awt.FontMetrics; +import com.codename1.desktopcompat.java.awt.Image; +import com.codename1.desktopcompat.java.awt.Toolkit; +import com.codename1.ui.Display; + +/// The one toolkit there is. +public final class DesktopToolkit extends Toolkit { + + private final EventQueue queue = new EventQueue(); + + @Override + public Dimension getScreenSize() { + if (!Display.isInitialized()) { + return new Dimension(0, 0); + } + Display d = Display.getInstance(); + return new Dimension(Units.toLogical(d.getDisplayWidth()), Units.toLogical(d.getDisplayHeight())); + } + + @Override + public int getScreenResolution() { + return 96; + } + + @Override + public FontMetrics getFontMetrics(Font font) { + return Fonts.metrics(font); + } + + @Override + public void sync() { + } + + @Override + public void beep() { + } + + @Override + public Image createImage(byte[] imagedata, int imageoffset, int imagelength) { + return new NativeImage(com.codename1.ui.Image.createImage(imagedata, imageoffset, imagelength)); + } + + @Override + protected EventQueue getSystemEventQueueImpl() { + return queue; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/EventBridge.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/EventBridge.java new file mode 100644 index 00000000000..3942b708e63 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/EventBridge.java @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.desktopcompat.java.awt.Window; +import com.codename1.desktopcompat.java.awt.event.FocusEvent; +import com.codename1.desktopcompat.java.awt.event.InputEvent; +import com.codename1.desktopcompat.java.awt.event.KeyEvent; +import com.codename1.desktopcompat.java.awt.event.MouseEvent; +import com.codename1.desktopcompat.java.awt.event.MouseWheelEvent; +import com.codename1.ui.Display; +import com.codename1.ui.events.PointerEvent; +import com.codename1.ui.plaf.UIManager; + +/// Turns Codename One input into AWT events. +/// +/// Pointer and key input enters at the form of a window. A pointer event +/// goes to the deepest visible component under the pointer that wants +/// mouse events, or the nearest ancestor that does; the component pressed +/// keeps receiving the drags and the release. Key events go to the focus +/// owner, or to the window when no component of it owns the focus. +public final class EventBridge { + + private static final int CLICK_INTERVAL = 500; + + private static Component focusOwner; + private static Component pressTarget; + private static Component hoverTarget; + private static long lastClickTime; + private static int clickCount; + private static int pressX; + private static int pressY; + private static int pressButton; + private static boolean dragged; + + private EventBridge() { + } + + // ------------------------------------------------------------ focus + + /// The component key events go to, or `null`. + public static Component focusOwner() { + return focusOwner; + } + + /// Moves the focus to `c`, firing focus lost and focus gained. + /// `toPeer` also focuses the Codename One widget behind it. + public static void setFocusOwner(Component c, boolean toPeer) { + Component old = focusOwner; + if (old == c) { + return; + } + focusOwner = c; + if (old != null) { + old.dispatchEvent(new FocusEvent(old, FocusEvent.FOCUS_LOST, false, c)); + } + if (c != null) { + c.dispatchEvent(new FocusEvent(c, FocusEvent.FOCUS_GAINED, false, old)); + com.codename1.ui.Component p = c.cn1PeerOrNull(); + if (toPeer && p instanceof Peer && ((Peer) p).nativeLook() && p.isFocusable() + && p.getComponentForm() != null) { + p.requestFocus(); + } + } + } + + /// The Codename One widget behind `c` lost the focus. + public static void nativeFocusLost(Component c) { + if (focusOwner == c) { + setFocusOwner(null, false); + } + } + + /// Drops every reference to a component that left its window. + public static void forget(Component c) { + if (focusOwner == c) { + focusOwner = null; + } + if (pressTarget == c) { + pressTarget = null; + } + if (hoverTarget == c) { + hoverTarget = null; + } + } + + // ------------------------------------------------------------ theme + + /// The background a window has when none was set: the theme's form + /// background, white with no display. + public static Color defaultBackground() { + if (!Display.isInitialized()) { + return Color.WHITE; + } + return new Color(UIManager.getInstance().getComponentStyle("Form").getBgColor() & 0xffffff); + } + + /// The foreground a window has when none was set: the theme's label + /// color, black with no display. + public static Color defaultForeground() { + if (!Display.isInitialized()) { + return Color.BLACK; + } + return new Color(UIManager.getInstance().getComponentStyle("Label").getFgColor() & 0xffffff); + } + + // ------------------------------------------------------------ pointer + + private static Component mouseTarget(Component hit) { + Component c = hit; + while (c != null && !c.cn1WantsMouse()) { + c = c.getParent(); + } + return c; + } + + private static int button() { + if (!Display.isInitialized()) { + return MouseEvent.BUTTON1; + } + PointerEvent pe = Display.getInstance().getCurrentPointerEvent(); + if (pe == null) { + return MouseEvent.BUTTON1; + } + switch (pe.getButton()) { + case PointerEvent.BUTTON_SECONDARY: + return MouseEvent.BUTTON3; + case PointerEvent.BUTTON_MIDDLE: + return MouseEvent.BUTTON2; + default: + return MouseEvent.BUTTON1; + } + } + + private static int keyModifiers() { + if (!Display.isInitialized()) { + return 0; + } + PointerEvent pe = Display.getInstance().getCurrentPointerEvent(); + int m = 0; + if (pe != null) { + if (pe.isShiftDown()) { + m |= InputEvent.SHIFT_MASK; + } + if (pe.isControlDown()) { + m |= InputEvent.CTRL_MASK; + } + if (pe.isAltDown()) { + m |= InputEvent.ALT_MASK; + } + if (pe.isMetaDown()) { + m |= InputEvent.META_MASK; + } + } + return m; + } + + private static int buttonMask(int button) { + switch (button) { + case MouseEvent.BUTTON1: + return InputEvent.BUTTON1_MASK; + case MouseEvent.BUTTON2: + return InputEvent.BUTTON2_MASK; + case MouseEvent.BUTTON3: + return InputEvent.BUTTON3_MASK; + default: + return 0; + } + } + + /// The position of a window relative point in `target`'s coordinates. + private static int[] local(Window w, Component target, int x, int y) { + int lx = x; + int ly = y; + for (Component c = target; c != null && c != w; c = c.getParent()) { + lx -= c.getX(); + ly -= c.getY(); + } + return new int[]{lx, ly}; + } + + private static void send(Window w, Component target, int id, int x, int y, int clicks, int button) { + int[] p = local(w, target, x, y); + int mods = keyModifiers() | buttonMask(button); + target.dispatchEvent(new MouseEvent(target, id, System.currentTimeMillis(), mods, p[0], p[1], clicks, + id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON3, button)); + } + + private static void hover(Window w, Component target, int x, int y) { + if (target == hoverTarget) { + return; + } + Component old = hoverTarget; + hoverTarget = target; + if (old != null) { + send(w, old, MouseEvent.MOUSE_EXITED, x, y, 0, MouseEvent.NOBUTTON); + } + if (target != null) { + send(w, target, MouseEvent.MOUSE_ENTERED, x, y, 0, MouseEvent.NOBUTTON); + } + } + + /// Delivers pointer input that reached the form of `w`. `id` is one of + /// `MOUSE_PRESSED`, `MOUSE_RELEASED`, `MOUSE_DRAGGED` and + /// `MOUSE_MOVED`; the position is in display coordinates. + public static void pointer(Window w, int id, int deviceX, int deviceY) { + com.codename1.ui.Component rp = w.cn1PeerOrNull(); + if (rp == null) { + return; + } + int x = Units.toLogical(deviceX - rp.getAbsoluteX()); + int y = Units.toLogical(deviceY - rp.getAbsoluteY()); + switch (id) { + case MouseEvent.MOUSE_PRESSED: { + Component target = mouseTarget(w.findComponentAt(x, y)); + hover(w, target, x, y); + pressTarget = target; + pressX = deviceX; + pressY = deviceY; + pressButton = button(); + dragged = false; + if (target != null) { + long now = System.currentTimeMillis(); + clickCount = now - lastClickTime < CLICK_INTERVAL + ? clickCount + 1 : 1; + send(w, target, id, x, y, clickCount, pressButton); + } + break; + } + case MouseEvent.MOUSE_DRAGGED: { + int slop = Math.max(2, (int) (3 * Units.scale())); + if (Math.abs(deviceX - pressX) > slop || Math.abs(deviceY - pressY) > slop) { + dragged = true; + } + if (pressTarget != null) { + send(w, pressTarget, id, x, y, 0, pressButton); + } + break; + } + case MouseEvent.MOUSE_RELEASED: { + Component target = pressTarget; + pressTarget = null; + if (target != null) { + send(w, target, id, x, y, clickCount, pressButton); + if (!dragged) { + lastClickTime = System.currentTimeMillis(); + send(w, target, MouseEvent.MOUSE_CLICKED, x, y, clickCount, pressButton); + } + } + break; + } + case MouseEvent.MOUSE_MOVED: { + Component target = mouseTarget(w.findComponentAt(x, y)); + hover(w, target, x, y); + if (target != null) { + send(w, target, id, x, y, 0, MouseEvent.NOBUTTON); + } + break; + } + default: + break; + } + } + + /// Delivers a wheel movement at a display position: `rotation` clicks, + /// positive toward the user. + public static void wheel(Window w, int deviceX, int deviceY, int rotation) { + com.codename1.ui.Component rp = w.cn1PeerOrNull(); + if (rp == null || rotation == 0) { + return; + } + int x = Units.toLogical(deviceX - rp.getAbsoluteX()); + int y = Units.toLogical(deviceY - rp.getAbsoluteY()); + Component target = w.findComponentAt(x, y); + while (target != null && target.getMouseWheelListeners().length == 0) { + target = target.getParent(); + } + if (target != null) { + int[] p = local(w, target, x, y); + target.dispatchEvent(new MouseWheelEvent(target, MouseEvent.MOUSE_WHEEL, System.currentTimeMillis(), + keyModifiers(), p[0], p[1], 0, false, MouseWheelEvent.WHEEL_UNIT_SCROLL, 3, rotation)); + } + } + + // ------------------------------------------------------------ keys + + private static int virtualKey(int code) { + if (code >= 'a' && code <= 'z') { + return code - ('a' - 'A'); + } + if ((code >= 'A' && code <= 'Z') || (code >= '0' && code <= '9')) { + return code; + } + switch (code) { + case ' ': + return KeyEvent.VK_SPACE; + case '\n': + case '\r': + return KeyEvent.VK_ENTER; + case 8: + return KeyEvent.VK_BACK_SPACE; + case '\t': + return KeyEvent.VK_TAB; + case 27: + return KeyEvent.VK_ESCAPE; + case 127: + return KeyEvent.VK_DELETE; + default: + break; + } + if (Display.isInitialized()) { + switch (Display.getInstance().getGameAction(code)) { + case Display.GAME_UP: + return KeyEvent.VK_UP; + case Display.GAME_DOWN: + return KeyEvent.VK_DOWN; + case Display.GAME_LEFT: + return KeyEvent.VK_LEFT; + case Display.GAME_RIGHT: + return KeyEvent.VK_RIGHT; + case Display.GAME_FIRE: + return KeyEvent.VK_ENTER; + default: + break; + } + } + return code > ' ' && code < 0xffff ? KeyEvent.getExtendedKeyCodeForChar(code) : KeyEvent.VK_UNDEFINED; + } + + private static char keyChar(int code) { + if (code == '\r') { + return '\n'; + } + if (code == '\n' || code == '\t' || code == 8 || code == 27 || code == 127 || (code >= ' ' && code < 0xffff)) { + return (char) code; + } + return KeyEvent.CHAR_UNDEFINED; + } + + /// Delivers a key that reached the form of `w`: key pressed followed + /// by key typed where the key has a character, or key released. + public static void key(Window w, boolean pressed, int code) { + Container c = w; + Component target = focusOwner != null && (focusOwner == w || c.isAncestorOf(focusOwner)) ? focusOwner : w; + long now = System.currentTimeMillis(); + char ch = keyChar(code); + target.dispatchEvent(new KeyEvent(target, pressed ? KeyEvent.KEY_PRESSED : KeyEvent.KEY_RELEASED, now, 0, + virtualKey(code), ch)); + if (pressed && ch != KeyEvent.CHAR_UNDEFINED) { + target.dispatchEvent(new KeyEvent(target, KeyEvent.KEY_TYPED, now, 0, KeyEvent.VK_UNDEFINED, ch)); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Fonts.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Fonts.java new file mode 100644 index 00000000000..889d7ed9cb7 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Fonts.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Font; +import com.codename1.desktopcompat.java.awt.FontMetrics; +import com.codename1.ui.Display; + +import java.util.HashMap; + +/// Maps AWT fonts onto the platform's fonts and measures them. +public final class Fonts { + + private static final HashMap CACHE = new HashMap(); + + private Fonts() { + } + + /// The Codename One font that draws `font` at `devicePixels` high. + public static com.codename1.ui.Font nativeFont(Font font, float devicePixels) { + int quarter = Math.max(4, Math.round(devicePixels * 4)); + Integer key = Integer.valueOf(quarter * 4 + (font.getStyle() & 3)); + com.codename1.ui.Font f = CACHE.get(key); + if (f != null) { + return f; + } + if (com.codename1.ui.Font.isNativeFontSchemeSupported()) { + String face; + if (font.isBold()) { + face = font.isItalic() ? "native:ItalicBold" : "native:MainBold"; + } else { + face = font.isItalic() ? "native:ItalicRegular" : "native:MainRegular"; + } + f = com.codename1.ui.Font.createTrueTypeFont(face, face) + .derive(quarter / 4f, com.codename1.ui.Font.STYLE_PLAIN); + } else { + int style = (font.isBold() ? com.codename1.ui.Font.STYLE_BOLD : 0) + | (font.isItalic() ? com.codename1.ui.Font.STYLE_ITALIC : 0); + f = com.codename1.ui.Font.createSystemFont(com.codename1.ui.Font.FACE_SYSTEM, style, + com.codename1.ui.Font.SIZE_MEDIUM); + } + CACHE.put(key, f); + return f; + } + + /// The AWT font nearest a Codename One one: its height in logical pixels. + public static Font fromNative(com.codename1.ui.Font f) { + float px = f.getPixelSize(); + if (px <= 0) { + px = f.getHeight() * 0.8f; + } + return new Font(Font.DIALOG, Font.PLAIN, Math.max(1, Math.round(px / Units.scale()))); + } + + /// The font of a component that was given none: the theme's label font, + /// or 12 pixel Dialog before a display exists. + public static Font defaultFont() { + if (!Display.isInitialized()) { + return new Font(Font.DIALOG, Font.PLAIN, 12); + } + return fromNative(com.codename1.ui.plaf.UIManager.getInstance().getComponentStyle("Label").getFont()); + } + + public static FontMetrics metrics(Font font) { + return new Metrics(font); + } + + /// Measures the platform font at the current scale and answers in + /// logical pixels, rounding widths to the nearest and heights up. + private static final class Metrics extends FontMetrics { + + private final com.codename1.ui.Font cn1; + private final float scale; + + Metrics(Font font) { + super(font); + scale = Units.scale(); + cn1 = Display.isInitialized() ? nativeFont(font, font.getSize2D() * scale) : null; + } + + @Override + public int getAscent() { + if (cn1 == null) { + return Math.round(font.getSize2D()); + } + int a = cn1.getAscent(); + return Math.round((a > 0 ? a : cn1.getHeight() * 0.8f) / scale); + } + + @Override + public int getDescent() { + return getHeight() - getAscent(); + } + + @Override + public int getHeight() { + if (cn1 == null) { + return Math.round(font.getSize2D() * 1.25f); + } + return (int) Math.ceil(cn1.getHeight() / scale - 0.001f); + } + + @Override + public int getMaxAdvance() { + return charWidth('W'); + } + + @Override + public int stringWidth(String str) { + if (cn1 == null) { + return super.stringWidth(str); + } + return Math.round(cn1.stringWidth(str) / scale); + } + + @Override + public int charWidth(char ch) { + if (cn1 == null) { + return super.charWidth(ch); + } + return Math.round(cn1.charWidth(ch) / scale); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/FrameForm.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/FrameForm.java new file mode 100644 index 00000000000..0928b012e40 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/FrameForm.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Window; +import com.codename1.desktopcompat.java.awt.event.MouseEvent; +import com.codename1.ui.Command; +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.events.WheelEvent; +import com.codename1.ui.layouts.BorderLayout; + +/// The form that shows a window: the window's peer fills it, and the +/// form's pointer and key input becomes the window's AWT events. +/// +/// A form shown over another gets a back command, which asks the window +/// to close the way the close box of a desktop window does. +public final class FrameForm extends Form { + + private final Window window; + private Form previous; + + public FrameForm(Window w) { + super(w.cn1Title(), new BorderLayout()); + window = w; + setScrollable(false); + com.codename1.ui.Component p = w.cn1Peer(); + add(BorderLayout.CENTER, p); + p.addMouseWheelListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (evt instanceof WheelEvent) { + WheelEvent we = (WheelEvent) evt; + int dy = we.getDeltaY(); + EventBridge.wheel(window, we.getX(), we.getY(), dy > 0 ? -1 : dy < 0 ? 1 : 0); + } + } + }); + } + + /// The window this form shows. + public Window window() { + return window; + } + + /// Shows the form, over the current one if there is one, and lays the + /// window out. + public void cn1Show() { + Form current = Display.getInstance().getCurrent(); + if (current != this) { + if (current != null) { + previous = current; + setBackCommand(new Command("Back") { + @Override + public void actionPerformed(ActionEvent evt) { + window.cn1Closing(); + } + }); + } + show(); + } + revalidate(); + } + + /// Goes back to the form this one was shown over, if it is showing. + public void cn1Hide() { + if (previous != null && Display.getInstance().getCurrent() == this) { + previous.showBack(); + } + } + + @Override + public void pointerPressed(int x, int y) { + EventBridge.pointer(window, MouseEvent.MOUSE_PRESSED, x, y); + super.pointerPressed(x, y); + } + + @Override + public void pointerDragged(int x, int y) { + EventBridge.pointer(window, MouseEvent.MOUSE_DRAGGED, x, y); + super.pointerDragged(x, y); + } + + @Override + public void pointerReleased(int x, int y) { + EventBridge.pointer(window, MouseEvent.MOUSE_RELEASED, x, y); + super.pointerReleased(x, y); + } + + @Override + public void pointerHover(int[] x, int[] y) { + if (x != null && y != null && x.length > 0 && y.length > 0) { + EventBridge.pointer(window, MouseEvent.MOUSE_MOVED, x[0], y[0]); + } + super.pointerHover(x, y); + } + + @Override + public void keyPressed(int keyCode) { + EventBridge.key(window, true, keyCode); + super.keyPressed(keyCode); + } + + @Override + public void keyReleased(int keyCode) { + EventBridge.key(window, false, keyCode); + super.keyReleased(keyCode); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/G2D.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/G2D.java new file mode 100644 index 00000000000..339c64de1fe --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/G2D.java @@ -0,0 +1,1390 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.AlphaComposite; +import com.codename1.desktopcompat.java.awt.BasicStroke; +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Composite; +import com.codename1.desktopcompat.java.awt.Font; +import com.codename1.desktopcompat.java.awt.FontMetrics; +import com.codename1.desktopcompat.java.awt.GradientPaint; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Graphics2D; +import com.codename1.desktopcompat.java.awt.Image; +import com.codename1.desktopcompat.java.awt.Paint; +import com.codename1.desktopcompat.java.awt.Rectangle; +import com.codename1.desktopcompat.java.awt.RenderingHints; +import com.codename1.desktopcompat.java.awt.Shape; +import com.codename1.desktopcompat.java.awt.Stroke; +import com.codename1.desktopcompat.java.awt.geom.AffineTransform; +import com.codename1.desktopcompat.java.awt.geom.Arc2D; +import com.codename1.desktopcompat.java.awt.geom.Ellipse2D; +import com.codename1.desktopcompat.java.awt.geom.Line2D; +import com.codename1.desktopcompat.java.awt.geom.Path2D; +import com.codename1.desktopcompat.java.awt.geom.PathIterator; +import com.codename1.desktopcompat.java.awt.geom.Rectangle2D; +import com.codename1.desktopcompat.java.awt.geom.RoundRectangle2D; +import com.codename1.desktopcompat.java.awt.image.BufferedImage; +import com.codename1.desktopcompat.java.awt.image.ImageObserver; +import com.codename1.ui.LinearGradientPaint; +import com.codename1.ui.MultipleGradientPaint; +import com.codename1.ui.Transform; +import com.codename1.ui.geom.GeneralPath; + +import java.util.Map; + +/// The layer's `Graphics2D`, drawing through a Codename One `Graphics`. +/// +/// ## Coordinates +/// +/// User space is logical pixels with the origin at the component's (or +/// image's) corner. The graphics keeps the whole mapping to the Codename One +/// context as one affine matrix -- the user transform, then the unit scale +/// ([Units]), then the component's position -- and applies it to geometry +/// *itself*: rectangles and straight lines are snapped to device pixels and +/// drawn with the port's primitives, every other shape is rebuilt as a path +/// in device coordinates. Nothing basic depends on the port's transform +/// support, so a translated or scaled drawing looks the same everywhere and +/// stays crisp at any density. Only what cannot be redrawn point by point +/// -- text and images under a rotation or shear -- uses the context's +/// transform, and falls back to drawing unrotated at the right place on a +/// port without one. +/// +/// ## State +/// +/// A Codename One context has one color, one clip and one font; AWT hands +/// out independent copies with `create()`. Each instance therefore keeps its +/// own state and writes it to the context before it draws, when another +/// instance drew last or something changed. [#finish()] puts the context +/// back the way [#forPeer] found it, which the paint cycle relies on. +/// +/// ## What is refused +/// +/// Composites other than source-over, paints other than a color or a +/// two-color gradient, XOR mode and `copyArea` throw +/// `UnsupportedOperationException`: drawing something else in their place +/// would be a bug nobody could see from the code. +public final class G2D extends Graphics2D { + + private static final BasicStroke DEFAULT_STROKE = new BasicStroke(); + + /// What every graphics created from one root shares: the context, which + /// of them wrote its state there last, and what to restore at the end. + private static final class Target { + final com.codename1.ui.Graphics g; + final BufferedImage image; + G2D active; + int alpha; + int clipX; + int clipY; + int clipW; + int clipH; + int color; + com.codename1.ui.Font font; + + Target(com.codename1.ui.Graphics g, BufferedImage image) { + this.g = g; + this.image = image; + } + } + + private final Target target; + private final com.codename1.ui.Graphics g; + private final float originX; + private final float originY; + private final float unit; + private final int baseX; + private final int baseY; + private final int baseW; + private final int baseH; + + private AffineTransform tx = new AffineTransform(); + // Device coordinates: X = a*x + c*y + e, Y = b*x + d*y + f. + private double a; + private double b; + private double c; + private double d; + private double e; + private double f; + /// Whether the matrix only scales and translates, without mirroring. + private boolean upright; + + private Paint paint = Color.black; + private Color color = Color.black; + private Color background = Color.white; + private Font font; + private Stroke stroke = DEFAULT_STROKE; + private AlphaComposite composite = AlphaComposite.SrcOver; + private final RenderingHints hints = new RenderingHints(null); + private int antialias = -1; + private int textAntialias = -1; + + private int clipX; + private int clipY; + private int clipW; + private int clipH; + private GeneralPath clipShape; + private boolean dirty = true; + + private G2D(Target target, float originX, float originY, float unit, int bx, int by, int bw, int bh) { + this.target = target; + this.g = target.g; + this.originX = originX; + this.originY = originY; + this.unit = unit; + this.baseX = bx; + this.baseY = by; + this.baseW = bw; + this.baseH = bh; + this.clipX = bx; + this.clipY = by; + this.clipW = bw; + this.clipH = bh; + matrixChanged(); + } + + private G2D(G2D o) { + this.target = o.target; + this.g = o.g; + this.originX = o.originX; + this.originY = o.originY; + this.unit = o.unit; + this.baseX = o.baseX; + this.baseY = o.baseY; + this.baseW = o.baseW; + this.baseH = o.baseH; + this.tx = new AffineTransform(o.tx); + this.paint = o.paint; + this.color = o.color; + this.background = o.background; + this.font = o.font; + this.stroke = o.stroke; + this.composite = o.composite; + this.hints.add(o.hints); + this.antialias = o.antialias; + this.textAntialias = o.textAntialias; + this.clipX = o.clipX; + this.clipY = o.clipY; + this.clipW = o.clipW; + this.clipH = o.clipH; + this.clipShape = o.clipShape; + matrixChanged(); + } + + /// A graphics for a component whose peer Codename One is painting: + /// `g` as the peer's `paint` received it and the peer's bounds in that + /// context. Pair with [#finish()]. + public static G2D forPeer(com.codename1.ui.Graphics g, int x, int y, int width, int height) { + Target t = new Target(g, null); + t.alpha = g.getAlpha(); + t.clipX = g.getClipX(); + t.clipY = g.getClipY(); + t.clipW = g.getClipWidth(); + t.clipH = g.getClipHeight(); + t.color = g.getColor(); + t.font = g.getFont(); + int x1 = Math.max(x, t.clipX); + int y1 = Math.max(y, t.clipY); + int x2 = Math.min(x + width, t.clipX + t.clipW); + int y2 = Math.min(y + height, t.clipY + t.clipH); + return new G2D(t, x, y, Units.scale(), x1, y1, Math.max(0, x2 - x1), Math.max(0, y2 - y1)); + } + + /// A graphics that draws into a buffered image through `g`, the graphics + /// of the image's Codename One surface. Image pixels are user units. + public static G2D forImage(BufferedImage image, com.codename1.ui.Graphics g, int width, int height) { + Target t = new Target(g, image); + t.alpha = 255; + t.clipW = width; + t.clipH = height; + t.color = g.getColor(); + t.font = g.getFont(); + return new G2D(t, 0, 0, 1f, 0, 0, width, height); + } + + /// Restores the context to the clip, color, alpha and font it had when + /// this graphics was made for a peer. + public void finish() { + g.setClip(target.clipX, target.clipY, target.clipW, target.clipH); + g.setColor(target.color); + g.setAlpha(target.alpha); + if (target.font != null) { + g.setFont(target.font); + } + target.active = null; + } + + /// The Codename One context this graphics draws through. + public com.codename1.ui.Graphics nativeGraphics() { + return g; + } + + // ------------------------------------------------------------ mapping + + private void matrixChanged() { + a = unit * tx.getScaleX(); + c = unit * tx.getShearX(); + e = unit * tx.getTranslateX() + originX; + b = unit * tx.getShearY(); + d = unit * tx.getScaleY(); + f = unit * tx.getTranslateY() + originY; + upright = b == 0 && c == 0 && a > 0 && d > 0; + } + + /// The matrix from user space to the context's coordinates as + /// `{m00, m10, m01, m11, m02, m12}`, the order of `AffineTransform`'s + /// constructor. + public double[] deviceMatrix() { + return new double[]{a, b, c, d, e, f}; + } + + private double mapX(double x, double y) { + return a * x + c * y + e; + } + + private double mapY(double x, double y) { + return b * x + d * y + f; + } + + /// How many device pixels a user unit of length covers. + private double lineScale() { + return Math.sqrt(Math.abs(a * d - b * c)); + } + + /// The device rectangle `{x, y, w, h}` a user rectangle snaps to: its + /// edges rounded one by one when upright, else the bounds of its four + /// corners. + public int[] deviceRect(double x, double y, double w, double h) { + if (upright) { + int x1 = (int) Math.round(a * x + e); + int y1 = (int) Math.round(d * y + f); + int x2 = (int) Math.round(a * (x + w) + e); + int y2 = (int) Math.round(d * (y + h) + f); + return new int[]{x1, y1, x2 - x1, y2 - y1}; + } + double[] xs = {mapX(x, y), mapX(x + w, y), mapX(x, y + h), mapX(x + w, y + h)}; + double[] ys = {mapY(x, y), mapY(x + w, y), mapY(x, y + h), mapY(x + w, y + h)}; + double minX = xs[0]; + double maxX = xs[0]; + double minY = ys[0]; + double maxY = ys[0]; + for (int i = 1; i < 4; i++) { + minX = Math.min(minX, xs[i]); + maxX = Math.max(maxX, xs[i]); + minY = Math.min(minY, ys[i]); + maxY = Math.max(maxY, ys[i]); + } + int x1 = (int) Math.floor(minX); + int y1 = (int) Math.floor(minY); + return new int[]{x1, y1, (int) Math.ceil(maxX) - x1, (int) Math.ceil(maxY) - y1}; + } + + /// The shape in device coordinates, every point moved by `offset` user + /// units first (half a pixel for the integer outline calls, whose + /// coordinates name pixels rather than the lines between them). + private GeneralPath devicePath(Shape s, double offset) { + GeneralPath p = new GeneralPath(); + PathIterator it = s.getPathIterator(null); + p.setWindingRule(it.getWindingRule() == PathIterator.WIND_EVEN_ODD + ? com.codename1.ui.geom.PathIterator.WIND_EVEN_ODD + : com.codename1.ui.geom.PathIterator.WIND_NON_ZERO); + double[] k = new double[6]; + while (!it.isDone()) { + int type = it.currentSegment(k); + for (int i = 0; i < 6; i += 2) { + double x = k[i] + offset; + double y = k[i + 1] + offset; + k[i] = mapX(x, y); + k[i + 1] = mapY(x, y); + } + switch (type) { + case PathIterator.SEG_MOVETO: + p.moveTo(k[0], k[1]); + break; + case PathIterator.SEG_LINETO: + p.lineTo(k[0], k[1]); + break; + case PathIterator.SEG_QUADTO: + p.quadTo(k[0], k[1], k[2], k[3]); + break; + case PathIterator.SEG_CUBICTO: + p.curveTo(k[0], k[1], k[2], k[3], k[4], k[5]); + break; + default: + p.closePath(); + break; + } + it.next(); + } + return p; + } + + // ------------------------------------------------------------ state + + private int alphaOf(Color col) { + return Math.round(target.alpha * (col.getAlpha() / 255f) * composite.getAlpha()); + } + + private void applyColor(Color col) { + g.setColor(col.getRGB() & 0xffffff); + g.setAlpha(alphaOf(col)); + } + + /// Makes the context carry this graphics' state. Called before every + /// draw; cheap when this graphics drew last and nothing changed. + private void sync() { + if (target.image != null) { + target.image.cn1Drawing(); + } + if (target.active == this && !dirty) { + return; + } + target.active = this; + dirty = false; + if (clipShape != null) { + g.setClip(clipShape); + g.clipRect(clipX, clipY, clipW, clipH); + } else { + g.setClip(clipX, clipY, clipW, clipH); + } + applyColor(color); + if (antialias >= 0) { + g.setAntiAliased(antialias == 1); + } + if (textAntialias >= 0) { + g.setAntiAliasedText(textAntialias == 1); + } + } + + @Override + public Graphics create() { + return new G2D(this); + } + + @Override + public void dispose() { + if (target.active == this) { + target.active = null; + } + } + + @Override + public Color getColor() { + return color; + } + + @Override + public void setColor(Color col) { + if (col == null) { + return; + } + color = col; + paint = col; + dirty = true; + } + + @Override + public Paint getPaint() { + return paint; + } + + /// Accepts a [Color] or a [GradientPaint]; null is ignored as in the JDK. + @Override + public void setPaint(Paint p) { + if (p == null) { + return; + } + if (p instanceof Color) { + setColor((Color) p); + } else if (p instanceof GradientPaint) { + paint = p; + } else { + throw new UnsupportedOperationException("Unsupported paint " + p.getClass().getName() + + ": only Color and GradientPaint can be drawn"); + } + } + + @Override + public void setPaintMode() { + } + + @Override + public void setXORMode(Color c1) { + throw new UnsupportedOperationException("XOR painting is not supported"); + } + + @Override + public Composite getComposite() { + return composite; + } + + /// Accepts `AlphaComposite` in the `SRC_OVER` rule with any alpha, and + /// `SRC` at full alpha, which differs from it only for translucent + /// colors. + @Override + public void setComposite(Composite comp) { + if (comp == null) { + throw new IllegalArgumentException("null Composite"); + } + if (comp instanceof AlphaComposite) { + AlphaComposite ac = (AlphaComposite) comp; + if (ac.getRule() == AlphaComposite.SRC_OVER + || ac.getRule() == AlphaComposite.SRC && ac.getAlpha() >= 1f) { + composite = ac; + dirty = true; + return; + } + } + throw new UnsupportedOperationException( + "Unsupported composite: only AlphaComposite.SRC_OVER can be drawn with"); + } + + @Override + public Stroke getStroke() { + return stroke; + } + + @Override + public void setStroke(Stroke s) { + if (s == null) { + throw new IllegalArgumentException("null Stroke"); + } + stroke = s; + } + + @Override + public void setBackground(Color col) { + background = col; + } + + @Override + public Color getBackground() { + return background; + } + + @Override + public Font getFont() { + if (font == null) { + font = Fonts.defaultFont(); + } + return font; + } + + @Override + public void setFont(Font fnt) { + if (fnt != null) { + font = fnt; + } + } + + @Override + public FontMetrics getFontMetrics(Font fnt) { + return Fonts.metrics(fnt); + } + + @Override + public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { + hints.put(hintKey, hintValue); + if (hintKey == RenderingHints.KEY_ANTIALIASING) { + antialias = hintValue == RenderingHints.VALUE_ANTIALIAS_ON ? 1 + : hintValue == RenderingHints.VALUE_ANTIALIAS_OFF ? 0 : -1; + dirty = true; + } else if (hintKey == RenderingHints.KEY_TEXT_ANTIALIASING) { + textAntialias = hintValue == RenderingHints.VALUE_TEXT_ANTIALIAS_OFF ? 0 + : hintValue == RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT ? -1 : 1; + dirty = true; + } + } + + @Override + public Object getRenderingHint(RenderingHints.Key hintKey) { + return hints.get(hintKey); + } + + @Override + public void setRenderingHints(Map newHints) { + hints.clear(); + antialias = -1; + textAntialias = -1; + addRenderingHints(newHints); + } + + @Override + public void addRenderingHints(Map newHints) { + for (Map.Entry en : newHints.entrySet()) { + Object key = en.getKey(); + if (key instanceof RenderingHints.Key) { + setRenderingHint((RenderingHints.Key) key, en.getValue()); + } + } + } + + @Override + public RenderingHints getRenderingHints() { + RenderingHints copy = new RenderingHints(null); + copy.add(hints); + return copy; + } + + // ------------------------------------------------------------ transform + + @Override + public void translate(int x, int y) { + tx.translate(x, y); + matrixChanged(); + } + + @Override + public void translate(double x, double y) { + tx.translate(x, y); + matrixChanged(); + } + + @Override + public void rotate(double theta) { + tx.rotate(theta); + matrixChanged(); + } + + @Override + public void rotate(double theta, double x, double y) { + tx.rotate(theta, x, y); + matrixChanged(); + } + + @Override + public void scale(double sx, double sy) { + tx.scale(sx, sy); + matrixChanged(); + } + + @Override + public void shear(double shx, double shy) { + tx.shear(shx, shy); + matrixChanged(); + } + + @Override + public void transform(AffineTransform t) { + tx.concatenate(t); + matrixChanged(); + } + + /// Replaces the user transform. Identity is the component's own corner, + /// so the transform [#getTransform()] answered earlier restores it. + @Override + public void setTransform(AffineTransform t) { + tx = t == null ? new AffineTransform() : new AffineTransform(t); + matrixChanged(); + } + + @Override + public AffineTransform getTransform() { + return new AffineTransform(tx); + } + + // ------------------------------------------------------------ clip + + private void intersectClip(int[] r, int x, int y, int w, int h) { + int x1 = Math.max(r[0], x); + int y1 = Math.max(r[1], y); + int x2 = Math.min(r[0] + r[2], x + w); + int y2 = Math.min(r[1] + r[3], y + h); + clipX = x1; + clipY = y1; + clipW = Math.max(0, x2 - x1); + clipH = Math.max(0, y2 - y1); + dirty = true; + } + + private GeneralPath shapeClip(Shape s) { + return g.isShapeClipSupported() ? devicePath(s, 0) : null; + } + + private void clipTo(double x, double y, double w, double h, boolean replace) { + int[] r = deviceRect(x, y, Math.max(0, w), Math.max(0, h)); + GeneralPath shape = upright ? null : shapeClip(new Rectangle2D.Double(x, y, w, h)); + if (replace) { + intersectClip(r, baseX, baseY, baseW, baseH); + clipShape = shape; + } else { + intersectClip(r, clipX, clipY, clipW, clipH); + if (shape != null) { + clipShape = shape; + } + } + } + + @Override + public void clipRect(int x, int y, int width, int height) { + clipTo(x, y, width, height, false); + } + + @Override + public void setClip(int x, int y, int width, int height) { + clipTo(x, y, width, height, true); + } + + /// Replaces the clip, never beyond the area the graphics was created + /// for; null goes back to that area. A shape that is not a rectangle + /// clips exactly where the port clips to shapes and to its bounds + /// elsewhere. + @Override + public void setClip(Shape clip) { + clipToShape(clip, true); + } + + /// Narrows the clip to its intersection with the shape. When both the + /// current clip and the new one are shapes rather than rectangles, the + /// result is the new shape within the bounds of the old. + @Override + public void clip(Shape s) { + if (s == null) { + setClip(null); + return; + } + clipToShape(s, false); + } + + private void clipToShape(Shape s, boolean replace) { + if (s == null) { + clipX = baseX; + clipY = baseY; + clipW = baseW; + clipH = baseH; + clipShape = null; + dirty = true; + return; + } + if (s instanceof Rectangle2D) { + Rectangle2D r = (Rectangle2D) s; + clipTo(r.getX(), r.getY(), r.getWidth(), r.getHeight(), replace); + return; + } + Rectangle2D bounds = s.getBounds2D(); + int[] r = deviceRect(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight()); + if (replace) { + intersectClip(r, baseX, baseY, baseW, baseH); + } else { + intersectClip(r, clipX, clipY, clipW, clipH); + } + GeneralPath shape = shapeClip(s); + if (shape != null || replace) { + clipShape = shape; + } + } + + /// The clip as a rectangle in user space: exact for a rectangular clip + /// under an upright transform, the bounding box otherwise. + @Override + public Shape getClip() { + return getClipBounds(); + } + + @Override + public Rectangle getClipBounds() { + if (upright) { + // A coordinate that was snapped to a device pixel comes back as + // the logical pixel it was, not its neighbor. + double slackX = 0.5 / a + 1e-6; + double slackY = 0.5 / d + 1e-6; + int x1 = (int) Math.floor((clipX - e) / a + slackX); + int y1 = (int) Math.floor((clipY - f) / d + slackY); + int x2 = (int) Math.ceil((clipX + clipW - e) / a - slackX); + int y2 = (int) Math.ceil((clipY + clipH - f) / d - slackY); + return new Rectangle(x1, y1, Math.max(0, x2 - x1), Math.max(0, y2 - y1)); + } + double det = a * d - b * c; + if (det == 0) { + return new Rectangle(); + } + double minX = 0; + double minY = 0; + double maxX = 0; + double maxY = 0; + for (int i = 0; i < 4; i++) { + double px = clipX + ((i & 1) == 0 ? 0 : clipW) - e; + double py = clipY + ((i & 2) == 0 ? 0 : clipH) - f; + double ux = (d * px - c * py) / det; + double uy = (a * py - b * px) / det; + if (i == 0) { + minX = ux; + maxX = ux; + minY = uy; + maxY = uy; + } else { + minX = Math.min(minX, ux); + maxX = Math.max(maxX, ux); + minY = Math.min(minY, uy); + maxY = Math.max(maxY, uy); + } + } + int x1 = (int) Math.floor(minX); + int y1 = (int) Math.floor(minY); + return new Rectangle(x1, y1, (int) Math.ceil(maxX) - x1, (int) Math.ceil(maxY) - y1); + } + + @Override + public void copyArea(int x, int y, int width, int height, int dx, int dy) { + throw new UnsupportedOperationException("copyArea is not supported"); + } + + // ------------------------------------------------------------ fills + + private boolean solid() { + return paint instanceof Color; + } + + private void fillSnapped(double x, double y, double w, double h) { + int[] r = deviceRect(x, y, w, h); + g.fillRect(r[0], r[1], Math.max(1, r[2]), Math.max(1, r[3])); + } + + /// Fills a rectangle with the gradient through the port's two-color + /// fill when the gradient runs straight across exactly that rectangle; + /// false when it does not and the general path must draw it. + private boolean fillAxisGradient(GradientPaint gp, double x, double y, double w, double h) { + if (!upright || gp.isCyclic()) { + return false; + } + double x1 = gp.getPoint1().getX(); + double y1 = gp.getPoint1().getY(); + double x2 = gp.getPoint2().getX(); + double y2 = gp.getPoint2().getY(); + boolean horizontal = y1 == y2 && x1 != x2; + boolean vertical = x1 == x2 && y1 != y2; + if (!horizontal && !vertical) { + return false; + } + double from = horizontal ? Math.min(x1, x2) : Math.min(y1, y2); + double to = horizontal ? Math.max(x1, x2) : Math.max(y1, y2); + double start = horizontal ? x : y; + double end = horizontal ? x + w : y + h; + if (Math.abs(from - start) > 1 || Math.abs(to - end) > 1) { + return false; + } + boolean forward = horizontal ? x1 < x2 : y1 < y2; + Color first = forward ? gp.getColor1() : gp.getColor2(); + Color second = forward ? gp.getColor2() : gp.getColor1(); + int[] r = deviceRect(x, y, w, h); + g.setAlpha(Math.round(target.alpha * composite.getAlpha() + * Math.min(first.getAlpha(), second.getAlpha()) / 255f)); + g.fillLinearGradient(first.getRGB() & 0xffffff, second.getRGB() & 0xffffff, r[0], r[1], r[2], r[3], + horizontal); + dirty = true; + return true; + } + + @Override + public void fillRect(int x, int y, int width, int height) { + if (width <= 0 || height <= 0) { + return; + } + if (upright && solid()) { + sync(); + fillSnapped(x, y, width, height); + } else { + fill(new Rectangle2D.Double(x, y, width, height)); + } + } + + /// Fills with the background color. On an image the area is erased + /// first, so a translucent background replaces what was there. + @Override + public void clearRect(int x, int y, int width, int height) { + if (width <= 0 || height <= 0) { + return; + } + sync(); + if (upright && target.image != null && background.getAlpha() < 255) { + int[] r = deviceRect(x, y, width, height); + g.clearRect(r[0], r[1], r[2], r[3]); + } + applyColor(background); + dirty = true; + if (upright) { + fillSnapped(x, y, width, height); + } else if (g.isShapeSupported()) { + g.fillShape(devicePath(new Rectangle2D.Double(x, y, width, height), 0)); + } + } + + @Override + public void fill(Shape s) { + sync(); + if (s instanceof Rectangle2D) { + Rectangle2D r = (Rectangle2D) s; + if (upright && solid()) { + if (r.getWidth() > 0 && r.getHeight() > 0) { + fillSnapped(r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } + return; + } + if (paint instanceof GradientPaint + && fillAxisGradient((GradientPaint) paint, r.getX(), r.getY(), r.getWidth(), r.getHeight())) { + return; + } + } + if (!g.isShapeSupported()) { + fillFlattened(s); + return; + } + GeneralPath p = devicePath(s, 0); + if (paint instanceof GradientPaint) { + GradientPaint gp = (GradientPaint) paint; + double x1 = gp.getPoint1().getX(); + double y1 = gp.getPoint1().getY(); + double x2 = gp.getPoint2().getX(); + double y2 = gp.getPoint2().getY(); + g.setColor(new LinearGradientPaint(mapX(x1, y1), mapY(x1, y1), mapX(x2, y2), mapY(x2, y2), + new float[]{0f, 1f}, + new int[]{gp.getColor1().getRGB() & 0xffffff, gp.getColor2().getRGB() & 0xffffff}, + gp.isCyclic() ? MultipleGradientPaint.CycleMethod.REFLECT + : MultipleGradientPaint.CycleMethod.NO_CYCLE, + MultipleGradientPaint.ColorSpaceType.SRGB, null)); + g.setAlpha(Math.round(target.alpha * composite.getAlpha() + * Math.min(gp.getColor1().getAlpha(), gp.getColor2().getAlpha()) / 255f)); + g.fillShape(p); + dirty = true; + } else { + g.fillShape(p); + } + } + + /// Fills each subpath as a polygon, for a port without shapes. Holes + /// and curves finer than a device pixel are lost. + private void fillFlattened(Shape s) { + PathIterator it = s.getPathIterator(null, 0.5 / Math.max(0.01, lineScale())); + int[] xs = new int[16]; + int[] ys = new int[16]; + int n = 0; + double[] k = new double[6]; + while (true) { + boolean done = it.isDone(); + int type = done ? PathIterator.SEG_MOVETO : it.currentSegment(k); + if (type == PathIterator.SEG_MOVETO || type == PathIterator.SEG_CLOSE) { + if (n > 2) { + g.fillPolygon(xs, ys, n); + } + if (type == PathIterator.SEG_CLOSE || done) { + n = 0; + } + } + if (done) { + break; + } + if (type == PathIterator.SEG_MOVETO) { + n = 0; + } + if (type != PathIterator.SEG_CLOSE) { + if (n == xs.length) { + int[] nx = new int[n * 2]; + int[] ny = new int[n * 2]; + System.arraycopy(xs, 0, nx, 0, n); + System.arraycopy(ys, 0, ny, 0, n); + xs = nx; + ys = ny; + } + xs[n] = (int) Math.round(mapX(k[0], k[1])); + ys[n] = (int) Math.round(mapY(k[0], k[1])); + n++; + } + it.next(); + } + } + + // ------------------------------------------------------------ strokes + + private BasicStroke plainStroke() { + if (stroke instanceof BasicStroke) { + BasicStroke bs = (BasicStroke) stroke; + if (bs.getDashArray() == null) { + return bs; + } + } + return null; + } + + /// Whether outlines are a single device pixel wide, which the port's + /// own outline primitives draw exactly. + private boolean hairline() { + BasicStroke bs = plainStroke(); + return bs != null && solid() && upright && bs.getLineWidth() * lineScale() < 1.5; + } + + @Override + public void draw(Shape s) { + stroke(s, 0); + } + + private void stroke(Shape s, double offset) { + if (!(stroke instanceof BasicStroke)) { + fill(stroke.createStrokedShape(s)); + return; + } + sync(); + BasicStroke bs = (BasicStroke) stroke; + if (paint instanceof GradientPaint) { + applyColor(((GradientPaint) paint).getColor1()); + dirty = true; + } + Shape src = bs.getDashArray() == null ? s : dashed(s, bs); + if (!g.isShapeSupported()) { + drawFlattened(src, offset); + return; + } + int cap = bs.getEndCap() == BasicStroke.CAP_ROUND ? com.codename1.ui.Stroke.CAP_ROUND + : bs.getEndCap() == BasicStroke.CAP_SQUARE ? com.codename1.ui.Stroke.CAP_SQUARE + : com.codename1.ui.Stroke.CAP_BUTT; + int join = bs.getLineJoin() == BasicStroke.JOIN_ROUND ? com.codename1.ui.Stroke.JOIN_ROUND + : bs.getLineJoin() == BasicStroke.JOIN_BEVEL ? com.codename1.ui.Stroke.JOIN_BEVEL + : com.codename1.ui.Stroke.JOIN_MITER; + float width = (float) Math.max(1, bs.getLineWidth() * lineScale()); + g.drawShape(devicePath(src, offset), new com.codename1.ui.Stroke(width, cap, join, bs.getMiterLimit())); + } + + /// Draws the flattened outline segment by segment, one device pixel + /// wide, for a port without shapes. + private void drawFlattened(Shape s, double offset) { + PathIterator it = s.getPathIterator(null, 0.5 / Math.max(0.01, lineScale())); + double[] k = new double[6]; + int startX = 0; + int startY = 0; + int lastX = 0; + int lastY = 0; + while (!it.isDone()) { + int type = it.currentSegment(k); + if (type == PathIterator.SEG_CLOSE) { + g.drawLine(lastX, lastY, startX, startY); + lastX = startX; + lastY = startY; + } else { + int x = (int) Math.round(mapX(k[0] + offset, k[1] + offset) - 0.5); + int y = (int) Math.round(mapY(k[0] + offset, k[1] + offset) - 0.5); + if (type == PathIterator.SEG_MOVETO) { + startX = x; + startY = y; + } else { + g.drawLine(lastX, lastY, x, y); + } + lastX = x; + lastY = y; + } + it.next(); + } + } + + /// The dashes of `s` under `bs` as a path of separate segments: the + /// outline is flattened and the pattern walked along it, starting + /// `dashPhase` into the pattern at every subpath. + static Path2D.Double dashed(Shape s, BasicStroke bs) { + float[] dash = bs.getDashArray(); + Path2D.Double out = new Path2D.Double(); + PathIterator it = s.getPathIterator(null, 0.25); + double[] k = new double[6]; + double startX = 0; + double startY = 0; + double lastX = 0; + double lastY = 0; + int index = 0; + double left = 0; + boolean on = true; + boolean pen = false; + while (!it.isDone()) { + int type = it.currentSegment(k); + double x = type == PathIterator.SEG_CLOSE ? startX : k[0]; + double y = type == PathIterator.SEG_CLOSE ? startY : k[1]; + if (type == PathIterator.SEG_MOVETO) { + startX = x; + startY = y; + index = 0; + on = true; + left = dash[0]; + double phase = bs.getDashPhase(); + while (phase > 0) { + if (phase >= left) { + phase -= left; + index = (index + 1) % dash.length; + left = dash[index]; + on = !on; + } else { + left -= phase; + phase = 0; + } + } + pen = false; + } else { + double dx = x - lastX; + double dy = y - lastY; + double length = Math.sqrt(dx * dx + dy * dy); + double done = 0; + while (length - done > 1e-9) { + double step = Math.min(left, length - done); + double t0 = done / length; + double t1 = (done + step) / length; + if (on && step > 0) { + if (!pen) { + out.moveTo(lastX + dx * t0, lastY + dy * t0); + pen = true; + } + out.lineTo(lastX + dx * t1, lastY + dy * t1); + } + done += step; + left -= step; + if (left <= 1e-9) { + index = (index + 1) % dash.length; + left = dash[index]; + on = !on; + pen = false; + } + } + } + lastX = x; + lastY = y; + it.next(); + } + return out; + } + + /// A line names the pixels it passes through, each one logical pixel + /// square. Horizontal and vertical lines are filled as the snapped + /// rectangle those pixels cover, so they are crisp and exactly as wide as + /// the stroke at any density; the rest are stroked through pixel centers. + @Override + public void drawLine(int x1, int y1, int x2, int y2) { + BasicStroke bs = plainStroke(); + if (bs != null && upright && solid() && (x1 == x2 || y1 == y2) && bs.getEndCap() != BasicStroke.CAP_ROUND) { + sync(); + double w = Math.max(bs.getLineWidth(), 1 / lineScale()); + double ext = bs.getEndCap() == BasicStroke.CAP_SQUARE ? w / 2 : 0; + double half = w / 2; + if (y1 == y2) { + fillSnapped(Math.min(x1, x2) + 0.5 - ext, y1 + 0.5 - half, Math.abs(x2 - x1) + 2 * ext, w); + } else { + fillSnapped(x1 + 0.5 - half, Math.min(y1, y2) + 0.5 - ext, w, Math.abs(y2 - y1) + 2 * ext); + } + return; + } + stroke(new Line2D.Double(x1, y1, x2, y2), 0.5); + } + + @Override + public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + if (width < 0 || height < 0) { + return; + } + if (hairline() || !g.isShapeSupported()) { + sync(); + int[] r = deviceRect(x, y, width, height); + g.drawRoundRect(r[0], r[1], r[2], r[3], (int) Math.round(arcWidth * a), (int) Math.round(arcHeight * d)); + return; + } + stroke(new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight), 0.5); + } + + @Override + public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + if (width <= 0 || height <= 0) { + return; + } + if (upright && solid()) { + sync(); + int[] r = deviceRect(x, y, width, height); + g.fillRoundRect(r[0], r[1], r[2], r[3], (int) Math.round(arcWidth * a), (int) Math.round(arcHeight * d)); + return; + } + fill(new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight)); + } + + @Override + public void drawOval(int x, int y, int width, int height) { + if (width < 0 || height < 0) { + return; + } + if (hairline() || !g.isShapeSupported()) { + sync(); + int[] r = deviceRect(x, y, width, height); + g.drawArc(r[0], r[1], r[2], r[3], 0, 360); + return; + } + stroke(new Ellipse2D.Double(x, y, width, height), 0.5); + } + + @Override + public void fillOval(int x, int y, int width, int height) { + if (width <= 0 || height <= 0) { + return; + } + if (upright && solid()) { + sync(); + int[] r = deviceRect(x, y, width, height); + g.fillArc(r[0], r[1], r[2], r[3], 0, 360); + return; + } + fill(new Ellipse2D.Double(x, y, width, height)); + } + + @Override + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + if (width < 0 || height < 0) { + return; + } + if (hairline() || !g.isShapeSupported()) { + sync(); + int[] r = deviceRect(x, y, width, height); + g.drawArc(r[0], r[1], r[2], r[3], startAngle, arcAngle); + return; + } + stroke(new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN), 0.5); + } + + @Override + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + if (width <= 0 || height <= 0) { + return; + } + if (upright && solid()) { + sync(); + int[] r = deviceRect(x, y, width, height); + g.fillArc(r[0], r[1], r[2], r[3], startAngle, arcAngle); + return; + } + fill(new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.PIE)); + } + + private static Path2D.Double polygon(int[] xPoints, int[] yPoints, int nPoints, boolean close) { + Path2D.Double p = new Path2D.Double(Path2D.WIND_EVEN_ODD); + for (int i = 0; i < nPoints; i++) { + if (i == 0) { + p.moveTo(xPoints[i], yPoints[i]); + } else { + p.lineTo(xPoints[i], yPoints[i]); + } + } + if (close && nPoints > 0) { + p.closePath(); + } + return p; + } + + @Override + public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { + if (nPoints > 1) { + stroke(polygon(xPoints, yPoints, nPoints, false), 0.5); + } + } + + @Override + public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { + if (nPoints > 1) { + stroke(polygon(xPoints, yPoints, nPoints, true), 0.5); + } + } + + @Override + public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { + if (nPoints > 2) { + fill(polygon(xPoints, yPoints, nPoints, true)); + } + } + + // ------------------------------------------------------------ text + + @Override + public void drawString(String str, int x, int y) { + drawString(str, (float) x, (float) y); + } + + /// Draws with the left end of the baseline at the point. The font is + /// sized for the current scale; under a rotation the context's transform + /// turns it, and a port without transforms draws it unrotated there. + @Override + public void drawString(String str, float x, float y) { + if (str == null) { + throw new NullPointerException("String is null"); + } + if (str.length() == 0) { + return; + } + sync(); + if (paint instanceof GradientPaint) { + applyColor(((GradientPaint) paint).getColor1()); + dirty = true; + } + Font fnt = getFont(); + double ls = lineScale(); + g.setFont(Fonts.nativeFont(fnt, (float) (fnt.getSize2D() * ls))); + double px = mapX(x, y); + double py = mapY(x, y); + if (upright || ls == 0 || !g.isTransformSupported()) { + g.drawStringBaseline(str, (int) Math.round(px), (int) Math.round(py)); + return; + } + Transform saved = g.getTransform(); + Transform t = saved.copy(); + t.concatenate(Transform.makeAffine(a / ls, b / ls, c / ls, d / ls, px, py)); + g.setTransform(t); + g.drawStringBaseline(str, 0, 0); + g.setTransform(saved); + } + + // ------------------------------------------------------------ images + + private boolean image(Image img, double x, double y, double w, double h, Color bg) { + if (img == null) { + return true; + } + com.codename1.ui.Image cn1 = img.cn1Image(); + if (cn1 == null || w <= 0 || h <= 0) { + return false; + } + sync(); + if (bg != null) { + applyColor(bg); + if (upright) { + fillSnapped(x, y, w, h); + } + } + g.setAlpha(Math.round(target.alpha * composite.getAlpha())); + dirty = true; + if (upright || !g.isTransformSupported()) { + int[] r = deviceRect(x, y, w, h); + if (r[2] == cn1.getWidth() && r[3] == cn1.getHeight()) { + g.drawImage(cn1, r[0], r[1]); + } else { + g.drawImage(cn1, r[0], r[1], Math.max(1, r[2]), Math.max(1, r[3])); + } + return true; + } + Transform saved = g.getTransform(); + Transform t = saved.copy(); + t.concatenate(Transform.makeAffine(a, b, c, d, mapX(x, y), mapY(x, y))); + g.setTransform(t); + g.drawImage(cn1, 0, 0, (int) Math.max(1, Math.round(w)), (int) Math.max(1, Math.round(h))); + g.setTransform(saved); + return true; + } + + @Override + public boolean drawImage(Image img, int x, int y, ImageObserver observer) { + return img == null || image(img, x, y, img.getWidth(observer), img.getHeight(observer), null); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { + return image(img, x, y, width, height, null); + } + + @Override + public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { + return img == null || image(img, x, y, img.getWidth(observer), img.getHeight(observer), bgcolor); + } + + @Override + public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, + ImageObserver observer) { + return image(img, x, y, width, height, bgcolor); + } + + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, + ImageObserver observer) { + return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null, observer); + } + + /// Draws the source rectangle of the image scaled onto the destination + /// rectangle. A destination or source given right-to-left or + /// bottom-to-top is drawn unflipped. + @Override + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, + Color bgcolor, ImageObserver observer) { + if (img == null) { + return true; + } + int dw = Math.abs(dx2 - dx1); + int dh = Math.abs(dy2 - dy1); + int sw = Math.abs(sx2 - sx1); + int sh = Math.abs(sy2 - sy1); + if (dw == 0 || dh == 0 || sw == 0 || sh == 0) { + return true; + } + int dx = Math.min(dx1, dx2); + int dy = Math.min(dy1, dy2); + double fx = dw / (double) sw; + double fy = dh / (double) sh; + G2D part = new G2D(this); + part.clipTo(dx, dy, dw, dh, false); + return part.image(img, dx - Math.min(sx1, sx2) * fx, dy - Math.min(sy1, sy2) * fy, + img.getWidth(observer) * fx, img.getHeight(observer) * fy, bgcolor); + } + + @Override + public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { + if (img == null) { + return true; + } + G2D moved = new G2D(this); + if (xform != null) { + moved.transform(xform); + } + return moved.image(img, 0, 0, img.getWidth(obs), img.getHeight(obs), null); + } + + // ------------------------------------------------------------ peers + + /// Lets a Codename One component paint itself where this graphics has + /// its origin: `painter` runs with the context carrying this graphics' + /// clip and alpha and shifted so that the component, which paints at its + /// own position (`x`, `y`), lands on the origin. A scaled or rotated + /// user transform is applied through the context where it has one. + public void paintNative(int x, int y, NativePainter painter) { + sync(); + g.setAlpha(Math.round(target.alpha * composite.getAlpha())); + double px = mapX(0, 0); + double py = mapY(0, 0); + boolean plain = upright && Math.abs(a - unit) < 1e-6 && Math.abs(d - unit) < 1e-6; + if (plain || !g.isTransformSupported()) { + int shiftX = (int) Math.round(px) - x; + int shiftY = (int) Math.round(py) - y; + g.translate(shiftX, shiftY); + painter.paint(g); + g.translate(-shiftX, -shiftY); + } else { + Transform saved = g.getTransform(); + Transform t = saved.copy(); + t.concatenate(Transform.makeAffine(a / unit, b / unit, c / unit, d / unit, px, py)); + t.translate(-x, -y); + g.setTransform(t); + painter.paint(g); + g.setTransform(saved); + } + target.active = null; + } + + /// Something that paints with a Codename One context; see + /// [#paintNative]. + public interface NativePainter { + void paint(com.codename1.ui.Graphics g); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Icons.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Icons.java new file mode 100644 index 00000000000..9b06da60068 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Icons.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Image; +import com.codename1.desktopcompat.java.awt.image.BufferedImage; +import com.codename1.desktopcompat.javax.swing.Icon; +import com.codename1.desktopcompat.javax.swing.ImageIcon; + +/// Turns a Swing icon into the image a Codename One widget shows. +public final class Icons { + + private Icons() { + } + + /// The icon as an image of its logical size in device pixels. An icon + /// that is not an image is painted once, at one image pixel per logical + /// pixel, and scaled. + public static com.codename1.ui.Image toNative(Icon icon, Component c) { + if (icon == null) { + return null; + } + int w = icon.getIconWidth(); + int h = icon.getIconHeight(); + if (w <= 0 || h <= 0) { + return null; + } + com.codename1.ui.Image img = null; + if (icon instanceof ImageIcon) { + Image i = ((ImageIcon) icon).getImage(); + if (i != null) { + img = i.cn1Image(); + } + } + if (img == null) { + BufferedImage b = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics g = b.createGraphics(); + try { + icon.paintIcon(c, g, 0, 0); + } finally { + g.dispose(); + } + img = b.cn1Image(); + } + if (img == null) { + return null; + } + int dw = Math.max(1, Units.toDevice(w)); + int dh = Math.max(1, Units.toDevice(h)); + if (img.getWidth() != dw || img.getHeight() != dh) { + img = img.scaled(dw, dh); + } + return img; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/LabelPeer.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/LabelPeer.java new file mode 100644 index 00000000000..7c1c7ee0b80 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/LabelPeer.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.ui.Graphics; + +/// The peer of a label: a Codename One label. +public class LabelPeer extends com.codename1.ui.Label implements Peer { + + private final PeerSupport support; + + public LabelPeer(Component owner) { + support = new PeerSupport(owner, this); + } + + @Override + public PeerSupport support() { + return support; + } + + @Override + public boolean nativeLook() { + return true; + } + + @Override + public void paintNativeLook(Graphics g) { + support.paintStyleBackground(g); + super.paint(g); + super.paintBorder(g); + } + + @Override + public void paintNativeChildren(Graphics g) { + } + + @Override + public void paint(Graphics g) { + support.paintOwner(g); + } + + @Override + protected void paintBorder(Graphics g) { + } + + @Override + public void repaint() { + if (support == null || !support.routeRepaint()) { + super.repaint(); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/LayoutBridge.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/LayoutBridge.java new file mode 100644 index 00000000000..fcfb272814b --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/LayoutBridge.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.ui.geom.Dimension; +import com.codename1.ui.layouts.Layout; + +/// The Codename One layout of a container peer: it runs the AWT layout, +/// which places the child peers as it sets the children's bounds. +/// +/// Only the root of a peer tree takes its size from Codename One; below +/// it the AWT bounds are the truth and the peers follow. +public final class LayoutBridge extends Layout { + + private final Container owner; + + public LayoutBridge(Container owner) { + this.owner = owner; + } + + @Override + public void layoutContainer(com.codename1.ui.Container parent) { + if (!(parent.getParent() instanceof ContainerPeer)) { + owner.cn1PeerResized(Units.toLogical(parent.getWidth()), Units.toLogical(parent.getHeight())); + } + owner.validate(); + } + + @Override + public Dimension getPreferredSize(com.codename1.ui.Container parent) { + com.codename1.desktopcompat.java.awt.Dimension d = owner.getPreferredSize(); + float s = Units.scale(); + return new Dimension((int) Math.ceil(d.width * s), (int) Math.ceil(d.height * s)); + } + + @Override + public boolean isOverlapSupported() { + return true; + } + + @Override + public boolean equals(Object o) { + return o instanceof LayoutBridge && ((LayoutBridge) o).owner == owner; + } + + @Override + public int hashCode() { + return System.identityHashCode(owner); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/NativeImage.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/NativeImage.java new file mode 100644 index 00000000000..2cdd09a7c9f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/NativeImage.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Image; +import com.codename1.desktopcompat.java.awt.image.ImageObserver; + +/// An AWT image that is a Codename One image: one decoded from a file or a +/// resource, or the result of scaling another. +public final class NativeImage extends Image { + + private final com.codename1.ui.Image image; + + public NativeImage(com.codename1.ui.Image image) { + this.image = image; + } + + @Override + public int getWidth(ImageObserver observer) { + return image.getWidth(); + } + + @Override + public int getHeight(ImageObserver observer) { + return image.getHeight(); + } + + /// Only an image created to be drawn into has a graphics, as in AWT. + @Override + public Graphics getGraphics() { + throw new UnsupportedOperationException("getGraphics() is only valid for images created off screen"); + } + + @Override + public com.codename1.ui.Image cn1Image() { + return image; + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Peer.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Peer.java new file mode 100644 index 00000000000..f29571183b5 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Peer.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +/// What every Codename One component that shows an AWT component +/// implements. +/// +/// A peer never paints itself the way Codename One would: it is not +/// opaque, its `paint` hands over to [PeerSupport#paintOwner], which runs +/// the AWT component's `paint`, and its `paintBorder` does nothing. The +/// AWT side then calls back for the two things only the peer can draw. +public interface Peer { + + /// The support object made in the peer's constructor. + PeerSupport support(); + + /// Whether the peer is a Codename One widget with a look of its own + /// (a button, a label), as opposed to a blank canvas or container. + boolean nativeLook(); + + /// Paints the widget as Codename One would: style background, content + /// and border. Nothing for a peer without a native look. + void paintNativeLook(com.codename1.ui.Graphics g); + + /// Paints the child peers; nothing for a peer that has none. + void paintNativeChildren(com.codename1.ui.Graphics g); +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/PeerSupport.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/PeerSupport.java new file mode 100644 index 00000000000..07379167c3f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/PeerSupport.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Font; +import com.codename1.ui.Display; +import com.codename1.ui.Painter; +import com.codename1.ui.events.FocusListener; +import com.codename1.ui.geom.Rectangle; +import com.codename1.ui.plaf.Border; +import com.codename1.ui.plaf.Style; + +/// The half of a peer that is the same for every peer: it connects one +/// Codename One component to the AWT component it shows. +public final class PeerSupport { + + private final Component owner; + private final com.codename1.ui.Component peer; + private G2D painting; + + public PeerSupport(Component owner, com.codename1.ui.Component peer) { + this.owner = owner; + this.peer = peer; + peer.setOpaque(false); + } + + /// The AWT component. + public Component owner() { + return owner; + } + + /// Runs the owner's painting with `g`, the context the peer's `paint` + /// received. The graphics starts with the owner's foreground and font. + public void paintOwner(com.codename1.ui.Graphics g) { + G2D g2 = G2D.forPeer(g, peer.getX(), peer.getY(), peer.getWidth(), peer.getHeight()); + G2D saved = painting; + painting = g2; + try { + Color fg = owner.getForeground(); + if (fg != null) { + g2.setColor(fg); + } + Font f = owner.getFont(); + if (f != null) { + g2.setFont(f); + } + owner.update(g2); + } finally { + painting = saved; + g2.finish(); + } + } + + /// Whether `g` draws to the context this peer is being painted with. + public boolean isPainting(G2D g) { + return painting != null && g.nativeGraphics() == painting.nativeGraphics(); + } + + /// Paints the peer's native look at the origin of `g`. + public void paintNative(G2D g) { + if (peer instanceof Peer && ((Peer) peer).nativeLook()) { + g.paintNative(peer.getX(), peer.getY(), new NativePass((Peer) peer, false)); + } + } + + /// Paints the peer's children at the origin of `g`. + public void paintChildren(G2D g) { + if (peer instanceof Peer) { + g.paintNative(peer.getX(), peer.getY(), new NativePass((Peer) peer, true)); + } + } + + /// One pass of a peer's own painting: its look, or its children. + private static final class NativePass implements G2D.NativePainter { + private final Peer target; + private final boolean children; + + NativePass(Peer target, boolean children) { + this.target = target; + this.children = children; + } + + @Override + public void paint(com.codename1.ui.Graphics ng) { + if (children) { + target.paintNativeChildren(ng); + } else { + target.paintNativeLook(ng); + } + } + } + + /// Paints the background the peer's style describes, which Codename + /// One itself no longer paints because the peer is not opaque. + public void paintStyleBackground(com.codename1.ui.Graphics g) { + Style s = peer.getStyle(); + Border b = s.getBorder(); + if (b != null && b.isBackgroundPainter()) { + b.paintBorderBackground(g, peer); + return; + } + Painter p = s.getBgPainter(); + if (p != null) { + p.paint(g, new Rectangle(peer.getX(), peer.getY(), peer.getWidth(), peer.getHeight())); + } + } + + /// Gives a canvas or container peer a style that draws and reserves + /// nothing. + public static void strip(com.codename1.ui.Component c) { + c.setUIID("Container"); + Style s = c.getAllStyles(); + s.setBgTransparency(0); + s.setPadding(0, 0, 0, 0); + s.setMargin(0, 0, 0, 0); + s.setBorder(null); + } + + /// Pushes the foreground, background and font that were set on the + /// owner into the styles of a peer with a native look. + public void applyStyle() { + if (!(peer instanceof Peer) || !((Peer) peer).nativeLook()) { + return; + } + Style s = peer.getAllStyles(); + if (owner.isForegroundSet()) { + s.setFgColor(owner.getForeground().getRGB() & 0xffffff); + } + if (owner.isBackgroundSet()) { + Color bg = owner.getBackground(); + s.setBgColor(bg.getRGB() & 0xffffff); + s.setBgTransparency(bg.getAlpha()); + } + if (owner.isFontSet()) { + Font f = owner.getFont(); + s.setFont(Fonts.nativeFont(f, f.getSize2D() * Units.scale())); + } + } + + /// Reports the peer's focus changes to the owner as focus events. + public void trackFocus() { + peer.addFocusListener(new FocusListener() { + @Override + public void focusGained(com.codename1.ui.Component cmp) { + EventBridge.setFocusOwner(owner, false); + } + + @Override + public void focusLost(com.codename1.ui.Component cmp) { + EventBridge.nativeFocusLost(owner); + } + }); + } + + /// Sends a repaint the peer asked for through the root of the peer + /// tree, because what lies behind a peer is painted by its ancestors. + /// Answers `false` when the peer is the root and must repaint itself. + public boolean routeRepaint() { + if (owner.getParent() == null || owner.getParent().cn1PeerOrNull() == null) { + return false; + } + repaint(owner, 0, 0, owner.getWidth(), owner.getHeight()); + return true; + } + + private static Component root(Component c) { + Component t = c; + while (t.getParent() != null && t.getParent().cn1PeerOrNull() != null) { + t = t.getParent(); + } + return t; + } + + /// Repaints a rectangle of `c`, given in its logical coordinates. + public static void repaint(Component c, int x, int y, int width, int height) { + com.codename1.ui.Component p = c.cn1PeerOrNull(); + if (p == null || p.getComponentForm() == null) { + return; + } + com.codename1.ui.Component rp = root(c).cn1PeerOrNull(); + float s = Units.scale(); + int x1 = (int) Math.floor(x * s); + int y1 = (int) Math.floor(y * s); + int x2 = (int) Math.ceil((x + width) * s); + int y2 = (int) Math.ceil((y + height) * s); + rp.repaint(p.getAbsoluteX() + x1, p.getAbsoluteY() + y1, x2 - x1, y2 - y1); + } + + /// Has the window `c` is in laid out again on the next turn of the + /// event dispatch thread, and repainted. + public static void scheduleLayout(Component c) { + if (!Display.isInitialized()) { + return; + } + com.codename1.ui.Component rp = root(c).cn1PeerOrNull(); + if (rp instanceof com.codename1.ui.Container && rp.getComponentForm() != null) { + ((com.codename1.ui.Container) rp).revalidateLater(); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/RadioButtonPeer.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/RadioButtonPeer.java new file mode 100644 index 00000000000..c4c8b942b8f --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/RadioButtonPeer.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.ui.Graphics; + +/// The peer of a radio button: a Codename One radio button. Grouping is +/// done by the Swing button group, not by Codename One. +public class RadioButtonPeer extends com.codename1.ui.RadioButton implements Peer { + + private final PeerSupport support; + + public RadioButtonPeer(Component owner) { + support = new PeerSupport(owner, this); + support.trackFocus(); + } + + @Override + public PeerSupport support() { + return support; + } + + @Override + public boolean nativeLook() { + return true; + } + + @Override + public void paintNativeLook(Graphics g) { + support.paintStyleBackground(g); + super.paint(g); + super.paintBorder(g); + } + + @Override + public void paintNativeChildren(Graphics g) { + } + + @Override + public void paint(Graphics g) { + support.paintOwner(g); + } + + @Override + protected void paintBorder(Graphics g) { + } + + @Override + public void repaint() { + if (support == null || !support.routeRepaint()) { + super.repaint(); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/TextFieldPeer.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/TextFieldPeer.java new file mode 100644 index 00000000000..1dec16e1584 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/TextFieldPeer.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.ui.Graphics; + +/// The peer of a text field: a Codename One text field, edited natively. +public class TextFieldPeer extends com.codename1.ui.TextField implements Peer { + + private final PeerSupport support; + + public TextFieldPeer(Component owner) { + support = new PeerSupport(owner, this); + support.trackFocus(); + } + + @Override + public PeerSupport support() { + return support; + } + + @Override + public boolean nativeLook() { + return true; + } + + @Override + public void paintNativeLook(Graphics g) { + support.paintStyleBackground(g); + super.paint(g); + super.paintBorder(g); + } + + @Override + public void paintNativeChildren(Graphics g) { + } + + @Override + public void paint(Graphics g) { + support.paintOwner(g); + } + + @Override + protected void paintBorder(Graphics g) { + } + + @Override + public void repaint() { + if (support == null || !support.routeRepaint()) { + super.repaint(); + } + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Units.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Units.java new file mode 100644 index 00000000000..b40ae157ba2 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/Units.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import com.codename1.ui.Display; + +/// The size of an AWT pixel. +/// +/// Desktop code lays itself out in pixels and assumes roughly 96 of them to +/// the inch: a 24 pixel row, a 12 point font. A phone packs three or four +/// device pixels into that distance, so every AWT coordinate is a *logical* +/// pixel of 1/96 inch and this class holds the number of device pixels one +/// of them covers. Component bounds, font sizes and everything drawn through +/// [G2D] are multiplied by it on the way to Codename One, and peer bounds +/// are snapped to whole device pixels edge by edge, so two components that +/// touch in logical pixels touch on the screen too. +/// +/// The scale comes from the display's density. The Display property +/// `desktopcompat.scale` overrides it for an application that wants its +/// pixels larger or smaller, and [#setScale(float)] overrides both -- that +/// is what tests use to get round numbers. +public final class Units { + + private static float override; + private static float property = -1; + + private Units() { + } + + /// Device pixels per logical pixel. + public static float scale() { + if (override > 0) { + return override; + } + if (!Display.isInitialized()) { + return 1f; + } + Display d = Display.getInstance(); + if (property < 0) { + property = 0; + String p = d.getProperty("desktopcompat.scale", null); + if (p != null && p.length() > 0) { + try { + property = Float.parseFloat(p); + } catch (NumberFormatException e) { + property = 0; + } + } + } + if (property > 0) { + return property; + } + // convertToPixels takes millimetres; a logical pixel is 25.4/96 of one. + float perMm = d.convertToPixels(1000, true) / 1000f; + float s = perMm * 25.4f / 96f; + return s > 0 ? s : 1f; + } + + /// Fixes the scale, or with 0 goes back to the display's own. Also + /// forgets the `desktopcompat.scale` property so it is read again. + public static void setScale(float scale) { + override = scale; + property = -1; + } + + /// The device pixel a logical coordinate snaps to. + public static int toDevice(float logical) { + return Math.round(logical * scale()); + } + + /// The device length of the logical span `[position, position + size)`: + /// the distance between its two snapped edges, not the snapped length, + /// so adjacent spans never gap or overlap. + public static int toDeviceSize(int position, int size) { + float s = scale(); + return Math.round((position + size) * s) - Math.round(position * s); + } + + /// The logical pixel a device coordinate falls in, and the largest + /// logical length that fits in `device` pixels. + public static int toLogical(int device) { + return (int) Math.floor(device / scale() + 0.001f); + } + + /// The smallest logical length that covers `device` pixels; what a + /// preferred size measured by Codename One becomes. + public static int toLogicalCeil(int device) { + return (int) Math.ceil(device / scale() - 0.001f); + } + + /// A device coordinate in logical pixels, unrounded. + public static float toLogicalExact(float device) { + return device / scale(); + } +} diff --git a/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/package-info.java b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/package-info.java new file mode 100644 index 00000000000..47f244e9c48 --- /dev/null +++ b/maven/swing-compat/src/main/java/com/codename1/desktopcompat/rt/package-info.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// The runtime under the AWT and Swing classes: unit conversion +/// ([com.codename1.desktopcompat.rt.Units]), the graphics that draws +/// through a Codename One context ([com.codename1.desktopcompat.rt.G2D]), +/// the peers that show components, and the bridges that carry layout and +/// input between the two toolkits. +/// +/// Nothing here has a desktop counterpart; applications do not name these +/// classes. +package com.codename1.desktopcompat.rt; diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/ApiShapeTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/ApiShapeTest.java new file mode 100644 index 00000000000..855a0672afd --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/ApiShapeTest.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/// Holds every public class of the layer to the shape of the JDK class it +/// stands in for. +/// +/// An application is compiled against the JDK's `java.awt` and `javax.swing` +/// and only then relocated onto this module, so a member that is spelled +/// differently here -- another parameter type, another return type, a method +/// the JDK never had -- is a link error on the device or an API nobody can +/// reach. For each public class under `com.codename1.desktopcompat.java` and +/// `.javax` the JDK class of the original name is loaded and compared: +/// every public or protected constructor, method and field must exist there +/// with the same descriptor once the package prefix is mapped back, the +/// superclass must map to an ancestor of the JDK class and every interface +/// to one the JDK class implements. Members named `cn1...` are the layer's +/// own hooks and are exempt. +/// +/// The directory read is `target/classes`, or the `swingcompat.classes` +/// system property. +public class ApiShapeTest { + + private static final String PREFIX = "com.codename1.desktopcompat."; + + private final List problems = new ArrayList(); + private int checked; + + @Test + public void everyPublicClassMatchesTheJdk() throws Exception { + File root = new File(System.getProperty("swingcompat.classes", "target/classes")); + File base = new File(root, "com/codename1/desktopcompat"); + walk(new File(base, "java"), "com.codename1.desktopcompat.java"); + walk(new File(base, "javax"), "com.codename1.desktopcompat.javax"); + if (!problems.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(problems.size()).append(" API shape problem(s):\n"); + for (String p : problems) { + sb.append(" ").append(p).append('\n'); + } + fail(sb.toString()); + } + assertTrue("no classes found under " + base, checked > 0); + } + + private void walk(File dir, String pkg) throws Exception { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + walk(f, pkg + "." + f.getName()); + } else if (f.getName().endsWith(".class") && !f.getName().equals("package-info.class")) { + String simple = f.getName().substring(0, f.getName().length() - 6); + check(Class.forName(pkg + "." + simple, false, ApiShapeTest.class.getClassLoader())); + } + } + } + + private static boolean api(int modifiers) { + return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers); + } + + /// Whether the class and every class it is nested in can be named by an + /// application. + private static boolean visible(Class c) { + for (Class k = c; k != null; k = k.getDeclaringClass()) { + if (!api(k.getModifiers())) { + return false; + } + } + return !c.isAnonymousClass() && !c.isLocalClass(); + } + + private void check(Class ours) { + if (!visible(ours)) { + return; + } + checked++; + String name = ours.getName(); + Class real; + try { + real = Class.forName(name.substring(PREFIX.length()), false, ClassLoader.getSystemClassLoader()); + } catch (ClassNotFoundException e) { + problems.add(name + ": the JDK has no such class"); + return; + } catch (LinkageError e) { + problems.add(name + ": the JDK class does not load: " + e); + return; + } + if (ours.isInterface() != real.isInterface()) { + problems.add(name + ": interface here but not in the JDK, or the reverse"); + } + if (Modifier.isFinal(real.getModifiers()) != Modifier.isFinal(ours.getModifiers())) { + problems.add(name + ": final differs from the JDK"); + } + if (Modifier.isAbstract(ours.getModifiers()) && !Modifier.isAbstract(real.getModifiers())) { + problems.add(name + ": abstract here but instantiable in the JDK"); + } + Class sup = ours.getSuperclass(); + if (sup != null && sup != Object.class) { + Class mapped = map(sup); + if (mapped == null || !mapped.isAssignableFrom(real) || mapped.isInterface()) { + problems.add(name + ": superclass " + sup.getName() + " is not an ancestor of the JDK class"); + } + } + for (Class i : ours.getInterfaces()) { + Class mapped = map(i); + if (mapped == null || !mapped.isAssignableFrom(real)) { + problems.add(name + ": interface " + i.getName() + " is not implemented by the JDK class"); + } + } + for (Constructor c : ours.getDeclaredConstructors()) { + if (!api(c.getModifiers()) || c.isSynthetic()) { + continue; + } + Class[] params = mapAll(c.getParameterTypes()); + boolean found = false; + if (params != null) { + try { + found = api(real.getDeclaredConstructor(params).getModifiers()); + } catch (NoSuchMethodException e) { + found = false; + } + } + if (!found) { + problems.add(name + ": constructor " + c + " is not in the JDK"); + } + } + for (Method m : ours.getDeclaredMethods()) { + if (!api(m.getModifiers()) || m.isSynthetic() || m.isBridge() || m.getName().startsWith("cn1")) { + continue; + } + Class[] params = mapAll(m.getParameterTypes()); + Class ret = map(m.getReturnType()); + Method found = params == null || ret == null ? null : findMethod(real, m.getName(), params, ret); + if (found == null) { + problems.add(name + ": method " + m + " is not in the JDK"); + } else if (Modifier.isStatic(found.getModifiers()) != Modifier.isStatic(m.getModifiers())) { + problems.add(name + ": method " + m + " is static here but not in the JDK, or the reverse"); + } else if (Modifier.isAbstract(m.getModifiers()) && !Modifier.isAbstract(found.getModifiers())) { + problems.add(name + ": method " + m + " is abstract here but concrete in the JDK"); + } + } + for (Field f : ours.getDeclaredFields()) { + if (!api(f.getModifiers()) || f.isSynthetic() || f.getName().startsWith("cn1")) { + continue; + } + Class type = map(f.getType()); + Field found = type == null ? null : findField(real, f.getName(), type); + if (found == null) { + problems.add(name + ": field " + f + " is not in the JDK"); + } else if (Modifier.isStatic(found.getModifiers()) != Modifier.isStatic(f.getModifiers())) { + problems.add(name + ": field " + f + " is static here but not in the JDK, or the reverse"); + } + } + } + + private static Method findMethod(Class real, String name, Class[] params, Class ret) { + if (real == null) { + return null; + } + for (Method m : real.getDeclaredMethods()) { + if (m.getName().equals(name) && api(m.getModifiers()) && m.getReturnType() == ret + && java.util.Arrays.equals(m.getParameterTypes(), params)) { + return m; + } + } + Method up = findMethod(real.getSuperclass(), name, params, ret); + if (up != null) { + return up; + } + for (Class i : real.getInterfaces()) { + up = findMethod(i, name, params, ret); + if (up != null) { + return up; + } + } + return null; + } + + private static Field findField(Class real, String name, Class type) { + if (real == null) { + return null; + } + for (Field f : real.getDeclaredFields()) { + if (f.getName().equals(name) && api(f.getModifiers()) && f.getType() == type) { + return f; + } + } + Field up = findField(real.getSuperclass(), name, type); + if (up != null) { + return up; + } + for (Class i : real.getInterfaces()) { + up = findField(i, name, type); + if (up != null) { + return up; + } + } + return null; + } + + private static Class[] mapAll(Class[] types) { + Class[] out = new Class[types.length]; + for (int i = 0; i < types.length; i++) { + out[i] = map(types[i]); + if (out[i] == null) { + return null; + } + } + return out; + } + + /// The JDK type a type of ours stands for, or null when it is a layer + /// type the JDK has no counterpart of. + private static Class map(Class c) { + if (c.isArray()) { + Class component = map(c.getComponentType()); + return component == null ? null : java.lang.reflect.Array.newInstance(component, 0).getClass(); + } + String name = c.getName(); + if (!name.startsWith(PREFIX)) { + return c; + } + try { + return Class.forName(name.substring(PREFIX.length()), false, ClassLoader.getSystemClassLoader()); + } catch (ClassNotFoundException e) { + return null; + } catch (LinkageError e) { + return null; + } + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/ConstantParityTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/ConstantParityTest.java new file mode 100644 index 00000000000..0b575820f1f --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/ConstantParityTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/// Holds every constant of the layer to the value of the same constant in +/// the JDK. +/// +/// A compiler inlines a `public static final` primitive or `String` into the +/// application that reads it, so a constant here that differs from the JDK +/// does not take effect in code compiled against the JDK: the application +/// keeps the JDK's number. For each public class under +/// `com.codename1.desktopcompat.java` and `.javax` every such field is +/// compared with the field of the same name of the JDK class. Classes the +/// JDK lacks are left to `ApiShapeTest`, and fields named `cn1...` are the +/// layer's own. +/// +/// The directory read is `target/classes`, or the `swingcompat.classes` +/// system property. +public class ConstantParityTest { + + private static final String PREFIX = "com.codename1.desktopcompat."; + + private final List problems = new ArrayList(); + + private int compared; + + @Test + public void constantsHaveTheValuesOfTheJdk() throws Exception { + File root = new File(System.getProperty("swingcompat.classes", "target/classes")); + File base = new File(root, "com/codename1/desktopcompat"); + walk(new File(base, "java"), "com.codename1.desktopcompat.java"); + walk(new File(base, "javax"), "com.codename1.desktopcompat.javax"); + if (!problems.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(problems.size()).append(" constant(s) differ from the JDK:\n"); + for (String p : problems) { + sb.append(" ").append(p).append('\n'); + } + fail(sb.toString()); + } + assertTrue("no constants compared under " + base, compared > 0); + } + + private void walk(File dir, String pkg) throws Exception { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + walk(f, pkg + "." + f.getName()); + } else if (f.getName().endsWith(".class") && !f.getName().equals("package-info.class")) { + String simple = f.getName().substring(0, f.getName().length() - 6); + check(Class.forName(pkg + "." + simple, false, ConstantParityTest.class.getClassLoader())); + } + } + } + + private void check(Class ours) throws Exception { + if (!Modifier.isPublic(ours.getModifiers())) { + return; + } + Class real; + try { + real = Class.forName(ours.getName().substring(PREFIX.length()), false, + ClassLoader.getSystemClassLoader()); + } catch (ClassNotFoundException e) { + return; + } catch (LinkageError e) { + return; + } + for (Field f : ours.getDeclaredFields()) { + int m = f.getModifiers(); + if (!Modifier.isPublic(m) || !Modifier.isStatic(m) || !Modifier.isFinal(m) + || f.getName().startsWith("cn1")) { + continue; + } + Class type = f.getType(); + if (!type.isPrimitive() && type != String.class) { + continue; + } + Field other; + try { + other = real.getField(f.getName()); + } catch (NoSuchFieldException e) { + continue; + } + if (!Modifier.isStatic(other.getModifiers()) || other.getType() != type) { + continue; + } + compared++; + Object mine = f.get(null); + Object theirs = other.get(null); + if (!mine.equals(theirs)) { + problems.add(ours.getName() + "." + f.getName() + " is " + mine + " but the JDK has " + theirs); + } + } + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/EdtAndGraphicsTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/EdtAndGraphicsTest.java new file mode 100644 index 00000000000..e15083b5474 --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/EdtAndGraphicsTest.java @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.codename1.compat.testing.MainThreadRule; +import com.codename1.desktopcompat.java.awt.AlphaComposite; +import com.codename1.desktopcompat.java.awt.EventQueue; +import com.codename1.desktopcompat.java.awt.Rectangle; +import com.codename1.desktopcompat.java.awt.event.ActionEvent; +import com.codename1.desktopcompat.java.awt.event.ActionListener; +import com.codename1.desktopcompat.java.awt.geom.AffineTransform; +import com.codename1.desktopcompat.javax.swing.SwingUtilities; +import com.codename1.desktopcompat.javax.swing.Timer; +import com.codename1.desktopcompat.rt.G2D; +import com.codename1.ui.Image; +import java.util.List; +import org.junit.Test; + +/// The event dispatch thread helpers, and the transform and clip +/// arithmetic of the graphics a peer paints with. +public class EdtAndGraphicsTest extends KernelTestBase { + + private static G2D graphics() { + Image target = Image.createImage(400, 400); + return G2D.forPeer(target.getGraphics(), 30, 40, 200, 100); + } + + @Test + public void theTestRunsOnTheEventDispatchThread() { + assertTrue(SwingUtilities.isEventDispatchThread()); + assertTrue(EventQueue.isDispatchThread()); + } + + @Test + public void invokeLaterRunsAfterTheCurrentEvent() { + final boolean[] ran = new boolean[1]; + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + ran[0] = true; + } + }); + assertFalse(ran[0]); + MainThreadRule.drain(); + assertTrue(ran[0]); + } + + @Test + public void invokeAndWaitOnTheEventDispatchThreadRunsAtOnce() throws Exception { + final boolean[] ran = new boolean[1]; + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + ran[0] = true; + } + }); + assertTrue(ran[0]); + } + + @Test + public void invokeAndWaitFromAnotherThreadRunsOnTheEventDispatchThread() throws Exception { + final boolean[] onEdt = new boolean[1]; + final boolean[] done = new boolean[1]; + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + EventQueue.invokeAndWait(new Runnable() { + @Override + public void run() { + onEdt[0] = EventQueue.isDispatchThread(); + } + }); + } catch (InterruptedException e) { + return; + } + done[0] = true; + } + }); + t.start(); + for (int i = 0; i < 400 && t.isAlive(); i++) { + MainThreadRule.drain(); + Thread.sleep(5); + } + t.join(2000); + assertTrue(done[0]); + assertTrue(onEdt[0]); + } + + @Test + public void aTimerFiresOnTheEventDispatchThreadUntilStopped() throws Exception { + final int[] fired = new int[1]; + final boolean[] onEdt = {true}; + Timer timer = new Timer(5, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + fired[0]++; + onEdt[0] &= EventQueue.isDispatchThread(); + } + }); + assertFalse(timer.isRunning()); + timer.start(); + assertTrue(timer.isRunning()); + for (int i = 0; i < 400 && fired[0] < 3; i++) { + MainThreadRule.drain(); + Thread.sleep(5); + } + timer.stop(); + assertFalse(timer.isRunning()); + assertTrue(fired[0] >= 3); + assertTrue(onEdt[0]); + int after = fired[0]; + Thread.sleep(30); + MainThreadRule.drain(); + assertEquals(after, fired[0]); + + final int[] once = new int[1]; + Timer single = new Timer(1, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + once[0]++; + } + }); + single.setRepeats(false); + single.start(); + for (int i = 0; i < 400 && once[0] == 0; i++) { + MainThreadRule.drain(); + Thread.sleep(5); + } + Thread.sleep(20); + MainThreadRule.drain(); + assertEquals(1, once[0]); + assertFalse(single.isRunning()); + } + + @Test + public void userSpaceStartsAtThePeerOriginScaledToDevicePixels() { + G2D g = graphics(); + assertArrayEquals(new double[]{2, 0, 0, 2, 30, 40}, g.deviceMatrix(), 1e-9); + assertTrue(g.getTransform().isIdentity()); + assertArrayEquals(new int[]{30, 40, 20, 10}, g.deviceRect(0, 0, 10, 5)); + g.finish(); + } + + @Test + public void translateAndScaleComposeLikeJava2D() { + G2D g = graphics(); + g.translate(5, 6); + g.scale(3, 2); + assertArrayEquals(new double[]{6, 0, 0, 4, 40, 52}, g.deviceMatrix(), 1e-9); + assertArrayEquals(new int[]{46, 56, 12, 8}, g.deviceRect(1, 1, 2, 2)); + AffineTransform t = g.getTransform(); + assertEquals(5, t.getTranslateX(), 1e-9); + assertEquals(3, t.getScaleX(), 1e-9); + g.setTransform(new AffineTransform()); + assertArrayEquals(new double[]{2, 0, 0, 2, 30, 40}, g.deviceMatrix(), 1e-9); + g.finish(); + } + + @Test + public void aCopyHasItsOwnTransform() { + G2D g = graphics(); + G2D copy = (G2D) g.create(); + copy.translate(10, 10); + assertArrayEquals(new double[]{2, 0, 0, 2, 30, 40}, g.deviceMatrix(), 1e-9); + assertArrayEquals(new double[]{2, 0, 0, 2, 50, 60}, copy.deviceMatrix(), 1e-9); + copy.dispose(); + G2D moved = (G2D) g.create(10, 20, 30, 30); + assertArrayEquals(new double[]{2, 0, 0, 2, 50, 80}, moved.deviceMatrix(), 1e-9); + Rectangle clip = moved.getClipBounds(); + assertTrue(clip.width <= 30 && clip.height <= 30); + g.finish(); + } + + @Test + public void aQuarterTurnMapsARectangleToItsRotatedBounds() { + G2D g = graphics(); + g.rotate(Math.PI / 2); + int[] r = g.deviceRect(0, 0, 10, 5); + assertArrayEquals(new int[]{20, 40, 10, 20}, r); + g.finish(); + } + + @Test + public void clipsIntersectInUserSpace() { + G2D g = graphics(); + g.setClip(10, 10, 50, 40); + assertEquals(new Rectangle(10, 10, 50, 40), g.getClipBounds()); + g.clipRect(30, 0, 100, 20); + assertEquals(new Rectangle(30, 10, 30, 10), g.getClipBounds()); + g.translate(30, 10); + assertEquals(new Rectangle(0, 0, 30, 10), g.getClipBounds()); + g.finish(); + } + + @Test + public void theClipNeverLeavesThePeer() { + G2D g = graphics(); + g.setClip(-50, -50, 1000, 1000); + Rectangle clip = g.getClipBounds(); + assertEquals(new Rectangle(0, 0, 100, 50), clip); + g.finish(); + } + + @Test + public void unsupportedJava2DFeaturesSayNo() { + G2D g = graphics(); + try { + g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR)); + assertTrue("expected an UnsupportedOperationException", false); + } catch (UnsupportedOperationException expected) { + assertTrue(expected.getMessage() != null); + } + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f)); + g.finish(); + } + + @Test + public void textIsDrawnAtTheScaledPosition() { + com.codename1.compat.testing.HeadlessImplementation.recordText = true; + G2D g = graphics(); + g.drawString("where", 10, 20); + g.finish(); + List text = com.codename1.compat.testing.HeadlessImplementation.drawnText; + Object[] drawn = find(text, "where"); + assertTrue(drawn != null); + assertEquals(50, ((Integer) drawn[1]).intValue()); + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/EventDeliveryTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/EventDeliveryTest.java new file mode 100644 index 00000000000..71957645d32 --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/EventDeliveryTest.java @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.codename1.desktopcompat.java.awt.BorderLayout; +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.FlowLayout; +import com.codename1.desktopcompat.java.awt.event.ActionEvent; +import com.codename1.desktopcompat.java.awt.event.ActionListener; +import com.codename1.desktopcompat.java.awt.event.FocusEvent; +import com.codename1.desktopcompat.java.awt.event.FocusListener; +import com.codename1.desktopcompat.java.awt.event.ItemEvent; +import com.codename1.desktopcompat.java.awt.event.ItemListener; +import com.codename1.desktopcompat.java.awt.event.KeyEvent; +import com.codename1.desktopcompat.java.awt.event.KeyListener; +import com.codename1.desktopcompat.java.awt.event.MouseAdapter; +import com.codename1.desktopcompat.java.awt.event.MouseEvent; +import com.codename1.desktopcompat.java.awt.event.MouseMotionAdapter; +import com.codename1.desktopcompat.javax.swing.ButtonGroup; +import com.codename1.desktopcompat.javax.swing.JButton; +import com.codename1.desktopcompat.javax.swing.JCheckBox; +import com.codename1.desktopcompat.javax.swing.JFrame; +import com.codename1.desktopcompat.javax.swing.JPanel; +import com.codename1.desktopcompat.javax.swing.JRadioButton; +import com.codename1.desktopcompat.javax.swing.JTextField; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +/// Input reaching a frame's form arrives as action, mouse, key and focus +/// events on the right component, in its logical coordinates. +public class EventDeliveryTest extends KernelTestBase { + + private static final class Mice extends MouseAdapter { + final List log = new ArrayList(); + MouseEvent last; + + private void add(String what, MouseEvent e) { + log.add(what); + last = e; + } + + @Override + public void mousePressed(MouseEvent e) { + add("pressed", e); + } + + @Override + public void mouseReleased(MouseEvent e) { + add("released", e); + } + + @Override + public void mouseClicked(MouseEvent e) { + add("clicked", e); + } + + @Override + public void mouseEntered(MouseEvent e) { + add("entered", e); + } + + @Override + public void mouseExited(MouseEvent e) { + add("exited", e); + } + } + + private static JPanel sized(int w, int h) { + JPanel p = new JPanel(null); + p.setPreferredSize(new Dimension(w, h)); + return p; + } + + @Test + public void clickingAButtonFiresOneActionEvent() { + JButton b = new JButton("Go"); + final List events = new ArrayList(); + b.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + events.add(e); + } + }); + Mice mice = new Mice(); + b.addMouseListener(mice); + JFrame f = new JFrame(); + f.add(b, BorderLayout.SOUTH); + show(f); + press(f, b, 5, 5); + release(f, b, 5, 5); + assertEquals(1, events.size()); + assertSame(b, events.get(0).getSource()); + assertEquals("Go", events.get(0).getActionCommand()); + assertTrue(mice.log.contains("pressed") && mice.log.contains("released") && mice.log.contains("clicked")); + } + + @Test + public void doClickAndTheNativeClickDriveTheModel() { + JCheckBox box = new JCheckBox("Check"); + final int[] items = new int[1]; + final int[] actions = new int[1]; + box.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + items[0] = e.getStateChange(); + } + }); + box.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + actions[0]++; + } + }); + JFrame f = new JFrame(); + f.add(box, BorderLayout.SOUTH); + show(f); + box.doClick(); + assertTrue(box.isSelected()); + assertEquals(ItemEvent.SELECTED, items[0]); + assertEquals(1, actions[0]); + assertTrue(((com.codename1.ui.CheckBox) box.cn1Peer()).isSelected()); + press(f, box, 5, 5); + release(f, box, 5, 5); + assertFalse(box.isSelected()); + assertEquals(ItemEvent.DESELECTED, items[0]); + assertEquals(2, actions[0]); + assertFalse(((com.codename1.ui.CheckBox) box.cn1Peer()).isSelected()); + } + + @Test + public void aButtonGroupKeepsOneRadioButtonSelected() { + JRadioButton a = new JRadioButton("A", true); + JRadioButton b = new JRadioButton("B"); + ButtonGroup group = new ButtonGroup(); + group.add(a); + group.add(b); + JFrame f = new JFrame(); + JPanel p = new JPanel(new FlowLayout()); + p.add(a); + p.add(b); + f.add(p, BorderLayout.CENTER); + show(f); + press(f, b, 5, 5); + release(f, b, 5, 5); + assertTrue(b.isSelected()); + assertFalse(a.isSelected()); + assertFalse(((com.codename1.ui.RadioButton) a.cn1Peer()).isSelected()); + press(f, b, 5, 5); + release(f, b, 5, 5); + assertTrue(b.isSelected()); + assertTrue(((com.codename1.ui.RadioButton) b.cn1Peer()).isSelected()); + } + + @Test + public void mouseEventsCarryLogicalCoordinatesAndGoToTheListeningAncestor() { + JPanel outer = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + JPanel pad = sized(30, 40); + JPanel inner = sized(100, 60); + outer.add(pad); + outer.add(inner); + Mice mice = new Mice(); + outer.addMouseListener(mice); + JFrame f = new JFrame(); + f.add(outer, BorderLayout.CENTER); + show(f); + assertEquals(30, inner.getX()); + press(f, inner, 7, 9); + assertEquals("entered", mice.log.get(0)); + assertEquals("pressed", mice.log.get(1)); + assertSame(outer, mice.last.getSource()); + assertEquals(37, mice.last.getX()); + assertEquals(9, mice.last.getY()); + assertEquals(MouseEvent.BUTTON1, mice.last.getButton()); + assertEquals(1, mice.last.getClickCount()); + release(f, inner, 7, 9); + assertEquals("clicked", mice.log.get(mice.log.size() - 1)); + + Mice innerMice = new Mice(); + inner.addMouseListener(innerMice); + mice.log.clear(); + press(f, inner, 7, 9); + assertSame(inner, innerMice.last.getSource()); + assertEquals(7, innerMice.last.getX()); + assertEquals(2, innerMice.last.getClickCount()); + assertEquals("exited", mice.log.get(0)); + release(f, inner, 7, 9); + } + + @Test + public void aDragGoesToThePressedComponentAndEndsWithoutAClick() { + JPanel a = sized(50, 50); + JPanel b = sized(50, 50); + JPanel content = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + content.add(a); + content.add(b); + Mice mice = new Mice(); + a.addMouseListener(mice); + final List drags = new ArrayList(); + a.addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseDragged(MouseEvent e) { + drags.add(e); + } + }); + JFrame f = new JFrame(); + f.add(content, BorderLayout.CENTER); + show(f); + press(f, a, 10, 10); + drag(f, b, 20, 30); + release(f, b, 20, 30); + assertEquals(1, drags.size()); + assertSame(a, drags.get(0).getSource()); + assertEquals(70, drags.get(0).getX()); + assertEquals(30, drags.get(0).getY()); + assertTrue(mice.log.contains("released")); + assertFalse(mice.log.contains("clicked")); + } + + @Test + public void keysGoToTheFocusOwnerElseToTheWindow() { + final List log = new ArrayList(); + KeyListener keys = new KeyListener() { + @Override + public void keyTyped(KeyEvent e) { + log.add("typed " + e.getKeyChar() + " " + name(e)); + } + + @Override + public void keyPressed(KeyEvent e) { + log.add("pressed " + e.getKeyCode() + " " + name(e)); + } + + @Override + public void keyReleased(KeyEvent e) { + log.add("released " + e.getKeyCode() + " " + name(e)); + } + + private String name(KeyEvent e) { + return e.getComponent().getName(); + } + }; + JFrame f = new JFrame(); + f.setName("frame"); + JPanel panel = new JPanel(); + panel.setName("panel"); + f.add(panel, BorderLayout.CENTER); + f.addKeyListener(keys); + panel.addKeyListener(keys); + show(f); + f.cn1Form().keyPressed('a'); + f.cn1Form().keyReleased('a'); + assertEquals("[pressed " + KeyEvent.VK_A + " frame, typed a frame, released " + KeyEvent.VK_A + " frame]", + log.toString()); + log.clear(); + assertTrue(panel.requestFocusInWindow()); + assertTrue(panel.isFocusOwner()); + f.cn1Form().keyPressed('7'); + assertEquals("[pressed " + KeyEvent.VK_7 + " panel, typed 7 panel]", log.toString()); + } + + @Test + public void focusMovesWithFocusLostAndFocusGained() { + final List log = new ArrayList(); + final JPanel a = new JPanel(); + final JPanel b = new JPanel(); + FocusListener l = new FocusListener() { + @Override + public void focusGained(FocusEvent e) { + log.add((e.getSource() == a ? "a" : "b") + " gained from " + + (e.getOppositeComponent() == null ? "none" : e.getOppositeComponent() == a ? "a" : "b")); + } + + @Override + public void focusLost(FocusEvent e) { + log.add((e.getSource() == a ? "a" : "b") + " lost"); + } + }; + a.addFocusListener(l); + b.addFocusListener(l); + JFrame f = new JFrame(); + JPanel content = new JPanel(); + content.add(a); + content.add(b); + f.add(content, BorderLayout.CENTER); + show(f); + a.requestFocus(); + b.requestFocus(); + assertEquals("[a gained from none, a lost, b gained from a]", log.toString()); + assertSame(b, f.getFocusOwner()); + content.remove(b); + assertNull(f.getFocusOwner()); + b.setFocusable(false); + assertFalse(b.requestFocusInWindow()); + } + + @Test + public void aTextFieldKeepsItsTextInThePeerAndPostsActions() { + JTextField field = new JTextField("abc", 10); + final String[] command = new String[1]; + field.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + command[0] = e.getActionCommand(); + } + }); + JFrame f = new JFrame(); + f.add(field, BorderLayout.NORTH); + show(f); + com.codename1.ui.TextField peer = (com.codename1.ui.TextField) field.cn1Peer(); + assertEquals("abc", peer.getText()); + peer.setText("typed"); + assertEquals("typed", field.getText()); + field.postActionEvent(); + assertEquals("typed", command[0]); + field.setText("set"); + assertEquals("set", peer.getText()); + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/FrameAndLayoutTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/FrameAndLayoutTest.java new file mode 100644 index 00000000000..dc3ce0a5cac --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/FrameAndLayoutTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.codename1.desktopcompat.java.awt.BorderLayout; +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.FlowLayout; +import com.codename1.desktopcompat.java.awt.GridLayout; +import com.codename1.desktopcompat.java.awt.event.WindowAdapter; +import com.codename1.desktopcompat.java.awt.event.WindowEvent; +import com.codename1.desktopcompat.javax.swing.JButton; +import com.codename1.desktopcompat.javax.swing.JFrame; +import com.codename1.desktopcompat.javax.swing.JLabel; +import com.codename1.desktopcompat.javax.swing.JPanel; +import com.codename1.desktopcompat.rt.ContainerPeer; +import com.codename1.desktopcompat.rt.Units; +import com.codename1.ui.Display; +import org.junit.Test; + +/// Showing a frame, and the bounds the layout managers give components +/// and their peers once it is shown. +public class FrameAndLayoutTest extends KernelTestBase { + + @Test + public void showingAFrameShowsItsFormAndSizesTheWindowToIt() { + JFrame f = new JFrame("Title"); + final int[] opened = new int[1]; + f.addWindowListener(new WindowAdapter() { + @Override + public void windowOpened(WindowEvent e) { + opened[0]++; + } + }); + assertFalse(f.isVisible()); + show(f); + assertTrue(f.isVisible()); + assertTrue(f.isShowing()); + assertSame(f.cn1Form(), Display.getInstance().getCurrent()); + assertEquals("Title", f.cn1Form().getTitle()); + assertEquals(1, opened[0]); + com.codename1.ui.Component peer = f.cn1Peer(); + assertTrue(peer.getWidth() > 0 && peer.getHeight() > 0); + assertEquals(Units.toLogical(peer.getWidth()), f.getWidth()); + assertEquals(Units.toLogical(peer.getHeight()), f.getHeight()); + assertEquals(f.getWidth(), f.getContentPane().getWidth()); + assertEquals(f.getHeight(), f.getContentPane().getHeight()); + } + + @Test + public void borderLayoutPlacesComponentsAndPeers() { + JFrame f = new JFrame(); + JButton south = new JButton("South"); + JPanel center = new JPanel(); + JLabel north = new JLabel("North"); + f.add(south, BorderLayout.SOUTH); + f.add(center, BorderLayout.CENTER); + f.getContentPane().add(north, BorderLayout.NORTH); + show(f); + int w = f.getContentPane().getWidth(); + int h = f.getContentPane().getHeight(); + Dimension sp = south.getPreferredSize(); + Dimension np = north.getPreferredSize(); + assertTrue(sp.height > 0 && np.height > 0); + assertEquals(0, north.getY()); + assertEquals(w, north.getWidth()); + assertEquals(np.height, north.getHeight()); + assertEquals(h - sp.height, south.getY()); + assertEquals(w, south.getWidth()); + assertEquals(np.height, center.getY()); + assertEquals(h - sp.height - np.height, center.getHeight()); + + com.codename1.ui.Component sPeer = south.cn1Peer(); + assertEquals(Units.toDevice(south.getY()), sPeer.getY()); + assertEquals(Units.toDeviceSize(south.getY(), south.getHeight()), sPeer.getHeight()); + assertEquals(Units.toDevice(w), sPeer.getWidth()); + assertSame(f.getContentPane().cn1Peer(), sPeer.getParent()); + } + + @Test + public void aWidgetsPreferredSizeIsItsPeersInLogicalPixels() { + JButton b = new JButton("Preferred"); + com.codename1.ui.Component peer = b.cn1Peer(); + Dimension d = b.getPreferredSize(); + assertEquals(Units.toLogicalCeil(peer.getPreferredW()), d.width); + assertEquals(Units.toLogicalCeil(peer.getPreferredH()), d.height); + b.setPreferredSize(new Dimension(120, 30)); + assertEquals(new Dimension(120, 30), b.getPreferredSize()); + } + + @Test + public void flowAndGridLayoutsWorkInLogicalPixels() { + JFrame f = new JFrame(); + JPanel flow = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 7)); + JPanel a = sized(40, 20); + JPanel b = sized(60, 30); + flow.add(a); + flow.add(b); + JPanel grid = new JPanel(new GridLayout(1, 2, 10, 0)); + JPanel g1 = new JPanel(); + JPanel g2 = new JPanel(); + grid.add(g1); + grid.add(g2); + grid.setPreferredSize(new Dimension(100, 50)); + f.add(flow, BorderLayout.CENTER); + f.add(grid, BorderLayout.SOUTH); + show(f); + assertEquals(5, a.getX()); + assertEquals(40, a.getWidth()); + assertEquals(50, b.getX()); + assertEquals(60, b.getWidth()); + assertEquals(7, b.getY()); + assertEquals(12, a.getY()); + assertEquals(50, grid.getHeight()); + int cell = (grid.getWidth() - 10) / 2; + assertEquals(cell, g1.getWidth()); + assertEquals(cell, g2.getWidth()); + assertEquals(g1.getX() + cell + 10, g2.getX()); + assertEquals(Units.toDevice(a.getX()), a.cn1Peer().getX()); + assertEquals(Units.toDevice(40), a.cn1Peer().getWidth()); + } + + @Test + public void peersAreStackedWithTheFirstChildOnTop() { + JPanel p = new JPanel(null); + JPanel first = new JPanel(); + JPanel second = new JPanel(); + p.add(first); + p.add(second); + ContainerPeer peer = (ContainerPeer) p.cn1Peer(); + assertSame(second.cn1Peer(), peer.getComponentAt(0)); + assertSame(first.cn1Peer(), peer.getComponentAt(1)); + JPanel third = new JPanel(); + p.add(third, 0); + assertSame(third.cn1Peer(), peer.getComponentAt(2)); + p.remove(first); + assertEquals(2, peer.getComponentCount()); + assertNotNull(first.cn1Peer()); + } + + @Test + public void revalidateLaysOutAComponentAddedLater() { + JFrame f = new JFrame(); + JPanel content = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + f.setContentPane(content); + show(f); + JPanel late = sized(30, 10); + content.add(late); + assertEquals(0, late.getWidth()); + content.revalidate(); + com.codename1.compat.testing.MainThreadRule.drain(); + f.cn1Form().revalidate(); + assertEquals(30, late.getWidth()); + assertEquals(Units.toDevice(30), late.cn1Peer().getWidth()); + } + + private static JPanel sized(int w, int h) { + JPanel p = new JPanel(); + p.setPreferredSize(new Dimension(w, h)); + return p; + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/KernelTestBase.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/KernelTestBase.java new file mode 100644 index 00000000000..80e11771633 --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/KernelTestBase.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat; + +import com.codename1.compat.testing.HeadlessImplementation; +import com.codename1.compat.testing.MainThreadRule; +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.javax.swing.JFrame; +import com.codename1.desktopcompat.rt.Units; +import com.codename1.ui.Image; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; + +/// What the tests that need a display share: the headless Codename One +/// implementation, the event dispatch thread, a scale of two device pixels +/// per logical pixel, and helpers that paint a frame and send it input. +public abstract class KernelTestBase { + + @Rule + public final MainThreadRule edt = new MainThreadRule(); + + private final List shown = new ArrayList(); + + @BeforeClass + public static void boot() { + HeadlessImplementation.install(); + } + + @Before + public void scale() { + Units.setScale(2f); + } + + @After + public void cleanUp() { + for (int i = shown.size() - 1; i >= 0; i--) { + shown.get(i).dispose(); + } + HeadlessImplementation.recordText = false; + HeadlessImplementation.drawnText.clear(); + Units.setScale(0); + } + + /// Shows the frame and remembers to dispose of it. + protected JFrame show(JFrame f) { + shown.add(f); + f.setVisible(true); + return f; + } + + /// Paints the frame's form and answers the strings drawn, each as + /// `{text, x, y}` in device pixels. + protected List paint(JFrame f) { + HeadlessImplementation.drawnText.clear(); + HeadlessImplementation.recordText = true; + try { + Image target = Image.createImage(HeadlessImplementation.WIDTH, HeadlessImplementation.HEIGHT); + f.cn1Form().paintComponent(target.getGraphics()); + return new ArrayList(HeadlessImplementation.drawnText); + } finally { + HeadlessImplementation.recordText = false; + HeadlessImplementation.drawnText.clear(); + } + } + + protected static Object[] find(List text, String s) { + for (int i = 0; i < text.size(); i++) { + if (s.equals(text.get(i)[0])) { + return text.get(i); + } + } + return null; + } + + /// The display position of a logical point of `c`. + protected static int[] onDisplay(Component c, int x, int y) { + com.codename1.ui.Component p = c.cn1Peer(); + return new int[]{p.getAbsoluteX() + Units.toDevice(x), p.getAbsoluteY() + Units.toDevice(y)}; + } + + protected static void press(JFrame f, Component c, int x, int y) { + int[] p = onDisplay(c, x, y); + f.cn1Form().pointerPressed(p[0], p[1]); + } + + protected static void drag(JFrame f, Component c, int x, int y) { + int[] p = onDisplay(c, x, y); + f.cn1Form().pointerDragged(p[0], p[1]); + } + + protected static void release(JFrame f, Component c, int x, int y) { + int[] p = onDisplay(c, x, y); + f.cn1Form().pointerReleased(p[0], p[1]); + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/LayoutParityTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/LayoutParityTest.java new file mode 100644 index 00000000000..932925949c4 --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/LayoutParityTest.java @@ -0,0 +1,955 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import org.junit.Test; + +import com.codename1.desktopcompat.java.awt.Component; +import com.codename1.desktopcompat.java.awt.Container; +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.Insets; +import com.codename1.desktopcompat.java.awt.LayoutManager; +import com.codename1.desktopcompat.java.awt.LayoutManager2; +import com.codename1.desktopcompat.javax.swing.JComponent; +import com.codename1.desktopcompat.javax.swing.border.EmptyBorder; + +import static org.junit.Assert.assertEquals; + +/// Lays the same component tree out twice -- once with the JDK's own layout +/// managers and once with this layer's -- and requires identical results. +/// +/// A tree is described once, as [Node]s, and built from both class +/// libraries. Leaves are bare lightweight components with fixed minimum, +/// preferred and maximum sizes, so nothing depends on fonts, a look and feel +/// or a display, and the JDK side runs headless. After the root is sized and +/// every container laid out top down, the two trees are written out as text +/// -- the bounds and visibility of every component, and what each +/// container's layout manager answers for its preferred, minimum and maximum +/// size -- and the two texts are compared. +/// +/// Besides the hand written cases, `GridBagLayout` and `BoxLayout` are run +/// over a few hundred trees drawn from a seeded random generator, since +/// their arithmetic has far more paths than a handful of examples reach. +public class LayoutParityTest { + + static { + System.setProperty("java.awt.headless", "true"); + } + + private static final int LEAF = 0; + private static final int PANEL = 1; + private static final int HGLUE = 2; + private static final int VGLUE = 3; + private static final int GLUE = 4; + private static final int HSTRUT = 5; + private static final int VSTRUT = 6; + private static final int RIGID = 7; + + private static final int FLOW = 0; + private static final int BORDER = 1; + private static final int GRID = 2; + private static final int CARD = 3; + private static final int BOX = 4; + private static final int GRIDBAG = 5; + private static final int OVERLAY = 6; + + /// One component of a tree, independent of the class library it is + /// built from. + private static final class Node { + final int kind; + int pw; + int ph; + int minw; + int minh; + int maxw = -1; + int maxh = -1; + float ax = 0.5f; + float ay = 0.5f; + boolean hidden; + int[] layout; + int[] border; + int[] colW; + int[] rowH; + double[] colWt; + double[] rowWt; + final List kids = new ArrayList(); + final List cons = new ArrayList(); + + Node(int kind) { + this.kind = kind; + } + + Node min(int w, int h) { + minw = w; + minh = h; + return this; + } + + Node max(int w, int h) { + maxw = w; + maxh = h; + return this; + } + + Node align(float x, float y) { + ax = x; + ay = y; + return this; + } + + Node hide() { + hidden = true; + return this; + } + + Node border(int top, int left, int bottom, int right) { + border = new int[] {top, left, bottom, right}; + return this; + } + + Node add(Node child) { + return add(child, null); + } + + Node add(Node child, Object constraint) { + kids.add(child); + cons.add(constraint); + return this; + } + } + + /// Grid bag constraints, independent of the class library. + private static final class G { + int gridx = -1; + int gridy = -1; + int gw = 1; + int gh = 1; + double wx; + double wy; + int anchor = 10; + int fill; + int top; + int left; + int bottom; + int right; + int ipadx; + int ipady; + + G at(int x, int y) { + gridx = x; + gridy = y; + return this; + } + + G span(int w, int h) { + gw = w; + gh = h; + return this; + } + + G weight(double x, double y) { + wx = x; + wy = y; + return this; + } + + G anchor(int a) { + anchor = a; + return this; + } + + G fill(int f) { + fill = f; + return this; + } + + G insets(int t, int l, int b, int r) { + top = t; + left = l; + bottom = b; + right = r; + return this; + } + + G ipad(int x, int y) { + ipadx = x; + ipady = y; + return this; + } + } + + private static Node leaf(int w, int h) { + Node n = new Node(LEAF); + n.pw = w; + n.ph = h; + n.minw = w; + n.minh = h; + return n; + } + + private static Node filler(int kind, int w, int h) { + Node n = new Node(kind); + n.pw = w; + n.ph = h; + return n; + } + + private static Node panel(int... layout) { + Node n = new Node(PANEL); + n.layout = layout; + return n; + } + + private static G g() { + return new G(); + } + + // ------------------------------------------------------------------ + // The JDK side + + private static final class RealLeaf extends java.awt.Component { + private static final long serialVersionUID = 1L; + private final float ax; + private final float ay; + + RealLeaf(float ax, float ay) { + this.ax = ax; + this.ay = ay; + } + + @Override + public float getAlignmentX() { + return ax; + } + + @Override + public float getAlignmentY() { + return ay; + } + } + + private static java.awt.Component real(Node n) { + switch (n.kind) { + case LEAF: { + java.awt.Component c = new RealLeaf(n.ax, n.ay); + c.setPreferredSize(new java.awt.Dimension(n.pw, n.ph)); + c.setMinimumSize(new java.awt.Dimension(n.minw, n.minh)); + if (n.maxw >= 0) { + c.setMaximumSize(new java.awt.Dimension(n.maxw, n.maxh)); + } + c.setVisible(!n.hidden); + return c; + } + case HGLUE: + return javax.swing.Box.createHorizontalGlue(); + case VGLUE: + return javax.swing.Box.createVerticalGlue(); + case GLUE: + return javax.swing.Box.createGlue(); + case HSTRUT: + return javax.swing.Box.createHorizontalStrut(n.pw); + case VSTRUT: + return javax.swing.Box.createVerticalStrut(n.ph); + case RIGID: + return javax.swing.Box.createRigidArea(new java.awt.Dimension(n.pw, n.ph)); + default: + break; + } + java.awt.Container c; + if (n.border != null) { + javax.swing.JComponent jc = new javax.swing.JComponent() { + private static final long serialVersionUID = 1L; + }; + jc.setBorder(javax.swing.BorderFactory.createEmptyBorder(n.border[0], n.border[1], n.border[2], + n.border[3])); + c = jc; + } else { + c = new java.awt.Container(); + } + int[] l = n.layout; + switch (l[0]) { + case FLOW: + c.setLayout(new java.awt.FlowLayout(l[1], l[2], l[3])); + break; + case BORDER: + c.setLayout(new java.awt.BorderLayout(l[1], l[2])); + break; + case GRID: + c.setLayout(new java.awt.GridLayout(l[1], l[2], l[3], l[4])); + break; + case CARD: + c.setLayout(new java.awt.CardLayout(l[1], l[2])); + break; + case BOX: + c.setLayout(new javax.swing.BoxLayout(c, l[1])); + break; + case OVERLAY: + c.setLayout(new javax.swing.OverlayLayout(c)); + break; + default: { + java.awt.GridBagLayout gb = new java.awt.GridBagLayout(); + gb.columnWidths = n.colW; + gb.rowHeights = n.rowH; + gb.columnWeights = n.colWt; + gb.rowWeights = n.rowWt; + c.setLayout(gb); + break; + } + } + for (int i = 0; i < n.kids.size(); i++) { + Object con = n.cons.get(i); + if (con instanceof G) { + G s = (G) con; + con = new java.awt.GridBagConstraints(s.gridx, s.gridy, s.gw, s.gh, s.wx, s.wy, s.anchor, s.fill, + new java.awt.Insets(s.top, s.left, s.bottom, s.right), s.ipadx, s.ipady); + } + java.awt.Component kid = real(n.kids.get(i)); + if (con == null) { + c.add(kid); + } else { + c.add(kid, con); + } + } + if (n.hidden) { + c.setVisible(false); + } + return c; + } + + private static void layout(java.awt.Component c) { + if (c instanceof java.awt.Container && !(c instanceof javax.swing.Box.Filler)) { + java.awt.Container k = (java.awt.Container) c; + k.invalidate(); + k.doLayout(); + for (int i = 0; i < k.getComponentCount(); i++) { + layout(k.getComponent(i)); + } + } + } + + private static void dump(java.awt.Component c, String path, StringBuilder sb) { + sb.append(path).append(' ').append(c.getX()).append(',').append(c.getY()).append(' ').append(c.getWidth()) + .append('x').append(c.getHeight()).append(c.isVisible() ? "" : " hidden"); + if (c instanceof java.awt.Container && !(c instanceof javax.swing.Box.Filler)) { + java.awt.Container k = (java.awt.Container) c; + java.awt.LayoutManager lm = k.getLayout(); + if (lm != null) { + java.awt.Dimension p = lm.preferredLayoutSize(k); + java.awt.Dimension m = lm.minimumLayoutSize(k); + sb.append(" pref=").append(p.width).append('x').append(p.height); + sb.append(" min=").append(m.width).append('x').append(m.height); + if (lm instanceof java.awt.LayoutManager2) { + java.awt.Dimension x = ((java.awt.LayoutManager2) lm).maximumLayoutSize(k); + sb.append(" max=").append(x.width).append('x').append(x.height); + } + } + java.awt.Insets in = k.getInsets(); + sb.append(" in=").append(in.top).append(',').append(in.left).append(',').append(in.bottom).append(',') + .append(in.right).append('\n'); + for (int i = 0; i < k.getComponentCount(); i++) { + dump(k.getComponent(i), path + "/" + i, sb); + } + } else { + sb.append('\n'); + } + } + + // ------------------------------------------------------------------ + // This layer's side + + private static final class OurLeaf extends Component { + private final float ax; + private final float ay; + + OurLeaf(float ax, float ay) { + this.ax = ax; + this.ay = ay; + } + + @Override + public float getAlignmentX() { + return ax; + } + + @Override + public float getAlignmentY() { + return ay; + } + } + + private static Component ours(Node n) { + switch (n.kind) { + case LEAF: { + Component c = new OurLeaf(n.ax, n.ay); + c.setPreferredSize(new Dimension(n.pw, n.ph)); + c.setMinimumSize(new Dimension(n.minw, n.minh)); + if (n.maxw >= 0) { + c.setMaximumSize(new Dimension(n.maxw, n.maxh)); + } + c.setVisible(!n.hidden); + return c; + } + case HGLUE: + return com.codename1.desktopcompat.javax.swing.Box.createHorizontalGlue(); + case VGLUE: + return com.codename1.desktopcompat.javax.swing.Box.createVerticalGlue(); + case GLUE: + return com.codename1.desktopcompat.javax.swing.Box.createGlue(); + case HSTRUT: + return com.codename1.desktopcompat.javax.swing.Box.createHorizontalStrut(n.pw); + case VSTRUT: + return com.codename1.desktopcompat.javax.swing.Box.createVerticalStrut(n.ph); + case RIGID: + return com.codename1.desktopcompat.javax.swing.Box.createRigidArea(new Dimension(n.pw, n.ph)); + default: + break; + } + Container c; + if (n.border != null) { + JComponent jc = new JComponent() { + }; + jc.setBorder(new EmptyBorder(n.border[0], n.border[1], n.border[2], n.border[3])); + c = jc; + } else { + c = new Container(); + } + int[] l = n.layout; + switch (l[0]) { + case FLOW: + c.setLayout(new com.codename1.desktopcompat.java.awt.FlowLayout(l[1], l[2], l[3])); + break; + case BORDER: + c.setLayout(new com.codename1.desktopcompat.java.awt.BorderLayout(l[1], l[2])); + break; + case GRID: + c.setLayout(new com.codename1.desktopcompat.java.awt.GridLayout(l[1], l[2], l[3], l[4])); + break; + case CARD: + c.setLayout(new com.codename1.desktopcompat.java.awt.CardLayout(l[1], l[2])); + break; + case BOX: + c.setLayout(new com.codename1.desktopcompat.javax.swing.BoxLayout(c, l[1])); + break; + case OVERLAY: + c.setLayout(new com.codename1.desktopcompat.javax.swing.OverlayLayout(c)); + break; + default: { + com.codename1.desktopcompat.java.awt.GridBagLayout gb = + new com.codename1.desktopcompat.java.awt.GridBagLayout(); + gb.columnWidths = n.colW; + gb.rowHeights = n.rowH; + gb.columnWeights = n.colWt; + gb.rowWeights = n.rowWt; + c.setLayout(gb); + break; + } + } + for (int i = 0; i < n.kids.size(); i++) { + Object con = n.cons.get(i); + if (con instanceof G) { + G s = (G) con; + con = new com.codename1.desktopcompat.java.awt.GridBagConstraints(s.gridx, s.gridy, s.gw, s.gh, s.wx, + s.wy, s.anchor, s.fill, new Insets(s.top, s.left, s.bottom, s.right), s.ipadx, s.ipady); + } + Component kid = ours(n.kids.get(i)); + if (con == null) { + c.add(kid); + } else { + c.add(kid, con); + } + } + if (n.hidden) { + c.setVisible(false); + } + return c; + } + + private static void layout(Component c) { + if (c instanceof Container && !(c instanceof com.codename1.desktopcompat.javax.swing.Box.Filler)) { + Container k = (Container) c; + k.invalidate(); + k.doLayout(); + for (int i = 0; i < k.getComponentCount(); i++) { + layout(k.getComponent(i)); + } + } + } + + private static void dump(Component c, String path, StringBuilder sb) { + sb.append(path).append(' ').append(c.getX()).append(',').append(c.getY()).append(' ').append(c.getWidth()) + .append('x').append(c.getHeight()).append(c.isVisible() ? "" : " hidden"); + if (c instanceof Container && !(c instanceof com.codename1.desktopcompat.javax.swing.Box.Filler)) { + Container k = (Container) c; + LayoutManager lm = k.getLayout(); + if (lm != null) { + Dimension p = lm.preferredLayoutSize(k); + Dimension m = lm.minimumLayoutSize(k); + sb.append(" pref=").append(p.width).append('x').append(p.height); + sb.append(" min=").append(m.width).append('x').append(m.height); + if (lm instanceof LayoutManager2) { + Dimension x = ((LayoutManager2) lm).maximumLayoutSize(k); + sb.append(" max=").append(x.width).append('x').append(x.height); + } + } + Insets in = k.getInsets(); + sb.append(" in=").append(in.top).append(',').append(in.left).append(',').append(in.bottom).append(',') + .append(in.right).append('\n'); + for (int i = 0; i < k.getComponentCount(); i++) { + dump(k.getComponent(i), path + "/" + i, sb); + } + } else { + sb.append('\n'); + } + } + + // ------------------------------------------------------------------ + + private static String dump(java.awt.Component c) { + StringBuilder sb = new StringBuilder(); + dump(c, "", sb); + return sb.toString(); + } + + private static String dump(Component c) { + StringBuilder sb = new StringBuilder(); + dump(c, "", sb); + return sb.toString(); + } + + /// Builds the tree from both libraries, lays it out at each of the + /// sizes given as width and height pairs, and compares. + private static void check(String what, Node tree, int... sizes) { + java.awt.Component r = real(tree); + Component o = ours(tree); + for (int i = 0; i < sizes.length; i += 2) { + r.setSize(sizes[i], sizes[i + 1]); + o.setSize(sizes[i], sizes[i + 1]); + layout(r); + layout(o); + String expected = dump(r); + String actual = dump(o); + if (!expected.equals(actual)) { + assertEquals(what + " at " + sizes[i] + "x" + sizes[i + 1] + "\n" + describe(tree, ""), expected, + actual); + } + } + } + + /// The tree as text, for the message of a failed comparison. + private static String describe(Node n, String indent) { + StringBuilder sb = new StringBuilder(indent); + sb.append("kind=").append(n.kind).append(" pref=").append(n.pw).append('x').append(n.ph).append(" min=") + .append(n.minw).append('x').append(n.minh).append(" max=").append(n.maxw).append('x').append(n.maxh) + .append(" align=").append(n.ax).append(',').append(n.ay).append(n.hidden ? " hidden" : ""); + if (n.layout != null) { + sb.append(" layout=").append(java.util.Arrays.toString(n.layout)); + } + if (n.border != null) { + sb.append(" border=").append(java.util.Arrays.toString(n.border)); + } + if (n.colW != null || n.rowH != null || n.colWt != null || n.rowWt != null) { + sb.append(" overrides=").append(java.util.Arrays.toString(n.colW)).append(java.util.Arrays.toString(n.rowH)) + .append(java.util.Arrays.toString(n.colWt)).append(java.util.Arrays.toString(n.rowWt)); + } + sb.append('\n'); + for (int i = 0; i < n.kids.size(); i++) { + Object con = n.cons.get(i); + if (con instanceof G) { + G s = (G) con; + sb.append(indent).append(" at ").append(s.gridx).append(',').append(s.gridy).append(" span ") + .append(s.gw).append(',').append(s.gh).append(" weight ").append(s.wx).append(',') + .append(s.wy).append(" anchor ").append(s.anchor).append(" fill ").append(s.fill) + .append(" insets ").append(s.top).append(',').append(s.left).append(',').append(s.bottom) + .append(',').append(s.right).append(" ipad ").append(s.ipadx).append(',').append(s.ipady) + .append('\n'); + } else if (con != null) { + sb.append(indent).append(" at ").append(con).append('\n'); + } + sb.append(describe(n.kids.get(i), indent + " ")); + } + return sb.toString(); + } + + private static Node sixLeaves(Node p) { + p.add(leaf(40, 20)).add(leaf(70, 35)).add(leaf(25, 10).hide()).add(leaf(55, 28)).add(leaf(90, 14)) + .add(leaf(33, 41).min(10, 12)); + return p; + } + + @Test + public void flowLayout() { + int[][] gaps = {{5, 5}, {7, 3}, {0, 0}, {-2, 4}}; + for (int align = 0; align <= 4; align++) { + for (int[] gap : gaps) { + check("flow align " + align + " gap " + gap[0], sixLeaves(panel(FLOW, align, gap[0], gap[1])), 400, + 200, 331, 90, 150, 300, 121, 77, 60, 60, 0, 0); + check("flow with insets, align " + align, + sixLeaves(panel(FLOW, align, gap[0], gap[1]).border(3, 11, 6, 2)), 400, 200, 175, 80, 20, 20); + } + } + check("empty flow", panel(FLOW, 1, 5, 5), 100, 100); + check("all hidden", panel(FLOW, 0, 5, 5).add(leaf(10, 10).hide()).add(leaf(20, 20).hide()), 100, 100); + } + + @Test + public void borderLayout() { + String[] names = {"North", "South", "East", "West", "Center"}; + int[][] sizes = {{60, 20}, {80, 25}, {30, 50}, {35, 45}, {100, 70}}; + int[][] gaps = {{0, 0}, {4, 6}}; + for (int[] gap : gaps) { + for (int mask = 0; mask < 32; mask++) { + Node p = panel(BORDER, gap[0], gap[1]); + Node q = panel(BORDER, gap[0], gap[1]).border(5, 7, 9, 11); + for (int i = 0; i < 5; i++) { + if ((mask & (1 << i)) != 0) { + p.add(leaf(sizes[i][0], sizes[i][1]).min(sizes[i][0] / 2, sizes[i][1] / 3), names[i]); + q.add(leaf(sizes[i][0], sizes[i][1]).min(sizes[i][0] / 2, sizes[i][1] / 3), names[i]); + } + } + check("border regions " + mask, p, 300, 200, 50, 30, 301, 77); + check("border regions with insets " + mask, q, 300, 200, 50, 30); + } + } + check("relative names", + panel(BORDER, 3, 2).add(leaf(60, 20), "First").add(leaf(80, 25), "Last").add(leaf(30, 50), "Before") + .add(leaf(35, 45), "After").add(leaf(10, 10), null), + 300, 200); + check("relative names win over absolute ones", + panel(BORDER, 3, 2).add(leaf(60, 20), "North").add(leaf(61, 31), "First").add(leaf(30, 50), "West") + .add(leaf(37, 50), "Before").add(leaf(10, 10), "Center"), + 300, 200); + check("hidden edge", + panel(BORDER, 3, 2).add(leaf(60, 20).hide(), "North").add(leaf(80, 25), "South") + .add(leaf(30, 50).hide(), "East").add(leaf(10, 10), "Center"), + 300, 200); + } + + @Test + public void gridLayout() { + int[][] shapes = {{2, 0}, {0, 3}, {3, 2}, {1, 0}, {0, 1}, {4, 7}}; + int[][] gaps = {{0, 0}, {5, 3}, {-1, 2}}; + int[] counts = {0, 1, 5, 7, 12}; + for (int[] shape : shapes) { + for (int[] gap : gaps) { + for (int count : counts) { + Node p = panel(GRID, shape[0], shape[1], gap[0], gap[1]); + Node q = panel(GRID, shape[0], shape[1], gap[0], gap[1]).border(4, 1, 2, 9); + for (int i = 0; i < count; i++) { + Node a = leaf(20 + i * 7, 15 + (i * 5) % 13).min(3 + i, 9 - (i % 4)); + Node b = leaf(20 + i * 7, 15 + (i * 5) % 13).min(3 + i, 9 - (i % 4)); + if (i == 2) { + a.hide(); + b.hide(); + } + p.add(a); + q.add(b); + } + String what = "grid " + shape[0] + "x" + shape[1] + " gap " + gap[0] + " count " + count; + check(what, p, 301, 203, 100, 100, 17, 5); + check(what + " with insets", q, 301, 203, 64, 31); + } + } + } + } + + @Test + public void cardLayout() { + Node tree = panel(CARD, 4, 6).border(1, 2, 3, 4).add(leaf(50, 40), "a").add(leaf(90, 20).min(5, 6), "b") + .add(leaf(30, 80), "c").add(leaf(10, 10), "d"); + java.awt.Container r = (java.awt.Container) real(tree); + Container o = (Container) ours(tree); + java.awt.CardLayout rl = (java.awt.CardLayout) r.getLayout(); + com.codename1.desktopcompat.java.awt.CardLayout ol = + (com.codename1.desktopcompat.java.awt.CardLayout) o.getLayout(); + r.setSize(200, 150); + o.setSize(200, 150); + for (int step = 0; step < 12; step++) { + switch (step) { + case 1: + case 2: + case 9: + rl.next(r); + ol.next(o); + break; + case 3: + rl.show(r, "a"); + ol.show(o, "a"); + break; + case 4: + case 10: + rl.previous(r); + ol.previous(o); + break; + case 5: + rl.first(r); + ol.first(o); + break; + case 6: + rl.show(r, "c"); + ol.show(o, "c"); + break; + case 7: + rl.show(r, "nothing by this name"); + ol.show(o, "nothing by this name"); + break; + case 8: + rl.last(r); + ol.last(o); + break; + case 11: + r.remove(r.getComponent(0)); + o.remove(o.getComponent(0)); + break; + default: + break; + } + layout(r); + layout(o); + assertEquals("card step " + step, dump(r), dump(o)); + } + check("empty card", panel(CARD, 0, 0), 100, 100); + check("all cards hidden before layout", panel(CARD, 0, 0).add(leaf(5, 5).hide(), "x").add(leaf(6, 6), "y"), + 100, 100); + } + + private static Node boxKids(Node p) { + p.add(leaf(40, 20).max(60, 30).align(0f, 0f)); + p.add(filler(HSTRUT, 7, 0)); + p.add(leaf(70, 35).min(20, 10).max(300, 400).align(1f, 1f)); + p.add(filler(GLUE, 0, 0)); + p.add(leaf(25, 10).hide().align(0.25f, 0.75f)); + p.add(filler(VSTRUT, 0, 9)); + p.add(leaf(55, 28).min(11, 7).align(0.3f, 0.8f)); + p.add(filler(RIGID, 13, 17)); + p.add(leaf(33, 41).min(33, 41).max(33, 41)); + return p; + } + + @Test + public void boxLayout() { + for (int axis = 0; axis <= 3; axis++) { + check("box axis " + axis, boxKids(panel(BOX, axis)), 600, 300, 300, 600, 243, 200, 200, 243, 120, 90, 90, + 120, 30, 30, 0, 0, 5000, 4000); + check("box with insets, axis " + axis, boxKids(panel(BOX, axis).border(6, 4, 2, 8)), 600, 300, 150, 150); + } + check("glue between", + panel(BOX, 0).add(leaf(30, 30)).add(filler(HGLUE, 0, 0)).add(leaf(40, 20)).add(filler(HGLUE, 0, 0)) + .add(leaf(10, 50).max(10, 50)), + 401, 100, 80, 20); + check("vertical glue", + panel(BOX, 1).add(leaf(30, 30).align(0f, 0.5f)).add(filler(VGLUE, 0, 0)) + .add(leaf(40, 20).max(40, 20).align(0f, 0.5f)).add(filler(VSTRUT, 0, 12)) + .add(leaf(10, 50).max(1000, 50).align(0f, 0.5f)), + 200, 400, 20, 80); + check("nested boxes", + panel(BOX, 0).border(2, 3, 4, 5) + .add(panel(BOX, 1).add(leaf(30, 30).max(90, 40)).add(filler(VGLUE, 0, 0)).add(leaf(50, 20))) + .add(filler(HSTRUT, 10, 0)) + .add(panel(BOX, 1).border(1, 1, 1, 1).add(leaf(60, 15).align(1f, 0f)) + .add(leaf(20, 15).align(0f, 0f).max(25, 15))) + .add(leaf(44, 44).align(0.5f, 0.2f)), + 500, 220, 140, 60); + check("empty box", panel(BOX, 1), 50, 50); + check("overlay", boxKids(panel(OVERLAY)), 300, 200, 40, 40); + check("overlay with insets", boxKids(panel(OVERLAY).border(3, 5, 7, 9)), 300, 200); + } + + @Test + public void boxLayoutRandom() { + Random rnd = new Random(20240607L); + float[] aligns = {0f, 0.5f, 1f, 0.25f, 0.7f}; + for (int round = 0; round < 300; round++) { + Node p = panel(BOX, rnd.nextInt(4)); + if (rnd.nextInt(3) == 0) { + p.border(rnd.nextInt(6), rnd.nextInt(6), rnd.nextInt(6), rnd.nextInt(6)); + } + int count = rnd.nextInt(7); + for (int i = 0; i < count; i++) { + int kind = rnd.nextInt(10); + if (kind == 0) { + p.add(filler(GLUE, 0, 0)); + } else if (kind == 1) { + p.add(filler(HSTRUT, rnd.nextInt(30), 0)); + } else if (kind == 2) { + p.add(filler(VSTRUT, 0, rnd.nextInt(30))); + } else { + int w = 5 + rnd.nextInt(90); + int h = 5 + rnd.nextInt(90); + Node n = leaf(w, h).min(rnd.nextInt(w + 1), rnd.nextInt(h + 1)).align( + aligns[rnd.nextInt(aligns.length)], aligns[rnd.nextInt(aligns.length)]); + if (rnd.nextInt(3) != 0) { + n.max(w + rnd.nextInt(200), h + rnd.nextInt(200)); + } + if (rnd.nextInt(9) == 0) { + n.hide(); + } + p.add(n); + } + } + check("random box " + round, p, rnd.nextInt(700), rnd.nextInt(500), rnd.nextInt(200), rnd.nextInt(200)); + } + } + + @Test + public void gridBagLayout() { + // 1: weights and fill + check("weights and fill", + panel(GRIDBAG).add(leaf(50, 20), g().at(0, 0).weight(1, 0).fill(2)) + .add(leaf(30, 20), g().at(1, 0).weight(2, 0).fill(1)) + .add(leaf(40, 30), g().at(0, 1).weight(0, 1).fill(3)) + .add(leaf(40, 30), g().at(1, 1).weight(0, 3)), + 400, 300, 401, 299, 100, 40, 60, 30); + // 2: every anchor, in a grid of cells larger than the components + Node anchors = panel(GRIDBAG); + int cell = 0; + for (int a = 10; a <= 26; a++) { + anchors.add(leaf(20 + a, 10 + a / 2), g().at(cell % 5, cell / 5).weight(1, 1).anchor(a)); + cell++; + } + check("anchors", anchors, 640, 480, 333, 257); + // 3: spans + check("spans", + panel(GRIDBAG).add(leaf(120, 20), g().at(0, 0).span(3, 1).fill(2)) + .add(leaf(30, 90), g().at(0, 1).span(1, 2).fill(3).weight(0, 1)) + .add(leaf(40, 25), g().at(1, 1).weight(1, 0)).add(leaf(45, 25), g().at(2, 1).weight(3, 0)) + .add(leaf(200, 31), g().at(1, 2).span(2, 1).weight(0, 2).anchor(14)), + 500, 400, 230, 150, 90, 60); + // 4: REMAINDER and RELATIVE + check("remainder and relative", + panel(GRIDBAG).add(leaf(30, 20), g()).add(leaf(31, 21), g()).add(leaf(32, 22), g().span(0, 1)) + .add(leaf(90, 23), g().span(-1, 1).fill(2).weight(1, 0)).add(leaf(34, 24), g().span(0, 1)) + .add(leaf(35, 25), g().span(0, 1).weight(0, 1).fill(1)).add(leaf(36, 26), g().span(1, 0)) + .add(leaf(37, 27), g().span(0, 0).anchor(18)), + 400, 300, 150, 120); + // 5: insets and internal padding + check("insets and ipad", + panel(GRIDBAG).border(3, 4, 5, 6) + .add(leaf(50, 20), g().at(0, 0).insets(1, 2, 3, 4).ipad(10, 6)) + .add(leaf(30, 20), g().at(1, 0).insets(7, 0, 0, 9).weight(1, 1).fill(1).ipad(3, 0)) + .add(leaf(40, 30), g().at(0, 1).span(2, 1).insets(5, 5, 5, 5).fill(2)) + .add(leaf(20, 20).hide(), g().at(5, 5).weight(9, 9)), + 300, 200, 301, 201, 110, 70); + // 6: shrinking below the preferred size falls back to minimum sizes + check("minimum sizes", + panel(GRIDBAG).add(leaf(100, 40).min(20, 10), g().at(0, 0).weight(1, 1).fill(1)) + .add(leaf(80, 40).min(30, 15), g().at(1, 0).weight(2, 0)) + .add(leaf(150, 30).min(40, 5), g().at(0, 1).span(2, 1).weight(0, 1).fill(2)), + 400, 200, 179, 60, 60, 20, 30, 10); + // 7: the public override arrays + Node forced = panel(GRIDBAG).add(leaf(20, 20), g().at(0, 0)).add(leaf(30, 30), g().at(1, 1).fill(1)) + .add(leaf(10, 10), g().at(3, 0).anchor(12)); + forced.colW = new int[] {50, 10, 40, 0, 25}; + forced.rowH = new int[] {5, 60}; + forced.colWt = new double[] {0, 1, 0, 0.5}; + forced.rowWt = new double[] {1, 0, 4}; + check("override arrays", forced, 400, 300, 100, 50); + // 8: nothing has weight, so the grid is centred + check("centred", panel(GRIDBAG).border(1, 9, 3, 2).add(leaf(50, 20), g().at(0, 0)) + .add(leaf(30, 60), g().at(1, 1)).add(leaf(10, 10), g().at(4, 0).span(1, 2).fill(3)), 301, 201, 60, + 40); + // 9: relative position mixed with absolute + check("mixed positions", + panel(GRIDBAG).add(leaf(20, 20), g().at(2, -1)).add(leaf(21, 21), g().at(2, -1)) + .add(leaf(22, 22), g().at(-1, 1)).add(leaf(23, 23), g().at(-1, 0).span(2, 1)) + .add(leaf(24, 24), g().at(0, 3).span(0, 1).fill(2)).add(leaf(25, 25), g()), + 300, 300); + check("empty grid bag", panel(GRIDBAG), 100, 100); + } + + @Test + public void gridBagLayoutRandom() { + Random rnd = new Random(99173L); + for (int round = 0; round < 400; round++) { + Node p = panel(GRIDBAG); + if (rnd.nextInt(3) == 0) { + p.border(rnd.nextInt(8), rnd.nextInt(8), rnd.nextInt(8), rnd.nextInt(8)); + } + if (rnd.nextInt(8) == 0) { + p.colW = new int[] {rnd.nextInt(40), rnd.nextInt(40), rnd.nextInt(40)}; + p.rowWt = new double[] {rnd.nextInt(3), rnd.nextInt(3)}; + } + int count = 1 + rnd.nextInt(8); + boolean relative = rnd.nextInt(4) == 0; + for (int i = 0; i < count; i++) { + int w = 5 + rnd.nextInt(90); + int h = 5 + rnd.nextInt(60); + Node n = leaf(w, h).min(rnd.nextInt(w + 1), rnd.nextInt(h + 1)); + if (rnd.nextInt(10) == 0) { + n.hide(); + } + G c = g(); + if (relative) { + c.at(rnd.nextInt(3) == 0 ? rnd.nextInt(4) : -1, rnd.nextInt(5) == 0 ? rnd.nextInt(4) : -1); + c.span(rnd.nextInt(4) == 0 ? rnd.nextInt(3) - 1 : 1, rnd.nextInt(9) == 0 ? rnd.nextInt(3) - 1 : 1); + } else { + c.at(rnd.nextInt(5), rnd.nextInt(5)); + c.span(1 + rnd.nextInt(3), 1 + rnd.nextInt(3)); + } + if (rnd.nextInt(2) == 0) { + c.weight(rnd.nextInt(4) * 0.5, rnd.nextInt(4) * 0.25); + } + c.anchor(10 + rnd.nextInt(17)).fill(rnd.nextInt(4)); + if (rnd.nextInt(3) == 0) { + c.insets(rnd.nextInt(6), rnd.nextInt(6), rnd.nextInt(6), rnd.nextInt(6)); + } + if (rnd.nextInt(4) == 0) { + c.ipad(rnd.nextInt(9), rnd.nextInt(9)); + } + p.add(n, c); + } + check("random grid bag " + round, p, 200 + rnd.nextInt(500), 150 + rnd.nextInt(400), rnd.nextInt(250), + rnd.nextInt(200)); + } + } + + @Test + public void nestedContainersWithBorders() { + Node form = panel(GRIDBAG).border(8, 8, 8, 8); + for (int row = 0; row < 4; row++) { + form.add(leaf(60 + row * 5, 16), g().at(0, row).anchor(13).insets(2, 2, 2, 6)); + form.add(leaf(120, 22).min(40, 22), g().at(1, row).weight(1, 0).fill(2).insets(2, 0, 2, 2)); + } + form.add(leaf(10, 10), g().at(0, 4).span(2, 1).weight(0, 1).fill(1)); + + Node buttons = panel(FLOW, 2, 6, 4).border(2, 0, 2, 0).add(leaf(70, 26)).add(leaf(70, 26)).add(leaf(90, 26)); + Node side = panel(BOX, 1).border(4, 4, 4, 4).add(leaf(80, 24).max(200, 24).align(0f, 0.5f)) + .add(filler(VSTRUT, 0, 5)).add(leaf(60, 24).max(200, 24).align(0f, 0.5f)).add(filler(VGLUE, 0, 0)) + .add(leaf(80, 40).align(0f, 0.5f)); + Node tiles = panel(GRID, 2, 3, 4, 4).border(1, 2, 3, 4); + for (int i = 0; i < 6; i++) { + tiles.add(panel(BORDER, 1, 1).border(1, 1, 1, 1).add(leaf(30, 10), "North").add(leaf(20 + i, 20), null)); + } + Node centre = panel(BORDER, 5, 5).add(form, "Center").add(tiles, "South"); + Node root = panel(BORDER, 6, 3).border(10, 12, 14, 16).add(leaf(200, 30), "North").add(side, "West") + .add(centre, "Center").add(buttons, "South"); + check("nested", root, 800, 600, 640, 480, 320, 240, 100, 100); + + Node cards = panel(CARD, 2, 2).border(5, 5, 5, 5).add(panel(FLOW, 1, 5, 5).add(leaf(40, 40)), "one") + .add(panel(GRID, 0, 2, 1, 1).add(leaf(10, 10)).add(leaf(20, 20)).add(leaf(30, 30)), "two"); + check("cards of panels", cards, 300, 200); + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/NoJdkDesktopReferenceTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/NoJdkDesktopReferenceTest.java new file mode 100644 index 00000000000..39e9afd3350 --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/NoJdkDesktopReferenceTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/// No class the module ships may name the JDK's own desktop packages. +/// +/// A device has no `java.awt`: a main source that imports the real +/// `java.awt.Color` where the layer's was meant compiles on the build +/// machine, where the JDK supplies it, and fails only when the application +/// is translated. Every UTF-8 constant of every class is read -- class +/// names, descriptors and generic signatures all live there -- and none may +/// contain one of the relocated package paths except as part of the layer's +/// own `com/codename1/desktopcompat/` name. +public class NoJdkDesktopReferenceTest { + + private static final String OWN = "com/codename1/desktopcompat/"; + private static final String[] FORBIDDEN = { + "java/awt/", "javax/swing/", "java/beans/", "javax/accessibility/", "org/jdesktop/" + }; + + @Test + public void noClassNamesTheJdkDesktopPackages() throws IOException { + File root = new File(System.getProperty("swingcompat.classes", "target/classes")); + List problems = new ArrayList(); + int count = walk(root, problems); + if (!problems.isEmpty()) { + StringBuilder sb = new StringBuilder("references to the JDK's desktop packages:\n"); + for (String p : problems) { + sb.append(" ").append(p).append('\n'); + } + fail(sb.toString()); + } + assertTrue("no classes under " + root, count > 0); + } + + private static int walk(File dir, List problems) throws IOException { + File[] files = dir.listFiles(); + int count = 0; + if (files == null) { + return 0; + } + for (File f : files) { + if (f.isDirectory()) { + count += walk(f, problems); + } else if (f.getName().endsWith(".class")) { + count++; + scan(f, problems); + } + } + return count; + } + + static boolean offends(String constant) { + for (String pkg : FORBIDDEN) { + int at = constant.indexOf(pkg); + while (at >= 0) { + boolean own = at >= OWN.length() && constant.startsWith(OWN, at - OWN.length()); + // An 'L' in front is the type marker of a descriptor unless + // it ends a lower case word ("xmljava/awt/"). + boolean descriptor = at > 0 && constant.charAt(at - 1) == 'L' + && (at < 2 || !Character.isLowerCase(constant.charAt(at - 2))); + boolean partOfLongerName = at > 0 && !descriptor + && (Character.isJavaIdentifierPart(constant.charAt(at - 1)) || constant.charAt(at - 1) == '/'); + if (!own && !partOfLongerName) { + return true; + } + at = constant.indexOf(pkg, at + 1); + } + } + return false; + } + + private static void scan(File f, List problems) throws IOException { + DataInputStream in = new DataInputStream(new FileInputStream(f)); + try { + in.readInt(); + in.readUnsignedShort(); + in.readUnsignedShort(); + int n = in.readUnsignedShort(); + for (int i = 1; i < n; i++) { + int tag = in.readUnsignedByte(); + switch (tag) { + case 1: + String s = in.readUTF(); + if (offends(s)) { + problems.add(f.getPath() + ": " + s); + } + break; + case 3: case 4: case 9: case 10: case 11: case 12: case 17: case 18: + in.readInt(); + break; + case 5: case 6: + in.readLong(); + i++; + break; + case 7: case 8: case 16: case 19: case 20: + in.readUnsignedShort(); + break; + case 15: + in.readUnsignedByte(); + in.readUnsignedShort(); + break; + default: + throw new IOException(f + ": constant pool tag " + tag); + } + } + } finally { + in.close(); + } + } + + @Test + public void theScanTellsTheLayerFromTheJdk() { + assertTrue(offends("Ljava/awt/Color;")); + assertTrue(offends("javax/swing/JButton")); + assertFalse(offends("Lcom/codename1/desktopcompat/java/awt/Color;")); + assertTrue(offends("(Lcom/codename1/desktopcompat/java/awt/Color;Ljava/awt/Font;)V")); + assertFalse(offends("java/lang/String")); + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/PaintRoutingTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/PaintRoutingTest.java new file mode 100644 index 00000000000..27da0437167 --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/PaintRoutingTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.codename1.desktopcompat.java.awt.BorderLayout; +import com.codename1.desktopcompat.java.awt.Canvas; +import com.codename1.desktopcompat.java.awt.Color; +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.Graphics; +import com.codename1.desktopcompat.java.awt.Graphics2D; +import com.codename1.desktopcompat.javax.swing.JButton; +import com.codename1.desktopcompat.javax.swing.JFrame; +import com.codename1.desktopcompat.javax.swing.JLabel; +import com.codename1.desktopcompat.javax.swing.JPanel; +import java.util.List; +import org.junit.Test; + +/// Who paints what: custom `paintComponent` code, the Codename One widget +/// behind a standard component, and neither when an override skips +/// `super.paintComponent`. +public class PaintRoutingTest extends KernelTestBase { + + @Test + public void aCustomPaintComponentDrawsInLogicalCoordinates() { + final int[] paints = new int[1]; + final boolean[] graphics2d = new boolean[1]; + final Color[] start = new Color[1]; + JPanel panel = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + paints[0]++; + graphics2d[0] = g instanceof Graphics2D; + start[0] = g.getColor(); + g.drawString("custom", 10, 20); + } + }; + panel.setForeground(Color.RED); + JFrame f = new JFrame(); + f.add(panel, BorderLayout.CENTER); + show(f); + List text = paint(f); + assertEquals(1, paints[0]); + assertTrue(graphics2d[0]); + Object[] drawn = find(text, "custom"); + assertNotNull(drawn); + assertEquals(onDisplay(panel, 10, 0)[0], ((Integer) drawn[1]).intValue()); + } + + @Test + public void theDefaultPaintComponentDrawsTheWidget() { + JButton b = new JButton("Go"); + JLabel l = new JLabel("Name"); + JFrame f = new JFrame(); + f.add(b, BorderLayout.SOUTH); + f.add(l, BorderLayout.NORTH); + show(f); + List text = paint(f); + assertNotNull(find(text, "Go")); + Object[] name = find(text, "Name"); + assertNotNull(name); + int x = ((Integer) name[1]).intValue(); + assertTrue(x >= l.cn1Peer().getAbsoluteX() && x < l.cn1Peer().getAbsoluteX() + l.cn1Peer().getWidth()); + } + + @Test + public void skippingSuperPaintComponentLeavesTheWidgetOut() { + JButton b = new JButton("Go") { + @Override + protected void paintComponent(Graphics g) { + g.drawString("mine", 2, 12); + } + }; + JFrame f = new JFrame(); + f.add(b, BorderLayout.SOUTH); + show(f); + List text = paint(f); + assertNull(find(text, "Go")); + assertNotNull(find(text, "mine")); + } + + @Test + public void callingSuperPaintComponentDrawsTheWidgetUnderCustomPainting() { + JButton b = new JButton("Go") { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + g.drawString("badge", 2, 12); + } + }; + JFrame f = new JFrame(); + f.add(b, BorderLayout.SOUTH); + show(f); + List text = paint(f); + int go = text.indexOf(find(text, "Go")); + int badge = text.indexOf(find(text, "badge")); + assertTrue(go >= 0 && badge > go); + } + + @Test + public void childrenArePaintedAfterTheirParentAndCanvasPaintRuns() { + final StringBuilder order = new StringBuilder(); + Canvas canvas = new Canvas() { + @Override + public void paint(Graphics g) { + order.append("canvas "); + g.drawString("canvas", 1, 10); + } + }; + canvas.setPreferredSize(new Dimension(50, 20)); + JPanel panel = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + order.append("panel "); + } + }; + panel.add(canvas); + JFrame f = new JFrame(); + f.add(panel, BorderLayout.CENTER); + show(f); + List text = paint(f); + assertEquals("panel canvas ", order.toString()); + Object[] drawn = find(text, "canvas"); + assertNotNull(drawn); + assertEquals(onDisplay(canvas, 1, 0)[0], ((Integer) drawn[1]).intValue()); + } + + @Test + public void anInvisibleComponentIsNotPainted() { + JButton b = new JButton("Hidden"); + b.setVisible(false); + JFrame f = new JFrame(); + f.add(b, BorderLayout.SOUTH); + show(f); + assertNull(find(paint(f), "Hidden")); + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/java/awt/geom/GeomParityTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/java/awt/geom/GeomParityTest.java new file mode 100644 index 00000000000..906d6e64457 --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/java/awt/geom/GeomParityTest.java @@ -0,0 +1,1444 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.awt.geom; + +import com.codename1.desktopcompat.java.awt.Dimension; +import com.codename1.desktopcompat.java.awt.Insets; +import com.codename1.desktopcompat.java.awt.Point; +import com.codename1.desktopcompat.java.awt.Rectangle; +import com.codename1.desktopcompat.java.awt.Shape; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/// Holds the geometry classes of the layer to the behaviour of the JDK +/// classes they stand in for. +/// +/// Every check builds the same object twice, once from this package and once +/// from the real `java.awt.geom`, feeds both the same deterministic inputs and +/// compares what comes back: matrices and outline coordinates within a +/// tolerance, classifications and hit tests exactly. +public class GeomParityTest { + + private static final double TOL = 1e-9; + private static final double FLOAT_TOL = 1e-4; + + // ---------------------------------------------------------------- transforms + + /// One step of a transform recipe, applied alike to both implementations. + private static final int TRANSLATE = 0; + private static final int SCALE = 1; + private static final int ROTATE = 2; + private static final int SHEAR = 3; + private static final int ROTATE_ABOUT = 4; + private static final int ROTATE_VEC = 5; + private static final int ROTATE_VEC_ABOUT = 6; + private static final int QUADRANT = 7; + private static final int QUADRANT_ABOUT = 8; + private static final int CONCAT = 9; + private static final int PRECONCAT = 10; + private static final int SET_ROTATION = 11; + private static final int SET_ROTATION_ABOUT = 12; + private static final int SET_ROTATION_VEC = 13; + private static final int SET_ROTATION_VEC_ABOUT = 14; + private static final int SET_QUADRANT = 15; + private static final int SET_QUADRANT_ABOUT = 16; + private static final int SET_SCALE = 17; + private static final int SET_SHEAR = 18; + private static final int SET_TRANSLATION = 19; + private static final int SET_MATRIX = 20; + private static final int INVERT = 21; + + private static final double[][][] RECIPES = { + {}, + {{TRANSLATE, 3, -4}}, + {{TRANSLATE, 0, 0}}, + {{SCALE, 2, 2}}, + {{SCALE, 2, 3}}, + {{SCALE, -1, 1}}, + {{SCALE, 1, -1}}, + {{SCALE, -1, -1}}, + {{SCALE, -2, -2}}, + {{SCALE, -2, 3}}, + {{SCALE, 2, -2}}, + {{SCALE, 1, 1}}, + {{ROTATE, Math.PI / 2}}, + {{ROTATE, Math.PI}}, + {{ROTATE, -Math.PI / 2}}, + {{ROTATE, 3 * Math.PI / 2}}, + {{ROTATE, 2 * Math.PI}}, + {{ROTATE, 0}}, + {{ROTATE, 0.3}}, + {{ROTATE, -2.1}}, + {{ROTATE, 0.3}, {SCALE, 2, 2}}, + {{ROTATE, 0.3}, {SCALE, 2, 3}}, + {{ROTATE, 0.3}, {SCALE, -1, 1}}, + {{ROTATE, 0.3}, {SCALE, -2, 2}}, + {{ROTATE, 1.1}, {SCALE, 1, -3}}, + {{ROTATE, Math.PI / 2}, {SCALE, 2, 2}}, + {{ROTATE, Math.PI / 2}, {SCALE, 2, -2}}, + {{ROTATE, Math.PI / 2}, {SCALE, 2, 5}}, + {{ROTATE, Math.PI / 2}, {SCALE, -2, 5}}, + {{SHEAR, 0.5, 0}}, + {{SHEAR, 0.5, -0.25}}, + {{SHEAR, 0, 0}}, + {{TRANSLATE, 5, 6}, {ROTATE, 0.7}, {SCALE, 1.5, 0.5}, {SHEAR, 0.2, 0.1}}, + {{ROTATE_ABOUT, 0.9, 4, 5}}, + {{ROTATE_ABOUT, Math.PI / 2, 4, 5}}, + {{TRANSLATE, 1, 2}, {ROTATE_ABOUT, -0.4, 7, -3}}, + {{ROTATE_VEC, 3, 4}}, + {{ROTATE_VEC, 0, 4}}, + {{ROTATE_VEC, 0, -4}}, + {{ROTATE_VEC, -2, 0}}, + {{ROTATE_VEC, 2, 0}}, + {{ROTATE_VEC, 0, 0}}, + {{SCALE, 2, 3}, {ROTATE_VEC, -1, 2}}, + {{ROTATE_VEC_ABOUT, 3, 4, 2, 9}}, + {{ROTATE_VEC_ABOUT, 0, -1, 2, 9}}, + {{QUADRANT, 0}}, + {{QUADRANT, 1}}, + {{QUADRANT, 2}}, + {{QUADRANT, 3}}, + {{QUADRANT, 5}}, + {{QUADRANT, -1}}, + {{SCALE, 2, 3}, {QUADRANT, 1}}, + {{QUADRANT_ABOUT, 1, 5, 7}}, + {{QUADRANT_ABOUT, 2, 5, 7}}, + {{QUADRANT_ABOUT, 3, 5, 7}}, + {{SHEAR, 0.3, 0.2}, {QUADRANT_ABOUT, 3, 5, 7}}, + {{TRANSLATE, 2, 3}, {CONCAT, 1, 0.5, -0.25, 2, 4, 5}}, + {{ROTATE, 0.4}, {PRECONCAT, 1, 0.5, -0.25, 2, 4, 5}}, + {{SCALE, 3, 3}, {PRECONCAT, 0, 1, -1, 0, 0, 0}}, + {{SHEAR, 1, 1}, {SET_ROTATION, 0.6}}, + {{SET_ROTATION, Math.PI}}, + {{SET_ROTATION_ABOUT, 0.6, 3, 2}}, + {{SET_ROTATION_ABOUT, Math.PI, 3, 2}}, + {{SET_ROTATION_VEC, 5, -12}}, + {{SET_ROTATION_VEC, 0, 0}}, + {{SET_ROTATION_VEC_ABOUT, 5, -12, 1, 1}}, + {{SET_ROTATION_VEC_ABOUT, -7, 0, 1, 1}}, + {{SET_QUADRANT, 1}}, + {{SET_QUADRANT, 6}}, + {{SET_QUADRANT_ABOUT, 1, 5, 7}}, + {{SET_QUADRANT_ABOUT, 3, -5, 7}}, + {{SET_SCALE, 4, 4}}, + {{SET_SCALE, 4, 0.25}}, + {{SET_SHEAR, 0.5, 2}}, + {{SET_TRANSLATION, 8, 9}}, + {{SET_MATRIX, 0, 1, 1, 0, 0, 0}}, + {{SET_MATRIX, 0, 2, 2, 0, 0, 0}}, + {{SET_MATRIX, 0, -3, 3, 0, 1, 0}}, + {{SET_MATRIX, 0, 3, -3, 0, 0, 0}}, + {{SET_MATRIX, 0, 2, -3, 0, 0, 0}}, + {{SET_MATRIX, 0, -1, -1, 0, 0, 0}}, + {{SET_MATRIX, 0, -2, -5, 0, 0, 0}}, + {{SET_MATRIX, 0, 0, 0, 0, 0, 0}}, + {{SET_MATRIX, 2, 0, 0, 0, 0, 0}}, + {{SET_MATRIX, 0, 0, 0, 3, 0, 0}}, + {{SET_MATRIX, 0, 0, 2, 0, 0, 0}}, + {{SET_MATRIX, 1, 2, 3, 4, 5, 6}}, + {{SET_MATRIX, 3, 4, -4, 3, 0, 0}}, + {{SET_MATRIX, 3, 4, 4, -3, 0, 0}}, + {{SET_MATRIX, 0.6, 0.8, -0.8, 0.6, 0, 0}}, + {{SET_MATRIX, 0.6, 0.8, 0.8, -0.6, 0, 0}}, + {{SET_MATRIX, 2, 1, -2, 4, 0, 0}}, + {{SET_MATRIX, -2, 1, 2, 4, 0, 0}}, + {{TRANSLATE, 5, 6}, {ROTATE, 0.7}, {SCALE, 1.5, 0.5}, {INVERT}}, + {{SCALE, 4, 4}, {INVERT}}, + {{QUADRANT_ABOUT, 1, 5, 7}, {INVERT}}, + }; + + private static void apply(double[] op, AffineTransform ours, java.awt.geom.AffineTransform real) + throws Exception { + switch ((int) op[0]) { + case TRANSLATE: + ours.translate(op[1], op[2]); + real.translate(op[1], op[2]); + break; + case SCALE: + ours.scale(op[1], op[2]); + real.scale(op[1], op[2]); + break; + case ROTATE: + ours.rotate(op[1]); + real.rotate(op[1]); + break; + case SHEAR: + ours.shear(op[1], op[2]); + real.shear(op[1], op[2]); + break; + case ROTATE_ABOUT: + ours.rotate(op[1], op[2], op[3]); + real.rotate(op[1], op[2], op[3]); + break; + case ROTATE_VEC: + ours.rotate(op[1], op[2]); + real.rotate(op[1], op[2]); + break; + case ROTATE_VEC_ABOUT: + ours.rotate(op[1], op[2], op[3], op[4]); + real.rotate(op[1], op[2], op[3], op[4]); + break; + case QUADRANT: + ours.quadrantRotate((int) op[1]); + real.quadrantRotate((int) op[1]); + break; + case QUADRANT_ABOUT: + ours.quadrantRotate((int) op[1], op[2], op[3]); + real.quadrantRotate((int) op[1], op[2], op[3]); + break; + case CONCAT: + ours.concatenate(new AffineTransform(op[1], op[2], op[3], op[4], op[5], op[6])); + real.concatenate(new java.awt.geom.AffineTransform(op[1], op[2], op[3], op[4], op[5], op[6])); + break; + case PRECONCAT: + ours.preConcatenate(new AffineTransform(op[1], op[2], op[3], op[4], op[5], op[6])); + real.preConcatenate(new java.awt.geom.AffineTransform(op[1], op[2], op[3], op[4], op[5], op[6])); + break; + case SET_ROTATION: + ours.setToRotation(op[1]); + real.setToRotation(op[1]); + break; + case SET_ROTATION_ABOUT: + ours.setToRotation(op[1], op[2], op[3]); + real.setToRotation(op[1], op[2], op[3]); + break; + case SET_ROTATION_VEC: + ours.setToRotation(op[1], op[2]); + real.setToRotation(op[1], op[2]); + break; + case SET_ROTATION_VEC_ABOUT: + ours.setToRotation(op[1], op[2], op[3], op[4]); + real.setToRotation(op[1], op[2], op[3], op[4]); + break; + case SET_QUADRANT: + ours.setToQuadrantRotation((int) op[1]); + real.setToQuadrantRotation((int) op[1]); + break; + case SET_QUADRANT_ABOUT: + ours.setToQuadrantRotation((int) op[1], op[2], op[3]); + real.setToQuadrantRotation((int) op[1], op[2], op[3]); + break; + case SET_SCALE: + ours.setToScale(op[1], op[2]); + real.setToScale(op[1], op[2]); + break; + case SET_SHEAR: + ours.setToShear(op[1], op[2]); + real.setToShear(op[1], op[2]); + break; + case SET_TRANSLATION: + ours.setToTranslation(op[1], op[2]); + real.setToTranslation(op[1], op[2]); + break; + case SET_MATRIX: + ours.setTransform(op[1], op[2], op[3], op[4], op[5], op[6]); + real.setTransform(op[1], op[2], op[3], op[4], op[5], op[6]); + break; + case INVERT: + ours.invert(); + real.invert(); + break; + default: + fail("unknown recipe step " + op[0]); + break; + } + } + + private static void assertSameMatrix(String label, AffineTransform ours, java.awt.geom.AffineTransform real) { + double[] a = new double[6]; + double[] b = new double[6]; + ours.getMatrix(a); + real.getMatrix(b); + for (int i = 0; i < 6; i++) { + assertEquals(label + " matrix[" + i + "]", b[i], a[i], TOL * Math.max(1.0, Math.abs(b[i]))); + } + assertEquals(label + " scaleX", real.getScaleX(), ours.getScaleX(), TOL); + assertEquals(label + " scaleY", real.getScaleY(), ours.getScaleY(), TOL); + assertEquals(label + " shearX", real.getShearX(), ours.getShearX(), TOL); + assertEquals(label + " shearY", real.getShearY(), ours.getShearY(), TOL); + assertEquals(label + " translateX", real.getTranslateX(), ours.getTranslateX(), TOL); + assertEquals(label + " translateY", real.getTranslateY(), ours.getTranslateY(), TOL); + } + + @Test + public void affineTransformMatchesTheJdk() throws Exception { + double[] pts = {0, 0, 1, 0, 0, 1, -3.5, 2.25, 100, -40, 0.001, 7}; + for (int r = 0; r < RECIPES.length; r++) { + String label = "recipe " + r; + AffineTransform ours = new AffineTransform(); + java.awt.geom.AffineTransform real = new java.awt.geom.AffineTransform(); + for (double[] op : RECIPES[r]) { + apply(op, ours, real); + } + assertSameMatrix(label, ours, real); + assertEquals(label + " type", real.getType(), ours.getType()); + assertEquals(label + " identity", real.isIdentity(), ours.isIdentity()); + assertEquals(label + " determinant", real.getDeterminant(), ours.getDeterminant(), + TOL * Math.max(1.0, Math.abs(real.getDeterminant()))); + + double[] a = new double[pts.length]; + double[] b = new double[pts.length]; + ours.transform(pts, 0, a, 0, pts.length / 2); + real.transform(pts, 0, b, 0, pts.length / 2); + assertArrayNear(label + " transform", b, a, TOL); + ours.deltaTransform(pts, 0, a, 0, pts.length / 2); + real.deltaTransform(pts, 0, b, 0, pts.length / 2); + assertArrayNear(label + " deltaTransform", b, a, TOL); + + float[] fsrc = new float[pts.length]; + for (int i = 0; i < pts.length; i++) { + fsrc[i] = (float) pts[i]; + } + float[] fa = new float[pts.length]; + float[] fb = new float[pts.length]; + ours.transform(fsrc, 0, fa, 0, pts.length / 2); + real.transform(fsrc, 0, fb, 0, pts.length / 2); + for (int i = 0; i < pts.length; i++) { + assertEquals(label + " float transform " + i, fb[i], fa[i], FLOAT_TOL * Math.max(1f, Math.abs(fb[i]))); + } + ours.transform(pts, 0, fa, 0, pts.length / 2); + real.transform(pts, 0, fb, 0, pts.length / 2); + for (int i = 0; i < pts.length; i++) { + assertEquals(label + " mixed transform " + i, fb[i], fa[i], FLOAT_TOL * Math.max(1f, Math.abs(fb[i]))); + } + ours.transform(fsrc, 0, a, 0, pts.length / 2); + real.transform(fsrc, 0, b, 0, pts.length / 2); + assertArrayNear(label + " float to double transform", b, a, TOL); + + Point2D p = ours.transform(new Point2D.Double(2.5, -1.5), null); + java.awt.geom.Point2D q = real.transform(new java.awt.geom.Point2D.Double(2.5, -1.5), null); + assertTrue(label, p instanceof Point2D.Double); + assertEquals(label + " point x", q.getX(), p.getX(), TOL); + assertEquals(label + " point y", q.getY(), p.getY(), TOL); + p = ours.deltaTransform(new Point2D.Float(2.5f, -1.5f), null); + q = real.deltaTransform(new java.awt.geom.Point2D.Float(2.5f, -1.5f), null); + assertTrue(label, p instanceof Point2D.Float); + assertEquals(label + " delta x", q.getX(), p.getX(), FLOAT_TOL); + assertEquals(label + " delta y", q.getY(), p.getY(), FLOAT_TOL); + + boolean realInvertible = true; + java.awt.geom.AffineTransform realInverse = null; + try { + realInverse = real.createInverse(); + } catch (java.awt.geom.NoninvertibleTransformException e) { + realInvertible = false; + } + boolean oursInvertible = true; + AffineTransform oursInverse = null; + try { + oursInverse = ours.createInverse(); + } catch (NoninvertibleTransformException e) { + oursInvertible = false; + } + assertEquals(label + " invertible", realInvertible, oursInvertible); + if (realInvertible) { + assertSameMatrix(label + " inverse", oursInverse, realInverse); + ours.inverseTransform(pts, 0, a, 0, pts.length / 2); + real.inverseTransform(pts, 0, b, 0, pts.length / 2); + assertArrayNear(label + " inverseTransform", b, a, TOL); + p = ours.inverseTransform(new Point2D.Double(2.5, -1.5), null); + q = real.inverseTransform(new java.awt.geom.Point2D.Double(2.5, -1.5), null); + assertEquals(label + " inverse point x", q.getX(), p.getX(), TOL * Math.max(1.0, Math.abs(q.getX()))); + assertEquals(label + " inverse point y", q.getY(), p.getY(), TOL * Math.max(1.0, Math.abs(q.getY()))); + } + } + } + + @Test + public void affineTransformFactoriesAndValueSemantics() throws Exception { + assertSameMatrix("translate", AffineTransform.getTranslateInstance(3, 4), + java.awt.geom.AffineTransform.getTranslateInstance(3, 4)); + assertSameMatrix("rotate", AffineTransform.getRotateInstance(0.8), + java.awt.geom.AffineTransform.getRotateInstance(0.8)); + assertSameMatrix("rotate about", AffineTransform.getRotateInstance(0.8, 5, 6), + java.awt.geom.AffineTransform.getRotateInstance(0.8, 5, 6)); + assertSameMatrix("rotate vec", AffineTransform.getRotateInstance(2.0, -7.0), + java.awt.geom.AffineTransform.getRotateInstance(2.0, -7.0)); + assertSameMatrix("rotate vec about", AffineTransform.getRotateInstance(2.0, -7.0, 5, 6), + java.awt.geom.AffineTransform.getRotateInstance(2.0, -7.0, 5, 6)); + assertSameMatrix("quadrant", AffineTransform.getQuadrantRotateInstance(3), + java.awt.geom.AffineTransform.getQuadrantRotateInstance(3)); + assertSameMatrix("quadrant about", AffineTransform.getQuadrantRotateInstance(3, 5, 6), + java.awt.geom.AffineTransform.getQuadrantRotateInstance(3, 5, 6)); + assertSameMatrix("scale", AffineTransform.getScaleInstance(3, 4), + java.awt.geom.AffineTransform.getScaleInstance(3, 4)); + assertSameMatrix("shear", AffineTransform.getShearInstance(3, 4), + java.awt.geom.AffineTransform.getShearInstance(3, 4)); + assertSameMatrix("floats", new AffineTransform(new float[] {1, 2, 3, 4, 5, 6}), + new java.awt.geom.AffineTransform(new float[] {1, 2, 3, 4, 5, 6})); + assertSameMatrix("four doubles", new AffineTransform(new double[] {1, 2, 3, 4}), + new java.awt.geom.AffineTransform(new double[] {1, 2, 3, 4})); + assertSameMatrix("six floats", new AffineTransform(1f, 2f, 3f, 4f, 5f, 6f), + new java.awt.geom.AffineTransform(1f, 2f, 3f, 4f, 5f, 6f)); + + AffineTransform a = new AffineTransform(1, 2, 3, 4, 5, 6); + Object copy = a.clone(); + assertTrue(copy instanceof AffineTransform); + assertEquals(a, copy); + assertEquals(a.hashCode(), copy.hashCode()); + assertFalse(a.equals(new AffineTransform())); + assertEquals(new AffineTransform(a), a); + a.setToIdentity(); + assertTrue(a.isIdentity()); + + // a destination that overlaps the source further along must not + // read values it has already overwritten + double[] ours = {1, 2, 3, 4, 5, 6, 0, 0}; + double[] real = ours.clone(); + new AffineTransform(2, 0, 0, 3, 1, 1).transform(ours, 0, ours, 2, 3); + new java.awt.geom.AffineTransform(2, 0, 0, 3, 1, 1).transform(real, 0, real, 2, 3); + assertArrayNear("overlap", real, ours, TOL); + + Point2D[] src = {new Point2D.Double(1, 2), new Point2D.Float(3, 4)}; + Point2D[] dst = new Point2D[2]; + new AffineTransform(2, 0, 0, 3, 1, 1).transform(src, 0, dst, 0, 2); + assertEquals(new Point2D.Double(3, 7), dst[0]); + assertTrue(dst[1] instanceof Point2D.Float); + assertEquals(7.0, dst[1].getX(), 0.0); + assertEquals(13.0, dst[1].getY(), 0.0); + + assertNull(new AffineTransform().createTransformedShape(null)); + Shape moved = AffineTransform.getTranslateInstance(10, 20) + .createTransformedShape(new Rectangle2D.Double(1, 2, 3, 4)); + java.awt.Shape realMoved = java.awt.geom.AffineTransform.getTranslateInstance(10, 20) + .createTransformedShape(new java.awt.geom.Rectangle2D.Double(1, 2, 3, 4)); + assertSamePath("transformed shape", moved.getPathIterator(null), realMoved.getPathIterator(null), TOL); + } + + private static void assertArrayNear(String label, double[] expected, double[] actual, double tol) { + assertEquals(label + " length", expected.length, actual.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(label + "[" + i + "]", expected[i], actual[i], tol * Math.max(1.0, Math.abs(expected[i]))); + } + } + + // ---------------------------------------------------------------- outlines + + private static void assertSamePath(String label, PathIterator ours, java.awt.geom.PathIterator real, double tol) { + assertEquals(label + " winding rule", real.getWindingRule(), ours.getWindingRule()); + double[] a = new double[6]; + double[] b = new double[6]; + float[] fa = new float[6]; + int index = 0; + while (!real.isDone()) { + assertFalse(label + ": ours ended at segment " + index, ours.isDone()); + int realType = real.currentSegment(b); + int oursType = ours.currentSegment(a); + assertEquals(label + " type of segment " + index, realType, oursType); + assertEquals(label + " float type of segment " + index, realType, ours.currentSegment(fa)); + int n = realType == java.awt.geom.PathIterator.SEG_CLOSE ? 0 + : realType == java.awt.geom.PathIterator.SEG_QUADTO ? 4 + : realType == java.awt.geom.PathIterator.SEG_CUBICTO ? 6 : 2; + for (int i = 0; i < n; i++) { + assertEquals(label + " segment " + index + " value " + i, b[i], a[i], tol); + assertEquals(label + " segment " + index + " float value " + i, (float) a[i], fa[i], + 1e-6f * Math.max(1f, Math.abs(fa[i]))); + } + real.next(); + ours.next(); + index++; + } + assertTrue(label + ": ours has more than " + index + " segments", ours.isDone()); + } + + private static AffineTransform[] oursTransforms() { + return new AffineTransform[] {null, new AffineTransform(1.5, 0.25, -0.5, 2.0, 7.0, -3.0)}; + } + + private static java.awt.geom.AffineTransform[] realTransforms() { + return new java.awt.geom.AffineTransform[] {null, + new java.awt.geom.AffineTransform(1.5, 0.25, -0.5, 2.0, 7.0, -3.0)}; + } + + private static void assertSameOutline(String label, Shape ours, java.awt.Shape real, double tol) { + AffineTransform[] a = oursTransforms(); + java.awt.geom.AffineTransform[] b = realTransforms(); + for (int i = 0; i < a.length; i++) { + assertSamePath(label + (i == 0 ? "" : " transformed"), ours.getPathIterator(a[i]), + real.getPathIterator(b[i]), tol); + } + } + + private static final double[][] FRAMES = { + {1, 2, 10, 6}, {-5.5, 3.25, 4, 9}, {0, 0, 1, 1}, {3, 3, 0, 5}, {2, 2, 7, 0}, {100, -200, 0.5, 300}, + }; + + @Test + public void rectangleLineAndEllipseOutlinesMatchTheJdk() { + for (double[] f : FRAMES) { + String label = "frame " + f[0] + "," + f[1] + "," + f[2] + "," + f[3]; + assertSameOutline("rectangle " + label, new Rectangle2D.Double(f[0], f[1], f[2], f[3]), + new java.awt.geom.Rectangle2D.Double(f[0], f[1], f[2], f[3]), TOL); + assertSameOutline("float rectangle " + label, + new Rectangle2D.Float((float) f[0], (float) f[1], (float) f[2], (float) f[3]), + new java.awt.geom.Rectangle2D.Float((float) f[0], (float) f[1], (float) f[2], (float) f[3]), + FLOAT_TOL); + assertSameOutline("ellipse " + label, new Ellipse2D.Double(f[0], f[1], f[2], f[3]), + new java.awt.geom.Ellipse2D.Double(f[0], f[1], f[2], f[3]), TOL); + assertSameOutline("float ellipse " + label, + new Ellipse2D.Float((float) f[0], (float) f[1], (float) f[2], (float) f[3]), + new java.awt.geom.Ellipse2D.Float((float) f[0], (float) f[1], (float) f[2], (float) f[3]), + FLOAT_TOL); + assertSameOutline("line " + label, new Line2D.Double(f[0], f[1], f[2], f[3]), + new java.awt.geom.Line2D.Double(f[0], f[1], f[2], f[3]), TOL); + assertSameOutline("float line " + label, + new Line2D.Float((float) f[0], (float) f[1], (float) f[2], (float) f[3]), + new java.awt.geom.Line2D.Float((float) f[0], (float) f[1], (float) f[2], (float) f[3]), + FLOAT_TOL); + assertSameOutline("integer rectangle " + label, + new Rectangle((int) f[0], (int) f[1], (int) f[2], (int) f[3]), + new java.awt.Rectangle((int) f[0], (int) f[1], (int) f[2], (int) f[3]), TOL); + } + assertSameOutline("quad", new QuadCurve2D.Double(1, 2, 3, 9, 8, 4), + new java.awt.geom.QuadCurve2D.Double(1, 2, 3, 9, 8, 4), TOL); + assertSameOutline("cubic", new CubicCurve2D.Double(1, 2, 3, 9, 8, 4, 6, -2), + new java.awt.geom.CubicCurve2D.Double(1, 2, 3, 9, 8, 4, 6, -2), TOL); + } + + private static final double[][] CORNERS = { + {0, 0}, {2, 2}, {4, 1}, {1, 5}, {30, 2}, {2, 30}, {40, 40}, {-3, -2}, {-50, 3}, + }; + + @Test + public void roundRectangleOutlinesMatchTheJdk() { + for (double[] f : FRAMES) { + for (double[] c : CORNERS) { + String label = "round rectangle " + f[0] + "," + f[1] + "," + f[2] + "," + f[3] + + " arc " + c[0] + "," + c[1]; + assertSameOutline(label, new RoundRectangle2D.Double(f[0], f[1], f[2], f[3], c[0], c[1]), + new java.awt.geom.RoundRectangle2D.Double(f[0], f[1], f[2], f[3], c[0], c[1]), TOL); + assertSameOutline("float " + label, + new RoundRectangle2D.Float((float) f[0], (float) f[1], (float) f[2], (float) f[3], + (float) c[0], (float) c[1]), + new java.awt.geom.RoundRectangle2D.Float((float) f[0], (float) f[1], (float) f[2], + (float) f[3], (float) c[0], (float) c[1]), FLOAT_TOL); + } + } + } + + private static final double[] STARTS = {0, 30, 90, -45, 200, 370, -400, 180, 12.5}; + private static final double[] EXTENTS = {90, 45, 135, 180, 270, 360, 400, -90, -200, -360, -500, 0, 10, 91, -179}; + private static final int[] CLOSURES = {Arc2D.OPEN, Arc2D.CHORD, Arc2D.PIE}; + + @Test + public void arcOutlinesMatchTheJdk() { + assertEquals(java.awt.geom.Arc2D.OPEN, Arc2D.OPEN); + assertEquals(java.awt.geom.Arc2D.CHORD, Arc2D.CHORD); + assertEquals(java.awt.geom.Arc2D.PIE, Arc2D.PIE); + for (double[] f : FRAMES) { + for (double start : STARTS) { + for (double extent : EXTENTS) { + for (int closure : CLOSURES) { + String label = "arc " + f[0] + "," + f[1] + "," + f[2] + "," + f[3] + " from " + start + + " by " + extent + " closure " + closure; + Arc2D ours = new Arc2D.Double(f[0], f[1], f[2], f[3], start, extent, closure); + java.awt.geom.Arc2D real = + new java.awt.geom.Arc2D.Double(f[0], f[1], f[2], f[3], start, extent, closure); + assertSameOutline(label, ours, real, TOL * Math.max(1.0, Math.abs(f[1]))); + assertSameOutline("float " + label, + new Arc2D.Float((float) f[0], (float) f[1], (float) f[2], (float) f[3], + (float) start, (float) extent, closure), + new java.awt.geom.Arc2D.Float((float) f[0], (float) f[1], (float) f[2], (float) f[3], + (float) start, (float) extent, closure), FLOAT_TOL * Math.max(1.0, Math.abs(f[1]))); + assertSameRect(label + " bounds", real.getBounds2D(), ours.getBounds2D(), 1e-9); + assertEquals(label + " start x", real.getStartPoint().getX(), ours.getStartPoint().getX(), TOL); + assertEquals(label + " start y", real.getStartPoint().getY(), ours.getStartPoint().getY(), 1e-7); + assertEquals(label + " end x", real.getEndPoint().getX(), ours.getEndPoint().getX(), TOL); + assertEquals(label + " end y", real.getEndPoint().getY(), ours.getEndPoint().getY(), 1e-7); + for (double angle = -725; angle <= 725; angle += 12.5) { + assertEquals(label + " containsAngle " + angle, real.containsAngle(angle), + ours.containsAngle(angle)); + } + } + } + } + } + // a frame with a negative size has no outline + assertSameOutline("negative arc", new Arc2D.Double(1, 2, -3, 4, 10, 80, Arc2D.PIE), + new java.awt.geom.Arc2D.Double(1, 2, -3, 4, 10, 80, java.awt.geom.Arc2D.PIE), TOL); + } + + private static void assertSameRect(String label, java.awt.geom.Rectangle2D real, Rectangle2D ours, double tol) { + assertEquals(label + " x", real.getX(), ours.getX(), tol * Math.max(1.0, Math.abs(real.getX()))); + assertEquals(label + " y", real.getY(), ours.getY(), tol * Math.max(1.0, Math.abs(real.getY()))); + assertEquals(label + " width", real.getWidth(), ours.getWidth(), tol * Math.max(1.0, Math.abs(real.getWidth()))); + assertEquals(label + " height", real.getHeight(), ours.getHeight(), + tol * Math.max(1.0, Math.abs(real.getHeight()))); + } + + @Test + public void arcAnglesFromPointsMatchTheJdk() { + double[][] pairs = {{12, 3, 4, 9}, {6, 0, 6, 20}, {-3, 5, 20, 5.5}, {7, 7, 7, 7}, {2, 8, 11, 1}}; + for (double[] p : pairs) { + Arc2D ours = new Arc2D.Double(1, 2, 10, 6, 0, 90, Arc2D.PIE); + java.awt.geom.Arc2D real = new java.awt.geom.Arc2D.Double(1, 2, 10, 6, 0, 90, java.awt.geom.Arc2D.PIE); + ours.setAngles(p[0], p[1], p[2], p[3]); + real.setAngles(p[0], p[1], p[2], p[3]); + assertEquals("setAngles start", real.getAngleStart(), ours.getAngleStart(), 1e-7); + assertEquals("setAngles extent", real.getAngleExtent(), ours.getAngleExtent(), 1e-7); + ours.setAngleStart(new Point2D.Double(p[2], p[3])); + real.setAngleStart(new java.awt.geom.Point2D.Double(p[2], p[3])); + assertEquals("setAngleStart", real.getAngleStart(), ours.getAngleStart(), 1e-7); + ours.setArcByCenter(p[0], p[1], 5, 20, 100, Arc2D.CHORD); + real.setArcByCenter(p[0], p[1], 5, 20, 100, java.awt.geom.Arc2D.CHORD); + assertSameOutline("by centre", ours, real, TOL); + ours.setFrame(1, 1, 4, 4); + real.setFrame(1, 1, 4, 4); + assertSameOutline("re-framed", ours, real, TOL); + } + try { + new Arc2D.Double(7); + fail("an unknown closure must be refused"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().length() > 0); + } + } + + // ---------------------------------------------------------------- hit tests + + /// Compares `contains(x, y)` over a grid that covers the shape and a + /// margin around it, and returns how many points were inside so a caller + /// can tell the grid really exercised the shape. + private static int assertSameContains(String label, Shape ours, java.awt.Shape real, + double x0, double y0, double x1, double y1) { + int inside = 0; + for (double y = y0; y <= y1; y += (y1 - y0) / 37.0) { + for (double x = x0; x <= x1; x += (x1 - x0) / 41.0) { + boolean expected = real.contains(x, y); + assertEquals(label + " contains " + x + "," + y, expected, ours.contains(x, y)); + if (expected) { + inside++; + } + } + } + return inside; + } + + @Test + public void containsPointMatchesTheJdk() { + int inside = 0; + for (double[] f : FRAMES) { + double x0 = f[0] - 1.3; + double y0 = f[1] - 1.7; + double x1 = f[0] + f[2] + 1.1; + double y1 = f[1] + f[3] + 1.9; + String label = f[0] + "," + f[1] + "," + f[2] + "," + f[3]; + inside += assertSameContains("rectangle " + label, new Rectangle2D.Double(f[0], f[1], f[2], f[3]), + new java.awt.geom.Rectangle2D.Double(f[0], f[1], f[2], f[3]), x0, y0, x1, y1); + inside += assertSameContains("ellipse " + label, new Ellipse2D.Double(f[0], f[1], f[2], f[3]), + new java.awt.geom.Ellipse2D.Double(f[0], f[1], f[2], f[3]), x0, y0, x1, y1); + inside += assertSameContains("line " + label, new Line2D.Double(f[0], f[1], f[2], f[3]), + new java.awt.geom.Line2D.Double(f[0], f[1], f[2], f[3]), x0, y0, x1, y1); + for (double[] c : CORNERS) { + inside += assertSameContains("round rectangle " + label + " arc " + c[0] + "," + c[1], + new RoundRectangle2D.Double(f[0], f[1], f[2], f[3], c[0], c[1]), + new java.awt.geom.RoundRectangle2D.Double(f[0], f[1], f[2], f[3], c[0], c[1]), + x0, y0, x1, y1); + } + for (double start : STARTS) { + for (double extent : EXTENTS) { + for (int closure : CLOSURES) { + inside += assertSameContains( + "arc " + label + " from " + start + " by " + extent + " closure " + closure, + new Arc2D.Double(f[0], f[1], f[2], f[3], start, extent, closure), + new java.awt.geom.Arc2D.Double(f[0], f[1], f[2], f[3], start, extent, closure), + x0, y0, x1, y1); + } + } + } + } + assertTrue("the grids must land inside the shapes: " + inside, inside > 10000); + // boundary ownership: left and top edges in, right and bottom out + Rectangle2D r = new Rectangle2D.Double(1, 2, 3, 4); + assertTrue(r.contains(1, 2)); + assertFalse(r.contains(4, 2)); + assertFalse(r.contains(1, 6)); + } + + private static final double[][] STAR = { + {50, 5}, {79, 95}, {3, 39}, {97, 39}, {21, 95}, + }; + + @Test + public void starPolygonContainsMatchesTheJdkUnderBothWindingRules() { + int[] rules = {Path2D.WIND_EVEN_ODD, Path2D.WIND_NON_ZERO}; + int[] inside = new int[2]; + for (int i = 0; i < rules.length; i++) { + Path2D ours = new Path2D.Double(rules[i]); + java.awt.geom.Path2D real = new java.awt.geom.Path2D.Double(rules[i]); + GeneralPath oursFloat = new GeneralPath(rules[i]); + java.awt.geom.GeneralPath realFloat = new java.awt.geom.GeneralPath(rules[i]); + for (int p = 0; p < STAR.length; p++) { + if (p == 0) { + ours.moveTo(STAR[p][0], STAR[p][1]); + real.moveTo(STAR[p][0], STAR[p][1]); + oursFloat.moveTo((float) STAR[p][0], (float) STAR[p][1]); + realFloat.moveTo((float) STAR[p][0], (float) STAR[p][1]); + } else { + ours.lineTo(STAR[p][0], STAR[p][1]); + real.lineTo(STAR[p][0], STAR[p][1]); + oursFloat.lineTo((float) STAR[p][0], (float) STAR[p][1]); + realFloat.lineTo((float) STAR[p][0], (float) STAR[p][1]); + } + } + ours.closePath(); + real.closePath(); + oursFloat.closePath(); + realFloat.closePath(); + assertEquals(real.getWindingRule(), ours.getWindingRule()); + assertSameOutline("star rule " + rules[i], ours, real, TOL); + assertSameOutline("float star rule " + rules[i], oursFloat, realFloat, FLOAT_TOL); + inside[i] = assertSameContains("star rule " + rules[i], ours, real, -4.3, -3.1, 104.7, 103.3); + assertSameContains("float star rule " + rules[i], oursFloat, realFloat, -4.3, -3.1, 104.7, 103.3); + for (double y = -3.1; y <= 103.3; y += 7.9) { + for (double x = -4.3; x <= 104.7; x += 6.7) { + assertEquals("static contains", real.contains(x, y), Path2D.contains(ours.getPathIterator(null), x, y)); + } + } + } + // the pentagon in the middle of the star is what the two rules + // disagree about + assertTrue("even-odd " + inside[0] + " non-zero " + inside[1], inside[1] > inside[0] && inside[0] > 0); + } + + @Test + public void pathRectangleTestsMatchTheJdkForRectanglesAndConvexPolygons() { + double[][][] polygons = { + {{10, 10}, {60, 10}, {60, 40}, {10, 40}}, + {{35, 5}, {65, 30}, {50, 60}, {20, 60}, {5, 30}}, + {{10, 10}, {61, 36}, {10, 60}}, + }; + int hits = 0; + int holds = 0; + for (double[][] polygon : polygons) { + Path2D ours = new Path2D.Double(); + java.awt.geom.Path2D real = new java.awt.geom.Path2D.Double(); + for (int p = 0; p < polygon.length; p++) { + if (p == 0) { + ours.moveTo(polygon[p][0], polygon[p][1]); + real.moveTo(polygon[p][0], polygon[p][1]); + } else { + ours.lineTo(polygon[p][0], polygon[p][1]); + real.lineTo(polygon[p][0], polygon[p][1]); + } + } + ours.closePath(); + real.closePath(); + assertSameRect("bounds", real.getBounds2D(), ours.getBounds2D(), TOL); + assertEquals(real.getBounds().x, ours.getBounds().x); + assertEquals(real.getBounds().width, ours.getBounds().width); + double[] sizes = {0.7, 3.3, 11.1, 27.9, 80.3}; + for (double w : sizes) { + for (double h : sizes) { + for (double y = -6.3; y <= 70; y += 4.7) { + for (double x = -5.9; x <= 70; x += 5.3) { + String label = "rect " + x + "," + y + "," + w + "," + h; + boolean expected = real.intersects(x, y, w, h); + assertEquals(label + " intersects", expected, ours.intersects(x, y, w, h)); + assertEquals(label + " intersects (rect)", expected, + ours.intersects(new Rectangle2D.Double(x, y, w, h))); + boolean contained = real.contains(x, y, w, h); + assertEquals(label + " contains", contained, ours.contains(x, y, w, h)); + assertEquals(label + " static contains", contained, + Path2D.contains(ours.getPathIterator(null), x, y, w, h)); + assertEquals(label + " static intersects", expected, + Path2D.intersects(ours.getPathIterator(null), new Rectangle2D.Double(x, y, w, h))); + hits += expected ? 1 : 0; + holds += contained ? 1 : 0; + } + } + } + } + // a rectangle that only shares an edge does not intersect, and + // empty rectangles never do + assertEquals(real.intersects(60, 10, 5, 5), ours.intersects(60, 10, 5, 5)); + assertEquals(real.intersects(20, 20, 0, 5), ours.intersects(20, 20, 0, 5)); + assertEquals(real.contains(20, 20, 0, 5), ours.contains(20, 20, 0, 5)); + } + assertTrue("grid must exercise both answers: " + hits + " " + holds, hits > 100 && holds > 20); + } + + @Test + public void pathBuildingMatchesTheJdk() { + Path2D ours = new Path2D.Double(); + java.awt.geom.Path2D real = new java.awt.geom.Path2D.Double(); + assertNull(ours.getCurrentPoint()); + try { + ours.lineTo(1, 1); + fail("a line needs a move first"); + } catch (IllegalPathStateException expected) { + assertTrue(expected.getMessage().length() > 0); + } + // a move directly after a move replaces it + ours.moveTo(1, 1); + real.moveTo(1, 1); + ours.moveTo(2, 3); + real.moveTo(2, 3); + ours.lineTo(10, 3); + real.lineTo(10, 3); + ours.quadTo(14, 8, 10, 12); + real.quadTo(14, 8, 10, 12); + ours.curveTo(8, 14, 4, 14, 2, 12); + real.curveTo(8, 14, 4, 14, 2, 12); + assertEquals(real.getCurrentPoint().getX(), ours.getCurrentPoint().getX(), 0.0); + ours.closePath(); + real.closePath(); + ours.closePath(); + real.closePath(); + assertEquals(real.getCurrentPoint().getX(), ours.getCurrentPoint().getX(), 0.0); + assertEquals(real.getCurrentPoint().getY(), ours.getCurrentPoint().getY(), 0.0); + ours.moveTo(20, 20); + real.moveTo(20, 20); + ours.lineTo(30, 25); + real.lineTo(30, 25); + // connected: the leading move becomes a line, or vanishes when the + // open subpath already ends there + ours.append(new Line2D.Double(30, 25, 40, 20), true); + real.append(new java.awt.geom.Line2D.Double(30, 25, 40, 20), true); + ours.append(new Rectangle2D.Double(50, 50, 5, 6), true); + real.append(new java.awt.geom.Rectangle2D.Double(50, 50, 5, 6), true); + ours.append(new Ellipse2D.Double(60, 60, 5, 6).getPathIterator(null), false); + real.append(new java.awt.geom.Ellipse2D.Double(60, 60, 5, 6).getPathIterator(null), false); + assertEquals(real.getCurrentPoint().getX(), ours.getCurrentPoint().getX(), TOL); + assertEquals(real.getCurrentPoint().getY(), ours.getCurrentPoint().getY(), TOL); + assertSameOutline("built path", ours, real, TOL); + assertSameRect("built bounds", real.getBounds2D(), ours.getBounds2D(), TOL); + + Shape oursShape = ours.createTransformedShape(new AffineTransform(0, 2, -2, 0, 5, 5)); + java.awt.Shape realShape = real.createTransformedShape(new java.awt.geom.AffineTransform(0, 2, -2, 0, 5, 5)); + assertTrue(oursShape instanceof Path2D.Double); + assertSameOutline("transformed copy", oursShape, realShape, TOL); + assertSameOutline("original untouched", ours, real, TOL); + + Path2D oursFloat = new Path2D.Float(ours, new AffineTransform(1.1, 0, 0, 0.9, 0.3, 0.7)); + java.awt.geom.Path2D realFloat = + new java.awt.geom.Path2D.Float(real, new java.awt.geom.AffineTransform(1.1, 0, 0, 0.9, 0.3, 0.7)); + assertSameOutline("float copy", oursFloat, realFloat, FLOAT_TOL); + assertTrue(oursFloat.getBounds2D() instanceof Rectangle2D.Float); + assertTrue(oursFloat.getCurrentPoint() instanceof Point2D.Float); + assertTrue(oursFloat.clone() instanceof Path2D.Float); + assertTrue(new GeneralPath(ours).clone() instanceof GeneralPath); + + ours.transform(new AffineTransform(2, 0, 0, 2, 1, 1)); + real.transform(new java.awt.geom.AffineTransform(2, 0, 0, 2, 1, 1)); + assertSameOutline("transformed in place", ours, real, TOL); + + Path2D fromShape = new Path2D.Double(new Arc2D.Double(1, 2, 10, 6, 30, 200, Arc2D.PIE)); + java.awt.geom.Path2D realFromShape = new java.awt.geom.Path2D.Double( + new java.awt.geom.Arc2D.Double(1, 2, 10, 6, 30, 200, java.awt.geom.Arc2D.PIE)); + assertSameOutline("from a shape", fromShape, realFromShape, TOL); + + ours.reset(); + assertNull(ours.getCurrentPoint()); + assertTrue(ours.getPathIterator(null).isDone()); + ours.setWindingRule(Path2D.WIND_EVEN_ODD); + assertEquals(Path2D.WIND_EVEN_ODD, ours.getWindingRule()); + try { + ours.setWindingRule(5); + fail("an unknown winding rule must be refused"); + } catch (IllegalArgumentException expected) { + assertEquals(Path2D.WIND_EVEN_ODD, ours.getWindingRule()); + } + } + + @Test + public void curvedShapesAgreeWithTheJdkAwayFromTheirEdges() { + // curves are hit-tested over a flattened outline, so points within a + // whisker of the edge are left out of the comparison + QuadCurve2D quad = new QuadCurve2D.Double(5, 5, 60, 90, 95, 10); + java.awt.geom.QuadCurve2D realQuad = new java.awt.geom.QuadCurve2D.Double(5, 5, 60, 90, 95, 10); + CubicCurve2D cubic = new CubicCurve2D.Double(5, 50, 30, -40, 70, 140, 95, 50); + java.awt.geom.CubicCurve2D realCubic = new java.awt.geom.CubicCurve2D.Double(5, 50, 30, -40, 70, 140, 95, 50); + Path2D blob = new Path2D.Double(); + java.awt.geom.Path2D realBlob = new java.awt.geom.Path2D.Double(); + blob.moveTo(10, 50); + realBlob.moveTo(10, 50); + blob.curveTo(10, 0, 90, 0, 90, 50); + realBlob.curveTo(10, 0, 90, 0, 90, 50); + blob.quadTo(50, 120, 10, 50); + realBlob.quadTo(50, 120, 10, 50); + Shape[] ours = {quad, cubic, blob, new Arc2D.Double(5, 5, 90, 70, 20, 250, Arc2D.PIE), + new Arc2D.Double(5, 5, 90, 70, 20, 250, Arc2D.CHORD), new Arc2D.Double(5, 5, 90, 70, -30, 100, Arc2D.OPEN)}; + java.awt.Shape[] real = {realQuad, realCubic, realBlob, + new java.awt.geom.Arc2D.Double(5, 5, 90, 70, 20, 250, java.awt.geom.Arc2D.PIE), + new java.awt.geom.Arc2D.Double(5, 5, 90, 70, 20, 250, java.awt.geom.Arc2D.CHORD), + new java.awt.geom.Arc2D.Double(5, 5, 90, 70, -30, 100, java.awt.geom.Arc2D.OPEN)}; + for (int s = 0; s < ours.length; s++) { + int inside = 0; + int hits = 0; + for (double y = -3.37; y <= 104; y += 2.71) { + for (double x = -4.11; x <= 104; x += 3.13) { + if (nearOutline(real[s], x, y, 0.05)) { + continue; + } + boolean expected = real[s].contains(x, y); + assertEquals("shape " + s + " contains " + x + "," + y, expected, ours[s].contains(x, y)); + inside += expected ? 1 : 0; + } + } + for (double y = -3.37; y <= 104; y += 9.71) { + for (double x = -4.11; x <= 104; x += 8.13) { + for (double size = 2.3; size < 60; size *= 2.9) { + if (outlineNearRect(real[s], x, y, size, size, 0.05)) { + continue; + } + boolean expected = real[s].intersects(x, y, size, size); + assertEquals("shape " + s + " intersects " + x + "," + y + " size " + size, expected, + ours[s].intersects(x, y, size, size)); + assertEquals("shape " + s + " contains " + x + "," + y + " size " + size, + real[s].contains(x, y, size, size), ours[s].contains(x, y, size, size)); + hits += expected ? 1 : 0; + } + } + } + assertTrue("shape " + s + " inside " + inside + " hits " + hits, inside > 20 && hits > 5); + } + assertSameRect("quad bounds", realQuad.getBounds2D(), quad.getBounds2D(), TOL); + assertSameRect("cubic bounds", realCubic.getBounds2D(), cubic.getBounds2D(), TOL); + assertEquals(realQuad.getFlatness(), quad.getFlatness(), TOL); + assertEquals(realCubic.getFlatness(), cubic.getFlatness(), TOL); + assertEquals(realCubic.getFlatnessSq(), cubic.getFlatnessSq(), 1e-7); + + QuadCurve2D ql = new QuadCurve2D.Double(); + QuadCurve2D qr = new QuadCurve2D.Float(); + java.awt.geom.QuadCurve2D rql = new java.awt.geom.QuadCurve2D.Double(); + java.awt.geom.QuadCurve2D rqr = new java.awt.geom.QuadCurve2D.Float(); + quad.subdivide(ql, qr); + realQuad.subdivide(rql, rqr); + assertSameOutline("quad left", ql, rql, TOL); + assertSameOutline("quad right", qr, rqr, FLOAT_TOL); + CubicCurve2D cl = new CubicCurve2D.Double(); + CubicCurve2D cr = new CubicCurve2D.Float(); + java.awt.geom.CubicCurve2D rcl = new java.awt.geom.CubicCurve2D.Double(); + java.awt.geom.CubicCurve2D rcr = new java.awt.geom.CubicCurve2D.Float(); + cubic.subdivide(cl, cr); + realCubic.subdivide(rcl, rcr); + assertSameOutline("cubic left", cl, rcl, TOL); + assertSameOutline("cubic right", cr, rcr, FLOAT_TOL); + + double[][] equations = {{2, -3, 1}, {1, 2, 1}, {5, 0, 1}, {4, 2, 0}, {3, 0, 0}, {-6, 1, 1}, {1e-3, -200, 1}, + {0, 0, 1}, {0, 3, 1}, {0, 0, 0}, {4, 0, -1}}; + for (double[] eqn : equations) { + double[] a = new double[2]; + double[] b = new double[2]; + int expected = java.awt.geom.QuadCurve2D.solveQuadratic(eqn, b); + assertEquals("root count", expected, QuadCurve2D.solveQuadratic(eqn, a)); + if (expected == 2) { + java.util.Arrays.sort(a); + java.util.Arrays.sort(b); + } + for (int i = 0; i < expected; i++) { + assertEquals("root " + i, b[i], a[i], 1e-9 * Math.max(1.0, Math.abs(b[i]))); + } + } + } + + /// Whether a point is within `margin` of the JDK's flattened outline of a + /// shape, closing lines included. + private static boolean nearOutline(java.awt.Shape s, double x, double y, double margin) { + return outlineNearRect(s, x, y, 0, 0, margin); + } + + /// Whether the outline of a shape comes within `margin` of the border of + /// a rectangle, which is where a flattened hit test may differ. + private static boolean outlineNearRect(java.awt.Shape s, double x, double y, double w, double h, double margin) { + java.awt.geom.PathIterator pi = s.getPathIterator(null, 0.001); + double[] c = new double[6]; + double sx = 0; + double sy = 0; + double cx = 0; + double cy = 0; + double[][] border = {{x, y, x + w, y}, {x + w, y, x + w, y + h}, {x + w, y + h, x, y + h}, {x, y + h, x, y}}; + while (true) { + double nx; + double ny; + boolean edge = true; + if (pi.isDone()) { + nx = sx; + ny = sy; + } else { + int type = pi.currentSegment(c); + if (type == java.awt.geom.PathIterator.SEG_MOVETO) { + // the open subpath is closed back to its start first + for (double[] b : border) { + if (segmentsWithin(cx, cy, sx, sy, b, margin)) { + return true; + } + } + sx = c[0]; + sy = c[1]; + nx = sx; + ny = sy; + edge = false; + } else if (type == java.awt.geom.PathIterator.SEG_CLOSE) { + nx = sx; + ny = sy; + } else { + nx = c[0]; + ny = c[1]; + } + } + if (edge) { + for (double[] b : border) { + if (segmentsWithin(cx, cy, nx, ny, b, margin)) { + return true; + } + } + } + cx = nx; + cy = ny; + if (pi.isDone()) { + return false; + } + pi.next(); + } + } + + private static boolean segmentsWithin(double x1, double y1, double x2, double y2, double[] b, double margin) { + if (x1 == x2 && y1 == y2) { + return false; + } + if (java.awt.geom.Line2D.linesIntersect(x1, y1, x2, y2, b[0], b[1], b[2], b[3])) { + return true; + } + return java.awt.geom.Line2D.ptSegDist(x1, y1, x2, y2, b[0], b[1]) <= margin + || java.awt.geom.Line2D.ptSegDist(x1, y1, x2, y2, b[2], b[3]) <= margin + || java.awt.geom.Line2D.ptSegDist(b[0], b[1], b[2], b[3], x1, y1) <= margin + || java.awt.geom.Line2D.ptSegDist(b[0], b[1], b[2], b[3], x2, y2) <= margin; + } + + @Test + public void rectangularShapesAgainstRectanglesMatchTheJdk() { + double[] sizes = {0, 0.4, 2.7, 9.3, 30}; + int hits = 0; + for (double w : sizes) { + for (double h : sizes) { + for (double y = -4.3; y <= 14; y += 1.9) { + for (double x = -3.7; x <= 16; x += 2.3) { + String label = "rect " + x + "," + y + "," + w + "," + h; + Rectangle2D q = new Rectangle2D.Double(x, y, w, h); + java.awt.geom.Rectangle2D rq = new java.awt.geom.Rectangle2D.Double(x, y, w, h); + + Rectangle2D rect = new Rectangle2D.Double(1, 2, 10, 6); + java.awt.geom.Rectangle2D realRect = new java.awt.geom.Rectangle2D.Double(1, 2, 10, 6); + assertEquals(label, realRect.intersects(rq), rect.intersects(q)); + assertEquals(label, realRect.contains(rq), rect.contains(q)); + assertEquals(label, realRect.intersectsLine(x, y, x + w, y + h), + rect.intersectsLine(x, y, x + w, y + h)); + assertEquals(label, realRect.intersectsLine(x + w, y, x, y + h), + rect.intersectsLine(new Line2D.Double(x + w, y, x, y + h))); + assertEquals(label, realRect.outcode(x, y), rect.outcode(x, y)); + assertEquals(label, realRect.outcode(x + w, y + h), rect.outcode(new Point2D.Double(x + w, y + h))); + assertSameRect(label + " intersection", realRect.createIntersection(rq), + rect.createIntersection(q), TOL); + assertSameRect(label + " union", realRect.createUnion(rq), rect.createUnion(q), TOL); + Rectangle2D sum = new Rectangle2D.Float(1, 2, 10, 6); + java.awt.geom.Rectangle2D realSum = new java.awt.geom.Rectangle2D.Float(1, 2, 10, 6); + sum.add(q); + realSum.add(rq); + assertSameRect(label + " add rect", realSum, sum, FLOAT_TOL); + sum.add(x - 3, y + 20); + realSum.add(x - 3, y + 20); + assertSameRect(label + " add point", realSum, sum, FLOAT_TOL); + assertEquals(label, new java.awt.geom.Line2D.Double(x, y, x + w, y + h).intersects(realRect), + new Line2D.Double(x, y, x + w, y + h).intersects(rect)); + + Ellipse2D ellipse = new Ellipse2D.Double(1, 2, 10, 6); + java.awt.geom.Ellipse2D realEllipse = new java.awt.geom.Ellipse2D.Double(1, 2, 10, 6); + assertEquals(label + " ellipse intersects", realEllipse.intersects(rq), ellipse.intersects(q)); + assertEquals(label + " ellipse contains", realEllipse.contains(rq), ellipse.contains(q)); + + RoundRectangle2D round = new RoundRectangle2D.Double(1, 2, 10, 6, 4, 3); + java.awt.geom.RoundRectangle2D realRound = + new java.awt.geom.RoundRectangle2D.Double(1, 2, 10, 6, 4, 3); + assertEquals(label + " round intersects", realRound.intersects(rq), round.intersects(q)); + assertEquals(label + " round contains", realRound.contains(rq), round.contains(q)); + + for (int closure : CLOSURES) { + Arc2D arc = new Arc2D.Double(1, 2, 10, 6, 30, 250, closure); + java.awt.geom.Arc2D realArc = new java.awt.geom.Arc2D.Double(1, 2, 10, 6, 30, 250, closure); + assertEquals(label + " arc contains, closure " + closure, realArc.contains(rq), + arc.contains(q)); + } + hits += realRect.intersects(rq) ? 1 : 0; + } + } + } + } + assertTrue("grid must overlap the shapes: " + hits, hits > 100); + assertEquals(java.awt.geom.Rectangle2D.OUT_LEFT, Rectangle2D.OUT_LEFT); + assertEquals(java.awt.geom.Rectangle2D.OUT_TOP, Rectangle2D.OUT_TOP); + assertEquals(java.awt.geom.Rectangle2D.OUT_RIGHT, Rectangle2D.OUT_RIGHT); + assertEquals(java.awt.geom.Rectangle2D.OUT_BOTTOM, Rectangle2D.OUT_BOTTOM); + assertEquals(new java.awt.geom.Rectangle2D.Double(1, 2, 0, 4).outcode(0, 0), + new Rectangle2D.Double(1, 2, 0, 4).outcode(0, 0)); + + Rectangle2D frame = new Rectangle2D.Double(); + java.awt.geom.Rectangle2D realFrame = new java.awt.geom.Rectangle2D.Double(); + frame.setFrameFromDiagonal(9, 8, 2, 3); + realFrame.setFrameFromDiagonal(9, 8, 2, 3); + assertSameRect("diagonal", realFrame, frame, TOL); + frame.setFrameFromCenter(5, 5, 2, 9); + realFrame.setFrameFromCenter(5, 5, 2, 9); + assertSameRect("centre", realFrame, frame, TOL); + assertEquals(realFrame.getCenterX(), frame.getCenterX(), TOL); + assertEquals(realFrame.getMaxY(), frame.getMaxY(), TOL); + Rectangle2D fractional = new Rectangle2D.Double(-1.5, 2.25, 3.75, 4.5); + java.awt.Rectangle realBounds = new java.awt.geom.Rectangle2D.Double(-1.5, 2.25, 3.75, 4.5).getBounds(); + assertEquals(realBounds.x, fractional.getBounds().x); + assertEquals(realBounds.y, fractional.getBounds().y); + assertEquals(realBounds.width, fractional.getBounds().width); + assertEquals(realBounds.height, fractional.getBounds().height); + assertEquals(new Rectangle2D.Double(1, 2, 3, 4), new Rectangle2D.Float(1, 2, 3, 4)); + assertEquals(new Rectangle2D.Double(1, 2, 3, 4).hashCode(), new Rectangle2D.Float(1, 2, 3, 4).hashCode()); + assertTrue(new Rectangle2D.Float(1, 2, 3, 4).clone() instanceof Rectangle2D.Float); + assertTrue(new Ellipse2D.Float(1, 2, 3, 4).clone() instanceof Ellipse2D.Float); + assertEquals(new Ellipse2D.Double(1, 2, 3, 4), new Ellipse2D.Double(1, 2, 3, 4).clone()); + assertEquals(new RoundRectangle2D.Double(1, 2, 3, 4, 1, 1), new RoundRectangle2D.Double(1, 2, 3, 4, 1, 1).clone()); + assertEquals(new Arc2D.Double(1, 2, 3, 4, 5, 6, Arc2D.PIE), new Arc2D.Double(1, 2, 3, 4, 5, 6, Arc2D.PIE).clone()); + assertFalse(new Arc2D.Double(1, 2, 3, 4, 5, 6, Arc2D.PIE).equals(new Arc2D.Double(1, 2, 3, 4, 5, 6, Arc2D.CHORD))); + } + + // ---------------------------------------------------------------- integer rectangle + + private static final int[][] INT_RECTS = { + {0, 0, 10, 10}, {5, 5, 10, 10}, {10, 0, 5, 5}, {2, 2, 3, 3}, {-5, -5, 4, 4}, {20, 20, 1, 1}, + {3, 3, 0, 0}, {3, 3, 0, 5}, {4, 4, -1, -1}, {0, 0, -3, 5}, {1, 1, 6, -2},{0, 0, 10, 0}, {9, 9, 1, 1}, + {Integer.MAX_VALUE - 5, 0, 10, 10}, {Integer.MIN_VALUE + 2, 3, 5, 5}, + }; + + private static void assertSameRect(String label, java.awt.Rectangle real, Rectangle ours) { + assertEquals(label + " x", real.x, ours.x); + assertEquals(label + " y", real.y, ours.y); + assertEquals(label + " width", real.width, ours.width); + assertEquals(label + " height", real.height, ours.height); + } + + @Test + public void integerRectangleMatchesTheJdk() { + for (int[] a : INT_RECTS) { + for (int[] b : INT_RECTS) { + String label = "[" + a[0] + "," + a[1] + "," + a[2] + "," + a[3] + "] with [" + + b[0] + "," + b[1] + "," + b[2] + "," + b[3] + "]"; + Rectangle ours = new Rectangle(a[0], a[1], a[2], a[3]); + Rectangle other = new Rectangle(b[0], b[1], b[2], b[3]); + java.awt.Rectangle real = new java.awt.Rectangle(a[0], a[1], a[2], a[3]); + java.awt.Rectangle realOther = new java.awt.Rectangle(b[0], b[1], b[2], b[3]); + assertEquals(label + " intersects", real.intersects(realOther), ours.intersects(other)); + assertEquals(label + " contains", real.contains(realOther), ours.contains(other)); + assertEquals(label + " contains point", real.contains(b[0], b[1]), ours.contains(b[0], b[1])); + assertEquals(label + " contains Point", real.contains(new java.awt.Point(b[0] + b[2], b[1])), + ours.contains(new Point(b[0] + b[2], b[1]))); + assertEquals(label + " isEmpty", real.isEmpty(), ours.isEmpty()); + assertEquals(label + " outcode", real.outcode(b[0], b[1]), ours.outcode(b[0], b[1])); + assertEquals(label + " equals", real.equals(realOther), ours.equals(other)); + if (Math.abs((long) a[0]) < 1000000 && Math.abs((long) b[0]) < 1000000) { + // the far-off rows are there for the tests above; what a + // sum beyond the int range becomes is not compared + assertSameRect(label + " intersection", real.intersection(realOther), ours.intersection(other)); + assertSameRect(label + " union", real.union(realOther), ours.union(other)); + Rectangle added = new Rectangle(ours); + java.awt.Rectangle realAdded = new java.awt.Rectangle(real); + added.add(other); + realAdded.add(realOther); + assertSameRect(label + " add", realAdded, added); + added.add(b[0], b[1]); + realAdded.add(b[0], b[1]); + assertSameRect(label + " add point", realAdded, added); + added.grow(b[2], -b[3]); + realAdded.grow(b[2], -b[3]); + assertSameRect(label + " grow", realAdded, added); + added.translate(b[0], b[1]); + realAdded.translate(b[0], b[1]); + assertSameRect(label + " translate", realAdded, added); + java.awt.geom.Rectangle2D realCross = real.createIntersection(realOther); + Rectangle2D cross = ours.createIntersection(other); + assertTrue(cross instanceof Rectangle); + assertSameRect(label + " createIntersection", realCross, cross, 0.0); + assertSameRect(label + " createUnion", real.createUnion(realOther), ours.createUnion(other), 0.0); + assertSameRect(label + " mixed intersection", + real.createIntersection(new java.awt.geom.Rectangle2D.Double(b[0] + 0.5, b[1], b[2], b[3])), + ours.createIntersection(new Rectangle2D.Double(b[0] + 0.5, b[1], b[2], b[3])), TOL); + assertSameRect(label + " mixed union", + real.createUnion(new java.awt.geom.Rectangle2D.Double(b[0] + 0.5, b[1], b[2], b[3])), + ours.createUnion(new Rectangle2D.Double(b[0] + 0.5, b[1], b[2], b[3])), TOL); + } + } + } + double[][] fractional = {{1.5, 2.5, 3.25, 4.75}, {-1.5, -2.25, 3, 4}, {0.2, 0.2, 0.2, 0.2}, {3, 4, 5, 6}, + {-0.5, 7.9, 0, 0}, {2.5, 2.5, -1.5, 3}}; + for (double[] f : fractional) { + Rectangle ours = new Rectangle(); + java.awt.Rectangle real = new java.awt.Rectangle(); + ours.setRect(f[0], f[1], f[2], f[3]); + real.setRect(f[0], f[1], f[2], f[3]); + assertSameRect("setRect " + f[0] + "," + f[1] + "," + f[2] + "," + f[3], real, ours); + ours.setFrame(f[0], f[1], f[2], f[3]); + assertSameRect("setFrame", real, ours); + } + Rectangle r = new Rectangle(new Point(1, 2), new Dimension(3, 4)); + assertEquals(new java.awt.Rectangle(1, 2, 3, 4).toString(), r.toString()); + assertEquals(new Rectangle(1, 2, 3, 4), r); + assertEquals(new Rectangle(1, 2, 3, 4).hashCode(), r.hashCode()); + assertEquals(r, new Rectangle2D.Double(1, 2, 3, 4)); + assertEquals(new Point(1, 2), r.getLocation()); + assertEquals(new Dimension(3, 4), r.getSize()); + assertTrue(r.clone() instanceof Rectangle); + assertTrue(r.getBounds2D() instanceof Rectangle); + r.setBounds(new Rectangle(5, 6, 7, 8)); + r.setLocation(new Point(9, 9)); + r.setSize(new Dimension(1, 1)); + assertEquals(new Rectangle(9, 9, 1, 1), r); + assertEquals(new Rectangle(new Dimension(4, 5)), new Rectangle(4, 5)); + assertEquals(new Rectangle(new Point(4, 5)), new Rectangle(4, 5, 0, 0)); + } + + @Test + public void pointDimensionAndInsetsBehaveLikeTheJdk() { + Point p = new Point(3, 4); + assertEquals(new java.awt.Point(3, 4).toString(), p.toString()); + p.translate(2, -1); + assertEquals(new Point(5, 3), p); + double[][] rounding = {{1.5, -1.5}, {2.4, -2.6}, {-0.5, 0.49}}; + for (double[] v : rounding) { + java.awt.Point real = new java.awt.Point(); + real.setLocation(v[0], v[1]); + p.setLocation(v[0], v[1]); + assertEquals(real.x, p.x); + assertEquals(real.y, p.y); + } + p.move(7, 8); + assertEquals(new Point2D.Double(7, 8), p); + assertEquals(new Point2D.Double(7, 8).hashCode(), p.hashCode()); + assertTrue(p.clone() instanceof Point); + assertTrue(new Point2D.Float(1, 2).clone() instanceof Point2D.Float); + assertEquals(new Point(p), p.getLocation()); + assertEquals(java.awt.geom.Point2D.distance(1, 2, 4, 6), Point2D.distance(1, 2, 4, 6), TOL); + assertEquals(25.0, new Point2D.Double(1, 2).distanceSq(new Point2D.Float(4, 6)), TOL); + assertEquals(5.0, new Point2D.Double(1, 2).distance(4, 6), TOL); + assertEquals(new java.awt.geom.Point2D.Double(1, 2).toString(), new Point2D.Double(1, 2).toString()); + assertEquals(new java.awt.geom.Point2D.Float(1, 2).toString(), new Point2D.Float(1, 2).toString()); + + Dimension d = new Dimension(3, 4); + assertEquals(new java.awt.Dimension(3, 4).toString(), d.toString()); + java.awt.Dimension realD = new java.awt.Dimension(); + realD.setSize(2.1, -2.1); + d.setSize(2.1, -2.1); + assertEquals(realD.width, d.width); + assertEquals(realD.height, d.height); + d.setSize(new Dimension(6, 7)); + assertEquals(new Dimension(6, 7), d.getSize()); + assertEquals(new Dimension(6, 7).hashCode(), d.hashCode()); + assertTrue(d.clone() instanceof Dimension); + Dimension2D d2 = new Dimension(); + d2.setSize(d); + assertEquals(d, d2); + + Insets i = new Insets(1, 2, 3, 4); + assertEquals(new java.awt.Insets(1, 2, 3, 4).toString(), i.toString()); + Object copy = i.clone(); + assertEquals(i, copy); + assertEquals(i.hashCode(), copy.hashCode()); + i.set(5, 6, 7, 8); + assertEquals(new Insets(5, 6, 7, 8), i); + assertFalse(i.equals(copy)); + } + + // ---------------------------------------------------------------- lines + + @Test + public void lineFunctionsMatchTheJdk() { + double[] v = {-3, 0, 1, 2.5, 4, 7}; + int crossings = 0; + for (double x1 : v) { + for (double y1 : v) { + for (double x2 : v) { + for (double y2 : new double[] {0, 2.5, 7}) { + for (double px : v) { + for (double py : new double[] {-3, 1, 2.5, 4}) { + String label = x1 + "," + y1 + "-" + x2 + "," + y2 + " point " + px + "," + py; + assertEquals(label + " relativeCCW", + java.awt.geom.Line2D.relativeCCW(x1, y1, x2, y2, px, py), + Line2D.relativeCCW(x1, y1, x2, y2, px, py)); + assertEquals(label + " ptSegDist", + java.awt.geom.Line2D.ptSegDist(x1, y1, x2, y2, px, py), + Line2D.ptSegDist(x1, y1, x2, y2, px, py), 1e-7); + assertEquals(label + " ptSegDistSq", + java.awt.geom.Line2D.ptSegDistSq(x1, y1, x2, y2, px, py), + Line2D.ptSegDistSq(x1, y1, x2, y2, px, py), TOL * 100); + if (x1 != x2 || y1 != y2) { + assertEquals(label + " ptLineDist", + java.awt.geom.Line2D.ptLineDist(x1, y1, x2, y2, px, py), + Line2D.ptLineDist(x1, y1, x2, y2, px, py), 1e-7); + } + boolean expected = java.awt.geom.Line2D.linesIntersect(x1, y1, x2, y2, px, py, 4, 1); + assertEquals(label + " linesIntersect", expected, + Line2D.linesIntersect(x1, y1, x2, y2, px, py, 4, 1)); + crossings += expected ? 1 : 0; + } + } + } + } + } + } + assertTrue("some segments must cross: " + crossings, crossings > 100); + Line2D line = new Line2D.Double(new Point2D.Double(0, 0), new Point2D.Double(10, 5)); + java.awt.geom.Line2D real = new java.awt.geom.Line2D.Double(0, 0, 10, 5); + assertEquals(real.relativeCCW(3, 9), line.relativeCCW(new Point2D.Double(3, 9))); + assertEquals(real.ptSegDist(3, 9), line.ptSegDist(new Point2D.Double(3, 9)), TOL); + assertEquals(real.ptLineDistSq(30, 9), line.ptLineDistSq(30, 9), TOL); + assertEquals(real.intersectsLine(0, 5, 10, 0), line.intersectsLine(new Line2D.Float(0, 5, 10, 0))); + assertEquals(real.intersects(2, 0, 3, 3), line.intersects(2, 0, 3, 3)); + assertFalse(line.contains(5, 2.5)); + assertSameRect("line bounds", new java.awt.geom.Line2D.Float(9, 1, 2, 6).getBounds2D(), + new Line2D.Float(9, 1, 2, 6).getBounds2D(), TOL); + assertEquals(real.getBounds().width, line.getBounds().width); + assertTrue(new Line2D.Float(9, 1, 2, 6).clone() instanceof Line2D.Float); + assertEquals(10.0, line.getP2().getX(), 0.0); + } + + // ---------------------------------------------------------------- flattening + + private static double cubicAt(double a, double b, double c, double d, double t) { + double u = 1 - t; + return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d; + } + + @Test + public void flattenedPointsLieOnTheCurveWithinTheFlatness() { + double[][] curves = { + {0, 0, 10, 40, 60, -30, 80, 20}, + {5, 5, 5, 95, 95, 95, 95, 5}, + {0, 0, 100, 0, 0, 50, 100, 50}, + {10, 10, 11, 10, 12, 10, 13, 10}, + }; + double[] flatnesses = {5, 1, 0.1, 0.01}; + for (double[] k : curves) { + // a dense sampling of the true curve to measure against + int samples = 20000; + double[] sx = new double[samples + 1]; + double[] sy = new double[samples + 1]; + for (int i = 0; i <= samples; i++) { + double t = i / (double) samples; + sx[i] = cubicAt(k[0], k[2], k[4], k[6], t); + sy[i] = cubicAt(k[1], k[3], k[5], k[7], t); + } + CubicCurve2D curve = new CubicCurve2D.Double(k[0], k[1], k[2], k[3], k[4], k[5], k[6], k[7]); + int previousCount = 0; + for (double flatness : flatnesses) { + FlatteningPathIterator it = new FlatteningPathIterator(curve.getPathIterator(null), flatness); + assertEquals(flatness, it.getFlatness(), 0.0); + assertEquals(10, it.getRecursionLimit()); + assertEquals(PathIterator.WIND_NON_ZERO, it.getWindingRule()); + double[] c = new double[6]; + assertEquals(PathIterator.SEG_MOVETO, it.currentSegment(c)); + double lastX = c[0]; + double lastY = c[1]; + assertEquals(k[0], lastX, 0.0); + assertEquals(k[1], lastY, 0.0); + it.next(); + int count = 0; + while (!it.isDone()) { + assertEquals(PathIterator.SEG_LINETO, it.currentSegment(c)); + // the end of each line is a point of the curve itself + assertTrue("end point off the curve by " + distanceToSamples(sx, sy, c[0], c[1]), + distanceToSamples(sx, sy, c[0], c[1]) < 0.01); + // and the line stays within the flatness of the curve + for (int s = 1; s < 8; s++) { + double mx = lastX + (c[0] - lastX) * s / 8.0; + double my = lastY + (c[1] - lastY) * s / 8.0; + double off = distanceToSamples(sx, sy, mx, my); + assertTrue("line strays " + off + " at flatness " + flatness, off <= flatness + 0.01); + } + lastX = c[0]; + lastY = c[1]; + count++; + it.next(); + } + assertEquals(k[6], lastX, 0.0); + assertEquals(k[7], lastY, 0.0); + assertTrue("a finer flatness cannot need fewer lines", count >= previousCount); + previousCount = count; + } + } + + // a quadratic and the closing of a subpath come through as well, and + // the limit caps the subdivision + Path2D path = new Path2D.Double(); + path.moveTo(0, 0); + path.quadTo(50, 100, 100, 0); + path.closePath(); + FlatteningPathIterator it = new FlatteningPathIterator(path.getPathIterator(null), 0.0, 3); + int lines = 0; + int closes = 0; + float[] f = new float[6]; + while (!it.isDone()) { + int type = it.currentSegment(f); + assertTrue(type != PathIterator.SEG_QUADTO && type != PathIterator.SEG_CUBICTO); + if (type == PathIterator.SEG_LINETO) { + lines++; + // on the parabola y = x * (100 - x) / 50 + assertEquals(f[0] * (100 - f[0]) / 50.0, f[1], 1e-3); + } else if (type == PathIterator.SEG_CLOSE) { + closes++; + } + it.next(); + } + assertEquals(8, lines); + assertEquals(1, closes); + // every shape's flattening overload answers lines only + Shape[] shapes = {new Ellipse2D.Double(1, 2, 30, 40), new RoundRectangle2D.Double(1, 2, 30, 40, 9, 9), + new Arc2D.Double(1, 2, 30, 40, 10, 200, Arc2D.PIE), new QuadCurve2D.Double(1, 2, 30, 40, 9, 9), path}; + for (Shape s : shapes) { + PathIterator flat = s.getPathIterator(null, 0.25); + int segments = 0; + while (!flat.isDone()) { + int type = flat.currentSegment(f); + assertTrue(type != PathIterator.SEG_QUADTO && type != PathIterator.SEG_CUBICTO); + segments++; + flat.next(); + } + assertTrue(segments > 3); + } + try { + new FlatteningPathIterator(path.getPathIterator(null), -1.0); + fail("a negative flatness must be refused"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().length() > 0); + } + } + + private static double distanceToSamples(double[] sx, double[] sy, double x, double y) { + double best = Double.MAX_VALUE; + for (int i = 1; i < sx.length; i++) { + best = Math.min(best, java.awt.geom.Line2D.ptSegDistSq(sx[i - 1], sy[i - 1], sx[i], sy[i], x, y)); + } + return Math.sqrt(best); + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/java/beans/PropertyChangeSupportTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/java/beans/PropertyChangeSupportTest.java new file mode 100644 index 00000000000..de26622d8d0 --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/java/beans/PropertyChangeSupportTest.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.java.beans; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/// Covers the firing rules of the bound-property support: equal values are +/// not events, named listeners see only their property, and a proxy is +/// registered under the name it carries. +public class PropertyChangeSupportTest { + + private static final class Recorder implements PropertyChangeListener { + final List events = new ArrayList(); + + public void propertyChange(PropertyChangeEvent evt) { + events.add(evt); + } + } + + private Object bean; + + private PropertyChangeSupport support; + + @Before + public void setUp() { + bean = new Object(); + support = new PropertyChangeSupport(bean); + } + + @Test(expected = NullPointerException.class) + public void nullSourceIsRejected() { + new PropertyChangeSupport(null); + } + + @Test + public void unnamedListenerSeesEveryProperty() { + Recorder r = new Recorder(); + support.addPropertyChangeListener(r); + support.firePropertyChange("a", "x", "y"); + support.firePropertyChange("b", 1, 2); + assertEquals(2, r.events.size()); + PropertyChangeEvent e = r.events.get(0); + assertSame(bean, e.getSource()); + assertEquals("a", e.getPropertyName()); + assertEquals("x", e.getOldValue()); + assertEquals("y", e.getNewValue()); + assertEquals(Integer.valueOf(2), r.events.get(1).getNewValue()); + } + + @Test + public void namedListenerSeesOnlyItsProperty() { + Recorder r = new Recorder(); + support.addPropertyChangeListener("a", r); + support.firePropertyChange("a", 1, 2); + support.firePropertyChange("b", 1, 2); + assertEquals(1, r.events.size()); + assertEquals("a", r.events.get(0).getPropertyName()); + } + + @Test + public void noEventWhenOldAndNewAreEqual() { + Recorder r = new Recorder(); + support.addPropertyChangeListener(r); + support.firePropertyChange("a", "same", new String("same")); + support.firePropertyChange("a", 3, 3); + support.firePropertyChange("a", true, true); + support.fireIndexedPropertyChange("a", 0, 5, 5); + assertTrue(r.events.isEmpty()); + } + + @Test + public void nullOldOrNewStillFires() { + Recorder r = new Recorder(); + support.addPropertyChangeListener(r); + support.firePropertyChange("a", null, "v"); + support.firePropertyChange("a", "v", null); + support.firePropertyChange("a", null, null); + assertEquals(3, r.events.size()); + } + + @Test + public void primitiveOverloadsBoxTheirValues() { + Recorder r = new Recorder(); + support.addPropertyChangeListener(r); + support.firePropertyChange("i", 1, 2); + support.firePropertyChange("b", false, true); + assertEquals(Integer.valueOf(1), r.events.get(0).getOldValue()); + assertEquals(Boolean.TRUE, r.events.get(1).getNewValue()); + } + + @Test + public void indexedEventsCarryTheIndex() { + Recorder r = new Recorder(); + support.addPropertyChangeListener(r); + support.fireIndexedPropertyChange("a", 4, "x", "y"); + support.fireIndexedPropertyChange("a", 5, 1, 2); + support.fireIndexedPropertyChange("a", 6, false, true); + assertEquals(3, r.events.size()); + assertEquals(4, ((IndexedPropertyChangeEvent) r.events.get(0)).getIndex()); + assertEquals(5, ((IndexedPropertyChangeEvent) r.events.get(1)).getIndex()); + assertEquals(6, ((IndexedPropertyChangeEvent) r.events.get(2)).getIndex()); + } + + @Test + public void commonListenersFireBeforeNamedOnes() { + final List order = new ArrayList(); + support.addPropertyChangeListener("a", new PropertyChangeListener() { + public void propertyChange(PropertyChangeEvent evt) { + order.add("named"); + } + }); + support.addPropertyChangeListener(new PropertyChangeListener() { + public void propertyChange(PropertyChangeEvent evt) { + order.add("common"); + } + }); + support.firePropertyChange("a", 1, 2); + assertEquals("[common, named]", order.toString()); + } + + @Test + public void proxyRegistersUnderItsName() { + Recorder r = new Recorder(); + support.addPropertyChangeListener(new PropertyChangeListenerProxy("a", r)); + assertEquals(0, support.getPropertyChangeListeners("b").length); + assertEquals(1, support.getPropertyChangeListeners("a").length); + assertSame(r, support.getPropertyChangeListeners("a")[0]); + support.firePropertyChange("a", 1, 2); + support.firePropertyChange("b", 1, 2); + assertEquals(1, r.events.size()); + } + + @Test + public void allListenersIncludeProxiesForNamedOnes() { + Recorder common = new Recorder(); + Recorder named = new Recorder(); + support.addPropertyChangeListener(common); + support.addPropertyChangeListener("a", named); + PropertyChangeListener[] all = support.getPropertyChangeListeners(); + assertEquals(2, all.length); + assertSame(common, all[0]); + assertTrue(all[1] instanceof PropertyChangeListenerProxy); + PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) all[1]; + assertEquals("a", proxy.getPropertyName()); + assertSame(named, proxy.getListener()); + } + + @Test + public void removeWorksWithAndWithoutAName() { + Recorder common = new Recorder(); + Recorder named = new Recorder(); + support.addPropertyChangeListener(common); + support.addPropertyChangeListener("a", named); + support.removePropertyChangeListener(common); + support.removePropertyChangeListener("a", named); + support.firePropertyChange("a", 1, 2); + assertTrue(common.events.isEmpty()); + assertTrue(named.events.isEmpty()); + assertEquals(0, support.getPropertyChangeListeners().length); + } + + @Test + public void removingAProxyRemovesTheNamedListener() { + Recorder r = new Recorder(); + support.addPropertyChangeListener("a", r); + support.removePropertyChangeListener(new PropertyChangeListenerProxy("a", r)); + support.firePropertyChange("a", 1, 2); + assertTrue(r.events.isEmpty()); + } + + @Test + public void hasListenersReflectsCommonAndNamed() { + assertFalse(support.hasListeners("a")); + Recorder named = new Recorder(); + support.addPropertyChangeListener("a", named); + assertTrue(support.hasListeners("a")); + assertFalse(support.hasListeners("b")); + assertFalse(support.hasListeners(null)); + support.addPropertyChangeListener(new Recorder()); + assertTrue(support.hasListeners("b")); + assertTrue(support.hasListeners(null)); + } + + @Test + public void listenerMayRemoveItselfWhileFiring() { + final Recorder after = new Recorder(); + support.addPropertyChangeListener(new PropertyChangeListener() { + public void propertyChange(PropertyChangeEvent evt) { + support.removePropertyChangeListener(this); + } + }); + support.addPropertyChangeListener(after); + support.firePropertyChange("a", 1, 2); + support.firePropertyChange("a", 2, 3); + assertEquals(2, after.events.size()); + } + + @Test + public void fireEventDeliversTheSameInstance() { + Recorder r = new Recorder(); + support.addPropertyChangeListener(r); + PropertyChangeEvent e = new PropertyChangeEvent(bean, "p", 1, 2); + support.firePropertyChange(e); + assertSame(e, r.events.get(0)); + } + + @Test + public void nullListenersAndNamesAreIgnored() { + support.addPropertyChangeListener((PropertyChangeListener) null); + support.addPropertyChangeListener("a", null); + support.addPropertyChangeListener(null, new Recorder()); + assertEquals(0, support.getPropertyChangeListeners().length); + assertEquals(0, support.getPropertyChangeListeners("a").length); + assertEquals(0, support.getPropertyChangeListeners(null).length); + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/javax/swing/DefaultButtonModelTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/javax/swing/DefaultButtonModelTest.java new file mode 100644 index 00000000000..a7e0abd932a --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/javax/swing/DefaultButtonModelTest.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.javax.swing; + +import java.util.ArrayList; +import java.util.List; + +import com.codename1.desktopcompat.java.awt.event.ActionEvent; +import com.codename1.desktopcompat.java.awt.event.ActionListener; +import com.codename1.desktopcompat.java.awt.event.ItemEvent; +import com.codename1.desktopcompat.java.awt.event.ItemListener; +import com.codename1.desktopcompat.javax.swing.event.ChangeEvent; +import com.codename1.desktopcompat.javax.swing.event.ChangeListener; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/// Covers the state machine of the button model: the state bits, the events +/// each change fires, and the exclusivity a `ButtonGroup` imposes on the +/// models it holds. +public class DefaultButtonModelTest { + + private DefaultButtonModel model; + + private int changes; + + private final List actions = new ArrayList(); + + private final List items = new ArrayList(); + + @Before + public void setUp() { + model = new DefaultButtonModel(); + listen(model); + } + + private void listen(DefaultButtonModel m) { + m.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + changes++; + } + }); + m.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + actions.add(e); + } + }); + m.addItemListener(new ItemListener() { + public void itemStateChanged(ItemEvent e) { + items.add(e); + } + }); + } + + @Test + public void startsEnabledAndNothingElse() { + DefaultButtonModel fresh = new DefaultButtonModel(); + assertTrue(fresh.isEnabled()); + assertFalse(fresh.isArmed()); + assertFalse(fresh.isSelected()); + assertFalse(fresh.isPressed()); + assertFalse(fresh.isRollover()); + assertEquals(DefaultButtonModel.ENABLED, fresh.stateMask); + assertNull(fresh.getSelectedObjects()); + } + + @Test + public void constantsAreDistinctBits() { + assertEquals(1, DefaultButtonModel.ARMED); + assertEquals(2, DefaultButtonModel.SELECTED); + assertEquals(4, DefaultButtonModel.PRESSED); + assertEquals(8, DefaultButtonModel.ENABLED); + assertEquals(16, DefaultButtonModel.ROLLOVER); + } + + @Test + public void stateChangesFireChangeEventsOnlyWhenTheStateMoves() { + model.setArmed(true); + assertEquals(1, changes); + model.setArmed(true); + assertEquals(1, changes); + model.setRollover(true); + assertEquals(2, changes); + model.setMnemonic('A'); + assertEquals(3, changes); + assertEquals('A', model.getMnemonic()); + } + + @Test + public void releasingWhileArmedFiresOneActionEvent() { + model.setActionCommand("go"); + model.setArmed(true); + model.setPressed(true); + assertTrue(actions.isEmpty()); + model.setPressed(false); + assertEquals(1, actions.size()); + ActionEvent e = actions.get(0); + assertEquals("go", e.getActionCommand()); + assertEquals(ActionEvent.ACTION_PERFORMED, e.getID()); + assertSame(model, e.getSource()); + } + + @Test + public void releasingWhileNotArmedFiresNoAction() { + model.setPressed(true); + model.setPressed(false); + assertTrue(actions.isEmpty()); + } + + @Test + public void disabledModelIgnoresArmPressAndRollover() { + model.setEnabled(false); + int before = changes; + model.setArmed(true); + model.setPressed(true); + model.setRollover(true); + assertEquals(before, changes); + assertFalse(model.isArmed()); + assertFalse(model.isPressed()); + assertFalse(model.isRollover()); + } + + @Test + public void disablingClearsArmedAndPressed() { + model.setArmed(true); + model.setPressed(true); + model.setEnabled(false); + assertFalse(model.isArmed()); + assertFalse(model.isPressed()); + assertFalse(model.isEnabled()); + } + + @Test + public void selectionFiresItemThenChangeEvents() { + model.setSelected(true); + assertTrue(model.isSelected()); + assertEquals(1, items.size()); + assertEquals(ItemEvent.SELECTED, items.get(0).getStateChange()); + assertSame(model, items.get(0).getItem()); + assertSame(model, items.get(0).getItemSelectable()); + assertEquals(1, changes); + model.setSelected(true); + assertEquals(1, items.size()); + model.setSelected(false); + assertEquals(ItemEvent.DESELECTED, items.get(1).getStateChange()); + assertEquals(2, changes); + } + + @Test + public void listenersCanBeRemovedAndListed() { + assertEquals(1, model.getChangeListeners().length); + assertEquals(1, model.getActionListeners().length); + assertEquals(1, model.getItemListeners().length); + model.removeChangeListener(model.getChangeListeners()[0]); + model.removeActionListener(model.getActionListeners()[0]); + model.removeItemListener(model.getItemListeners()[0]); + model.setSelected(true); + assertEquals(0, changes); + assertTrue(items.isEmpty()); + } + + @Test + public void groupKeepsOneModelSelected() { + ButtonGroup group = new ButtonGroup(); + DefaultButtonModel a = new DefaultButtonModel(); + DefaultButtonModel b = new DefaultButtonModel(); + a.setGroup(group); + b.setGroup(group); + assertSame(group, a.getGroup()); + a.setSelected(true); + assertTrue(a.isSelected()); + assertSame(a, group.getSelection()); + b.setSelected(true); + assertTrue(b.isSelected()); + assertFalse(a.isSelected()); + assertSame(b, group.getSelection()); + assertTrue(group.isSelected(b)); + assertFalse(group.isSelected(a)); + } + + @Test + public void groupedModelCannotBeDeselectedDirectly() { + ButtonGroup group = new ButtonGroup(); + DefaultButtonModel a = new DefaultButtonModel(); + a.setGroup(group); + a.setSelected(true); + a.setSelected(false); + assertTrue(a.isSelected()); + assertSame(a, group.getSelection()); + } + + @Test + public void groupFiresEventsOnBothModels() { + ButtonGroup group = new ButtonGroup(); + DefaultButtonModel a = new DefaultButtonModel(); + DefaultButtonModel b = new DefaultButtonModel(); + a.setGroup(group); + b.setGroup(group); + listen(a); + listen(b); + a.setSelected(true); + int itemsAfterA = items.size(); + assertEquals(1, itemsAfterA); + b.setSelected(true); + assertEquals(3, items.size()); + assertEquals(ItemEvent.DESELECTED, items.get(1).getStateChange()); + assertSame(a, items.get(1).getItem()); + assertEquals(ItemEvent.SELECTED, items.get(2).getStateChange()); + assertSame(b, items.get(2).getItem()); + } + + @Test + public void clearSelectionDeselectsTheModel() { + ButtonGroup group = new ButtonGroup(); + DefaultButtonModel a = new DefaultButtonModel(); + a.setGroup(group); + a.setSelected(true); + group.clearSelection(); + assertFalse(a.isSelected()); + assertNull(group.getSelection()); + assertEquals(0, group.getButtonCount()); + } + + @Test + public void ungroupedModelsAreIndependent() { + DefaultButtonModel a = new DefaultButtonModel(); + DefaultButtonModel b = new DefaultButtonModel(); + a.setSelected(true); + b.setSelected(true); + assertTrue(a.isSelected()); + assertTrue(b.isSelected()); + a.setSelected(false); + assertFalse(a.isSelected()); + } +} diff --git a/maven/swing-compat/src/test/java/com/codename1/desktopcompat/rt/UnitsTest.java b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/rt/UnitsTest.java new file mode 100644 index 00000000000..7be15953dd3 --- /dev/null +++ b/maven/swing-compat/src/test/java/com/codename1/desktopcompat/rt/UnitsTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.desktopcompat.rt; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Test; + +/// The conversion between logical pixels and device pixels. +public class UnitsTest { + + @After + public void reset() { + Units.setScale(0); + } + + @Test + public void scaleIsOneWithoutADisplayOrOverride() { + Units.setScale(0); + // No display is initialised in this class unless another test class + // ran first in the same JVM, so only the override is asserted. + Units.setScale(1f); + assertEquals(1f, Units.scale(), 0f); + assertEquals(17, Units.toDevice(17)); + } + + @Test + public void positionsRoundToTheNearestDevicePixel() { + Units.setScale(1.5f); + assertEquals(0, Units.toDevice(0)); + assertEquals(2, Units.toDevice(1)); + assertEquals(3, Units.toDevice(2)); + assertEquals(15, Units.toDevice(10)); + } + + @Test + public void adjacentRectanglesStayAdjacent() { + float[] scales = {1f, 1.25f, 1.5f, 2f, 2.625f, 3.3333333f}; + for (int s = 0; s < scales.length; s++) { + Units.setScale(scales[s]); + for (int x = 0; x < 60; x++) { + for (int w = 0; w < 40; w++) { + assertEquals("scale " + scales[s] + " x " + x + " w " + w, Units.toDevice(x + w), + Units.toDevice(x) + Units.toDeviceSize(x, w)); + } + } + } + } + + @Test + public void logicalSizesRoundDownAndUp() { + Units.setScale(2f); + assertEquals(5, Units.toLogical(11)); + assertEquals(6, Units.toLogicalCeil(11)); + assertEquals(5, Units.toLogicalCeil(10)); + assertEquals(5.5f, Units.toLogicalExact(11), 0.0001f); + } + + @Test + public void aLogicalSizeFitsInTheDeviceSizeItCameFrom() { + float[] scales = {1.25f, 1.5f, 2.625f, 3.3333333f}; + for (int s = 0; s < scales.length; s++) { + Units.setScale(scales[s]); + for (int device = 0; device < 2000; device += 7) { + int logical = Units.toLogical(device); + assertEquals(true, Units.toDevice(logical) <= device); + } + } + } +} From 03da1925e974ccafbd4b05b657eb01c2b5d30865 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:07:50 +0300 Subject: [PATCH 007/257] Wire the Swing and JavaFX layers into the Maven and Gradle plugins and the archetype A project turns the desktop layers on by having src/main/desktop (java/, kotlin/, resources/): the archetype's desktop-compat profile and the Gradle plugin add both runtime jars when the directory exists, and the remap step ships a desktop runtime only when the compiled classes refer to it. The Android rule is unchanged: its jar alone switches it on. - ProjectLayout.desktopSourceDir(), and DesktopSources for the layout and the entry record (cn1-desktop.properties: mainClass, kind). - Maven: remap-compat (remap-android delegates to it), prepare-desktop-sources with an empty compileDesktopResources hook for a later FXML/CSS step, and import-desktop-project. - DesktopProjectImporter shares the import record with the Android importer through ImportedFiles; DesktopPomUpdater shares the pom edits with AndroidPomUpdater through PomWiring. - Gradle: RemapCompatAction replaces RemapAndroidAction; Kotlin's classes of a module with desktop sources are checked after they are relocated, because the Swing runtime exists only under its relocated names. - desktop-compat-test.sh, with what it shares with android-compat-test.sh moved to inc/compat.sh. Co-Authored-By: Claude Opus 5.5 --- maven/build-engine/spotbugs-exclude.xml | 9 + .../maven/AndroidProjectImporter.java | 143 +---- .../com/codename1/maven/CompatLayers.java | 127 ++++ .../com/codename1/maven/CompatRemapper.java | 51 +- .../maven/DesktopProjectImporter.java | 579 ++++++++++++++++++ .../com/codename1/maven/DesktopSources.java | 174 ++++++ .../com/codename1/maven/ImportedFiles.java | 233 +++++++ .../java/com/codename1/maven/Relocation.java | 11 + .../codename1/maven/CompatRemapperTest.java | 101 +++ .../maven/DesktopProjectImporterTest.java | 456 ++++++++++++++ .../archetype-resources/common/pom.xml | 48 +- .../resources/archetype-resources/pom.xml | 12 + .../spotbugs-exclude.xml | 15 +- .../java/com/codename1/gradle/AppSupport.java | 107 +++- ...roidAction.java => RemapCompatAction.java} | 61 +- .../gradle/KotlinRelocationWiringTest.java | 34 + maven/codenameone-maven-plugin/pom.xml | 16 + .../codename1/maven/AndroidPomUpdater.java | 70 +-- .../codename1/maven/DesktopPomUpdater.java | 97 +++ .../maven/ImportDesktopProjectMojo.java | 132 ++++ .../java/com/codename1/maven/PomWiring.java | 110 ++++ .../maven/PrepareDesktopSourcesMojo.java | 161 +++++ .../com/codename1/maven/RemapAndroidMojo.java | 43 +- .../com/codename1/maven/RemapCompatMojo.java | 85 +++ .../maven/AndroidPomUpdaterTest.java | 15 +- .../maven/DesktopPomUpdaterTest.java | 135 ++++ .../integration-tests/android-compat-test.sh | 80 +-- .../integration-tests/desktop-compat-test.sh | 96 +++ maven/integration-tests/inc/compat.sh | 104 ++++ .../com/codename1/project/ProjectLayout.java | 11 + .../codename1/project/ProjectLayoutsTest.java | 11 + 31 files changed, 2989 insertions(+), 338 deletions(-) create mode 100644 maven/build-engine/src/main/java/com/codename1/maven/DesktopProjectImporter.java create mode 100644 maven/build-engine/src/main/java/com/codename1/maven/DesktopSources.java create mode 100644 maven/build-engine/src/main/java/com/codename1/maven/ImportedFiles.java create mode 100644 maven/build-engine/src/test/java/com/codename1/maven/DesktopProjectImporterTest.java rename maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/{RemapAndroidAction.java => RemapCompatAction.java} (53%) create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DesktopPomUpdater.java create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportDesktopProjectMojo.java create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/PomWiring.java create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/PrepareDesktopSourcesMojo.java create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapCompatMojo.java create mode 100644 maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/DesktopPomUpdaterTest.java create mode 100755 maven/integration-tests/desktop-compat-test.sh create mode 100644 maven/integration-tests/inc/compat.sh diff --git a/maven/build-engine/spotbugs-exclude.xml b/maven/build-engine/spotbugs-exclude.xml index 25d77303053..6f0eac8d2b7 100644 --- a/maven/build-engine/spotbugs-exclude.xml +++ b/maven/build-engine/spotbugs-exclude.xml @@ -291,4 +291,13 @@ + + + + + diff --git a/maven/build-engine/src/main/java/com/codename1/maven/AndroidProjectImporter.java b/maven/build-engine/src/main/java/com/codename1/maven/AndroidProjectImporter.java index 5e1805480e3..06b2979b81f 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/AndroidProjectImporter.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/AndroidProjectImporter.java @@ -29,13 +29,10 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -128,18 +125,21 @@ public Result importProject(File source, String module, File commonDir, String m Result r = new Result(); File target = new File(commonDir, "src/main/android"); try { - Map previous = readImportRecord(target); - Map imported = new TreeMap(); + // The manifest's hash is recorded below, once the build script has + // been merged into it; it is always imported (a module without one + // is refused), so it is never stale either. + ImportedFiles files = new ImportedFiles(target, IMPORT_RECORD, "the Android project", log) + .deferring(MANIFEST); for (String name : new String[] {"AndroidManifest.xml", "res", "assets", "java", "kotlin"}) { File src = new File(main, name); if (src.exists()) { - r.copiedFiles += copy(src, new File(target, name), name, previous, imported); + r.copiedFiles += files.copy(src, new File(target, name), name); } } // A manifest the developer edited since the earlier import was kept // by copy() with its earlier record; it already carries the merge. - boolean manifestKept = imported.containsKey(MANIFEST); - removeStaleImports(target, previous, imported); + boolean manifestKept = files.has(MANIFEST); + files.removeStale(); File moduleDir = main.getParentFile().getParentFile(); File gradle = new File(moduleDir, "build.gradle.kts"); if (!gradle.isFile()) { @@ -173,9 +173,9 @@ public Result importProject(File source, String module, File commonDir, String m log.info("Added the Gradle namespace and version to the copied manifest"); } // Recorded as written, so a later import can tell a local edit. - imported.put(MANIFEST, sha256(manifest)); + files.record(MANIFEST); } - writeImportRecord(target, imported); + files.write(); if (mainPackage != null && mainClass != null) { writeEntryPoint(new File(commonDir, "src/main/java"), mainPackage, mainClass); } @@ -201,16 +201,7 @@ public Result importProject(File source, String module, File commonDir, String m } static boolean hasKotlin(File dir) { - File[] files = dir.listFiles(); - if (files == null) { - return false; - } - for (File f : files) { - if (f.isDirectory() ? hasKotlin(f) : f.getName().endsWith(".kt")) { - return true; - } - } - return false; + return ImportedFiles.hasKotlin(dir); } /// Adds `name="value"` to the `` element when it has no such @@ -356,119 +347,7 @@ static void readGradle(String gradle, Result r) { /// The manifest's path in the import record. static final String MANIFEST = "AndroidManifest.xml"; - /// Copies `src` to `dest`, recording each copied file's path (relative to - /// `src/main/android`, `/` separated) and content hash in `imported`. The - /// manifest's hash is left for the caller to record once it has merged the - /// build script into it. - /// - /// A file an earlier import wrote and the developer changed since (its - /// bytes no longer match `previous`) is kept, as [#removeStaleImports] - /// keeps one, and as an import keeps a customized entry point: it is - /// recorded with its earlier hash, so the next import still recognizes - /// the edit, and deleting it lets an import copy it afresh. - private int copy(File src, File dest, String path, Map previous, Map imported) - throws IOException { - if (src.isDirectory()) { - int n = 0; - File[] files = src.listFiles(); - if (files != null) { - for (File f : files) { - n += copy(f, new File(dest, f.getName()), path + "/" + f.getName(), previous, imported); - } - } - return n; - } - String recorded = previous.get(path); - if (recorded != null && dest.isFile() && !recorded.equals(sha256(dest)) - && !java.util.Arrays.equals(Files.readAllBytes(src.toPath()), Files.readAllBytes(dest.toPath()))) { - log.warn("Kept " + dest + ": it was changed after the earlier import, so the Android project's copy " - + "was not imported; delete it and import again to take that copy"); - imported.put(path, recorded); - return 0; - } - dest.getParentFile().mkdirs(); - Files.copy(src.toPath(), dest.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); - if (!MANIFEST.equals(path)) { - imported.put(path, sha256(dest)); - } - return 1; - } - - /// Removes what an earlier import copied and this one did not, so a file - /// deleted or renamed upstream does not stay behind to be compiled or - /// packaged. Only a file listed in the earlier import's record is a - /// candidate, and only while it still holds exactly the bytes that import - /// wrote: a file the developer added, or edited after importing it, is - /// kept. Directories the removal empties go too. - private void removeStaleImports(File target, Map previous, Map imported) - throws IOException { - for (Map.Entry e : previous.entrySet()) { - String path = e.getKey(); - // The manifest is always imported (a module without one is refused); - // its record is written only after the merge, so it is not here yet. - if (imported.containsKey(path) || MANIFEST.equals(path) || path.contains("..")) { - continue; - } - File f = new File(target, path.replace('/', File.separatorChar)); - if (!f.isFile()) { - continue; - } - if (!e.getValue().equals(sha256(f))) { - log.warn("Kept " + f + ": it is no longer in the Android project, but it was changed after " - + "the earlier import"); - continue; - } - Files.delete(f.toPath()); - log.info("Removed " + f + ": it is no longer in the Android project"); - File dir = f.getParentFile(); - while (dir != null && !dir.equals(target)) { - String[] left = dir.list(); - if (left == null || left.length > 0 || !dir.delete()) { - break; - } - dir = dir.getParentFile(); - } - } - } - /// The record an import leaves in `src/main/android`: one /// ` ` line per file it copied. static final String IMPORT_RECORD = ".android-import-files"; - - private static Map readImportRecord(File target) throws IOException { - Map out = new TreeMap(); - File f = new File(target, IMPORT_RECORD); - if (!f.isFile()) { - return out; - } - for (String line : Files.readAllLines(f.toPath(), Charset.forName("UTF-8"))) { - int sp = line.indexOf(' '); - if (sp > 0 && sp < line.length() - 1) { - out.put(line.substring(sp + 1), line.substring(0, sp)); - } - } - return out; - } - - private static void writeImportRecord(File target, Map imported) throws IOException { - StringBuilder b = new StringBuilder(); - for (Map.Entry e : imported.entrySet()) { - b.append(e.getValue()).append(' ').append(e.getKey()).append('\n'); - } - target.mkdirs(); - Files.write(new File(target, IMPORT_RECORD).toPath(), b.toString().getBytes(Charset.forName("UTF-8"))); - } - - private static String sha256(File f) throws IOException { - try { - byte[] d = MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(f.toPath())); - StringBuilder b = new StringBuilder(d.length * 2); - for (byte x : d) { - b.append(Character.forDigit((x >> 4) & 0xf, 16)).append(Character.forDigit(x & 0xf, 16)); - } - return b.toString(); - } catch (NoSuchAlgorithmException e) { - throw new IOException("SHA-256 is unavailable", e); - } - } } diff --git a/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java b/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java index 611574d201f..f5515799ced 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/CompatLayers.java @@ -22,7 +22,14 @@ */ package com.codename1.maven; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.Remapper; + import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -34,6 +41,13 @@ /// A layer is switched on by its runtime jar being on the compile classpath: /// the project templates add that dependency when the layer's source directory /// exists, so nothing else has to be configured. +/// +/// The two desktop layers share one source directory (`src/main/desktop`), so +/// the templates add both of their jars and cannot know which of them the +/// sources need. For those the jar is necessary but not sufficient: the +/// application's compiled classes must also name the layer's API +/// ([#active(Iterable, Iterable)]). A Swing application therefore does not +/// ship the JavaFX runtime, and nothing has to be declared for that. public final class CompatLayers { /// The Swing layer. `java.awt` and `javax.swing` belong to the JDK, so @@ -76,6 +90,119 @@ public static List active(Iterable classpath) { return out; } + /// Whether `layer` is switched on by its jar alone. The Android layer is: + /// its sources have a directory of their own, and the build compiles + /// resources and generates classes for it before any class exists to be + /// examined. The desktop layers also have to be used. + public static boolean activeByPresence(Relocation layer) { + return layer != SWING && layer != JAVAFX; + } + + /// The layers an application ships: those whose runtime jar is among + /// `classpath` and which, unless the jar alone decides + /// ([#activeByPresence]), a class under one of `classDirs` refers to. + /// + /// A reference counts under the name the application was compiled against + /// (`javax/swing/JTable`) and under the name it has once relocated, so a + /// directory an earlier run already rewrote gives the same answer. The + /// runtimes a previous run extracted into a directory are not application + /// code and are not read. + public static List active(Iterable classpath, Iterable classDirs) throws IOException { + List present = active(classpath); + List undecided = new ArrayList(); + for (Relocation r : present) { + if (!activeByPresence(r)) { + undecided.add(r); + } + } + if (undecided.isEmpty()) { + return present; + } + ReferenceScan scan = new ReferenceScan(undecided); + if (classDirs != null) { + for (File dir : classDirs) { + if (dir != null && dir.isDirectory()) { + scan.directory(dir, ""); + } + } + } + List out = new ArrayList(); + for (Relocation r : present) { + if (activeByPresence(r) || scan.found.contains(r)) { + out.add(r); + } + } + return out; + } + + /// Finds which of a set of layers some class refers to, stopping as soon + /// as every one of them has been seen. + private static final class ReferenceScan extends Remapper { + private final List wanted; + private final List found = new ArrayList(); + + ReferenceScan(List wanted) { + this.wanted = wanted; + } + + @Override + public String map(String internalName) { + if (internalName != null) { + for (Relocation r : wanted) { + if (!found.contains(r) && (r.owns(internalName) || r.original(internalName) != null)) { + found.add(r); + } + } + } + return internalName; + } + + private boolean done() { + return found.size() == wanted.size(); + } + + /// `rel` is the directory's path inside the classes directory, with + /// its trailing slash, or empty for the classes directory itself. + void directory(File dir, String rel) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (done()) { + return; + } + if (f.isDirectory()) { + String child = rel + f.getName() + "/"; + if (!isExtractedRuntime(child)) { + directory(f, child); + } + } else if (f.getName().endsWith(".class")) { + // Every name a class file holds goes through the remapper: + // supertypes, descriptors, signatures, annotations and + // the instructions' operands alike. + new ClassReader(Files.readAllBytes(f.toPath())).accept( + new ClassRemapper(new ClassWriter(0), this), ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + } + } + } + } + + /// Whether `rel` (a directory inside a classes directory, with its + /// trailing slash) is where some layer's runtime, or the shared JDK + /// classes, are extracted. + static boolean isExtractedRuntime(String rel) { + if (rel.startsWith(Relocation.JDK_PACKAGE)) { + return true; + } + for (Relocation r : ALL) { + if (rel.startsWith(r.target())) { + return true; + } + } + return false; + } + /// `layer`'s runtime jar among `classpath`, or null. public static File runtimeJar(Relocation layer, Iterable classpath) { File found = null; diff --git a/maven/build-engine/src/main/java/com/codename1/maven/CompatRemapper.java b/maven/build-engine/src/main/java/com/codename1/maven/CompatRemapper.java index dadb5921abb..6b87328d9a7 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/CompatRemapper.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/CompatRemapper.java @@ -35,7 +35,8 @@ /// layers it has switched on, and ships their runtimes with it. /// /// The one entry point a build plugin needs: it works out the active layers -/// from the compile classpath ([CompatLayers#active]), relocates by all of +/// from the compile classpath and the compiled classes +/// ([CompatLayers#active(Iterable, Iterable)]), relocates by all of /// their rules in a single pass ([ClassRelocator]) and extracts each runtime /// jar, and the shared JDK classes, exactly once. An application with no /// layer is left untouched, so the step can run unconditionally. @@ -55,10 +56,13 @@ public final class CompatRemapper { private final List classpath; private final File onClickNames; private final Log log; - private final List active; - private final ClassRelocator relocator; + /// Worked out on first use, from everything the caller configured by + /// then: the handler directories are examined too. + private List active; + private ClassRelocator relocator; private final List handlerDirs = new ArrayList(); private boolean shipRuntime = true; + private File desktopEntry; /// `classesDir` is the application's output directory, rewritten in /// place. `classpath` is its compile classpath, which decides the active @@ -77,8 +81,6 @@ public CompatRemapper(File classesDir, Iterable classpath, File onClickNam } this.onClickNames = onClickNames; this.log = log; - this.active = Collections.unmodifiableList(CompatLayers.active(this.classpath)); - this.relocator = new ClassRelocator(active); } /// Only relocates the classes, without copying in any runtime or @@ -102,26 +104,53 @@ public CompatRemapper withHandlerDirectories(List dirs) { return this; } + /// The record of the desktop application's entry point + /// ([DesktopSources#entryRecord]), which the Swing and JavaFX entry point + /// generators read; null, or a file that does not exist, for an + /// application that has none. + public CompatRemapper withDesktopEntryRecord(File record) { + this.desktopEntry = record; + return this; + } + + /// The file given to [#withDesktopEntryRecord], or null. + File desktopEntryRecord() { + return desktopEntry; + } + /// The layers this application has switched on, in the order their rules - /// are tried; empty when it has none. - public List activeLayers() { + /// are tried; empty when it has none. A desktop layer counts only when the + /// classes directory, or one of the handler directories, refers to it. + public List activeLayers() throws BuildException { + if (active == null) { + List dirs = new ArrayList(); + dirs.add(classesDir); + dirs.addAll(handlerDirs); + try { + active = Collections.unmodifiableList(CompatLayers.active(classpath, dirs)); + } catch (IOException e) { + throw new BuildException("Could not read the compiled classes: " + e.getMessage(), e); + } + relocator = new ClassRelocator(active); + } return active; } /// Whether `layer` is among the active ones. - public boolean isActive(Relocation layer) { - return active.contains(layer); + public boolean isActive(Relocation layer) throws BuildException { + return activeLayers().contains(layer); } /// The relocator composed of every active layer's rules. - public ClassRelocator relocator() { + public ClassRelocator relocator() throws BuildException { + activeLayers(); return relocator; } /// Relocates and ships. Answers false, having done nothing, when the /// application has no compatibility layer. public boolean run() throws BuildException { - if (active.isEmpty()) { + if (activeLayers().isEmpty()) { log.debug("No compatibility layer on the classpath; nothing to relocate"); return false; } diff --git a/maven/build-engine/src/main/java/com/codename1/maven/DesktopProjectImporter.java b/maven/build-engine/src/main/java/com/codename1/maven/DesktopProjectImporter.java new file mode 100644 index 00000000000..064c8182f55 --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/maven/DesktopProjectImporter.java @@ -0,0 +1,579 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.build.Log; +import com.codename1.builders.BuildException; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/// Imports a Swing or JavaFX project (a Maven or Gradle project, or one +/// module of it) into a Codename One application: copies its `src/main` +/// sources and resources into `src/main/desktop`, records how the application +/// starts ([DesktopSources#ENTRY_RECORD]), and reports which of its +/// dependencies the desktop layers cover. +/// +/// Nothing is asked. The entry point is found by reading the sources, and +/// where they leave a choice the source project's own main-class setting +/// decides it; when that does not either, the import stops before copying +/// anything and lists the candidates. +public final class DesktopProjectImporter { + + /// The record an import leaves in `src/main/desktop`; see [ImportedFiles]. + static final String IMPORT_RECORD = ".desktop-import-files"; + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + /// Coordinates a desktop layer implements, by exact `group:artifact`. + static final Map COVERED = new LinkedHashMap(); + + /// Coordinates a desktop layer implements, by `group:artifact` prefix. + static final String[][] COVERED_PREFIXES = { + {"org.swinglabs.swingx:swingx-", "SwingX"}, + }; + + static { + COVERED.put("org.openjfx:javafx-controls", "JavaFX controls"); + COVERED.put("org.openjfx:javafx-fxml", "FXML"); + COVERED.put("org.openjfx:javafx-graphics", "JavaFX graphics"); + COVERED.put("org.openjfx:javafx-base", "JavaFX base"); + COVERED.put("com.miglayout:miglayout-swing", "covered by the built-in MiG layout support"); + COVERED.put("org.jetbrains.kotlin:kotlin-stdlib", "Kotlin standard library"); + COVERED.put("org.jetbrains.kotlin:kotlin-stdlib-jdk7", "Kotlin standard library"); + COVERED.put("org.jetbrains.kotlin:kotlin-stdlib-jdk8", "Kotlin standard library"); + } + + /// Dependencies that only matter to tests. + static final String[] IGNORED = {"junit:", "org.junit", "org.mockito", "org.testfx", "org.hamcrest", + "org.assertj"}; + + public static final class Result { + public final List covered = new ArrayList(); + public final List uncovered = new ArrayList(); + /// The `module-info.java` files that were left out, relative to + /// `src/main/desktop`. A Codename One application is not a module. + public final List droppedModuleInfo = new ArrayList(); + /// The recorded entry point; null when the import kept an existing + /// record it could not improve on. + public String mainClass; + /// [DesktopSources#KIND_SWING] or [DesktopSources#KIND_JAVAFX]. + public String kind; + public boolean kotlin; + public int copiedFiles; + } + + /// A class the application could be started through. + static final class Candidate { + final String name; + boolean javafx; + + Candidate(String name) { + this.name = name; + } + + String kind() { + return javafx ? DesktopSources.KIND_JAVAFX : DesktopSources.KIND_SWING; + } + } + + private final Log log; + + public DesktopProjectImporter(Log log) { + this.log = log; + } + + private static boolean hasSources(File dir) { + return new File(dir, "src/main/java").isDirectory() || new File(dir, "src/main/kotlin").isDirectory(); + } + + /// The directory whose `src/main` is imported: `source/` when a + /// module is named, else `source` itself, else the one directory directly + /// inside it that has sources. + public static File moduleDir(File source, String module) throws BuildException { + if (module != null && module.length() > 0) { + File m = new File(source, module); + if (!hasSources(m)) { + throw new BuildException("No src/main/java or src/main/kotlin under " + m); + } + return m; + } + if (hasSources(source)) { + return source; + } + List found = new ArrayList(); + File[] children = source.listFiles(); + if (children != null) { + Arrays.sort(children); + for (File c : children) { + if (c.isDirectory() && hasSources(c)) { + found.add(c); + } + } + } + if (found.size() == 1) { + return found.get(0); + } + if (found.isEmpty()) { + throw new BuildException("No src/main/java or src/main/kotlin under " + source + + " or the directories directly inside it"); + } + StringBuilder names = new StringBuilder(); + for (File f : found) { + names.append(names.length() == 0 ? "" : ", ").append(f.getName()); + } + throw new BuildException(source + " has several modules with sources (" + names + + "); name the one to import with -Dcn1.desktop.module="); + } + + /// Imports `source` (or its `module`) into the application whose common + /// module is `commonDir`. `mainClassOverride`, when not null, names the + /// entry point and no other is considered. + public Result importProject(File source, String module, File commonDir, String mainClassOverride) + throws BuildException { + File moduleDir = moduleDir(source, module); + File main = new File(moduleDir, "src/main"); + File target = new File(commonDir, "src/main/desktop"); + Result r = new Result(); + try { + // Decided from the source project, before anything is copied: an + // import that cannot tell how the application starts leaves the + // application as it found it. + Map candidates = new TreeMap(); + scan(new File(main, "java"), candidates); + scan(new File(main, "kotlin"), candidates); + Properties existing = DesktopSources.readEntryRecord(target); + Candidate entry = chooseEntry(candidates, mainClassOverride, declaredMainClasses(source, moduleDir), + existing == null ? null : existing.getProperty("mainClass"), existing != null); + + ImportedFiles files = new ImportedFiles(target, IMPORT_RECORD, "the desktop project", log) + .skipping("module-info.java") + // The application is packaged by the Codename One build, + // which writes the manifest of whatever it produces. + .skipping("MANIFEST.MF"); + for (String name : new String[] {"java", "kotlin", "resources"}) { + File src = new File(main, name); + if (src.isDirectory()) { + r.copiedFiles += files.copy(src, new File(target, name), name); + } + } + files.removeStale(); + files.write(); + for (String skipped : files.skipped()) { + if (skipped.endsWith("module-info.java")) { + r.droppedModuleInfo.add(skipped); + log.info("Left out " + skipped + ": a Codename One application is not a Java module"); + } + } + if (entry != null) { + r.mainClass = entry.name; + r.kind = entry.kind(); + writeEntryRecord(target, existing, entry); + } else if (existing != null) { + r.mainClass = existing.getProperty("mainClass"); + r.kind = existing.getProperty("kind"); + } + readDependencies(moduleDir, r); + if (ImportedFiles.hasKotlin(target)) { + // The application's Kotlin build switches on when + // src/main/kotlin exists; it compiles the desktop sources too. + File kotlinDir = new File(commonDir, "src/main/kotlin"); + if (!kotlinDir.isDirectory()) { + if (!kotlinDir.mkdirs()) { + throw new IOException("Cannot create " + kotlinDir); + } + Files.write(new File(kotlinDir, ".keep").toPath(), new byte[0]); + log.info("The desktop sources include Kotlin: created " + kotlinDir + + " so the application compiles Kotlin"); + } + r.kotlin = true; + } + } catch (IOException e) { + throw new BuildException("Import failed: " + e.getMessage(), e); + } + return r; + } + + /// Picks the entry point. Answers null only when there is no candidate + /// and an existing record to keep. + /// + /// In order: the override; the only candidate; the candidate the source + /// project's build names as its main class; the candidate an earlier + /// import recorded. A build that names a plain launcher class, in a + /// project with exactly one `javafx.application.Application` subclass, + /// means that subclass: the launcher only exists to call it, and the + /// JavaFX layer starts the application class itself. + Candidate chooseEntry(Map candidates, String override, List declared, + String recorded, boolean hasRecord) throws BuildException { + if (override != null && override.length() > 0) { + Candidate c = candidates.get(override); + if (c == null) { + throw new BuildException("-Dcn1.desktop.mainClass=" + override + " names no class that extends " + + "javafx.application.Application or declares public static void main(String[]). " + + describe(candidates)); + } + return c; + } + if (candidates.isEmpty()) { + if (hasRecord) { + return null; + } + throw new BuildException("Nothing was imported: no class extends javafx.application.Application or " + + "declares public static void main(String[]), so there is nothing to start."); + } + if (candidates.size() == 1) { + return candidates.values().iterator().next(); + } + for (String name : declared) { + Candidate c = candidates.get(name); + if (c == null) { + continue; + } + if (!c.javafx) { + Candidate onlyApplication = null; + int applications = 0; + for (Candidate other : candidates.values()) { + if (other.javafx) { + onlyApplication = other; + applications++; + } + } + if (applications == 1) { + log.info("The project's main class " + name + " is a launcher; the application starts through " + + onlyApplication.name); + return onlyApplication; + } + } + return c; + } + if (recorded != null && candidates.containsKey(recorded)) { + return candidates.get(recorded); + } + throw new BuildException("Nothing was imported: the project has several classes it could start through, " + + "and its build names none of them as the main class. " + describe(candidates) + + " Name one with -Dcn1.desktop.mainClass="); + } + + private static String describe(Map candidates) { + if (candidates.isEmpty()) { + return "The sources have no such class."; + } + StringBuilder b = new StringBuilder("Candidates:"); + for (Candidate c : candidates.values()) { + b.append("\n ").append(c.name).append(" (").append(c.kind()).append(')'); + } + b.append('\n'); + return b.toString(); + } + + private void writeEntryRecord(File target, Properties existing, Candidate entry) throws IOException { + if (existing != null && entry.name.equals(existing.getProperty("mainClass")) + && entry.kind().equals(existing.getProperty("kind"))) { + // Already says so; whatever else the developer wrote in it stays. + return; + } + String text = "# How the desktop application in this directory starts; read by the Codename One build.\n" + + "# mainClass: the class that starts it, as the sources declare it.\n" + + "# kind: " + DesktopSources.KIND_JAVAFX + " when that class extends javafx.application.Application, " + + DesktopSources.KIND_SWING + " when it is started through its main method.\n" + + "mainClass=" + entry.name + "\n" + + "kind=" + entry.kind() + "\n"; + if (!target.isDirectory() && !target.mkdirs()) { + throw new IOException("Cannot create " + target); + } + Files.write(DesktopSources.entryRecord(target).toPath(), text.getBytes(UTF8)); + log.info("Recorded the entry point " + entry.name + " (" + entry.kind() + ") in " + + DesktopSources.ENTRY_RECORD); + } + + // ---- entry point detection ------------------------------------------- + + private static final Pattern NOISE = Pattern.compile( + "\"\"\".*?\"\"\"|\"(?:\\\\.|[^\"\\\\\\n])*\"|'(?:\\\\.|[^'\\\\\\n])*'|/\\*.*?\\*/|//[^\\n]*", + Pattern.DOTALL); + private static final Pattern PACKAGE = Pattern.compile("(?m)^\\s*package\\s+([\\w.]+)"); + private static final Pattern FX_IMPORT = Pattern.compile( + "(?m)^\\s*import\\s+javafx\\.application\\.(?:Application|\\*)\\s*;?\\s*$"); + private static final Pattern JAVA_EXTENDS = Pattern.compile( + "\\bclass\\s+(\\w+)\\s*(?:<[^{]*?>)?\\s*extends\\s+((?:javafx\\.application\\.)?Application)\\b"); + private static final Pattern JAVA_MAIN = Pattern.compile( + "\\b(?:public\\s+static|static\\s+public)\\s+(?:final\\s+)?void\\s+main\\s*\\(\\s*(?:final\\s+)?String\\s*" + + "(?:\\[\\s*\\]\\s*\\w+|\\.\\.\\.\\s*\\w+|\\s+\\w+\\s*\\[\\s*\\])\\s*\\)"); + private static final Pattern KOTLIN_EXTENDS = Pattern.compile( + "\\bclass\\s+(\\w+)[^{\\n]*?:[^{\\n]*?((?:javafx\\.application\\.)?Application)\\s*\\("); + private static final Pattern KOTLIN_STATIC_MAIN = Pattern.compile("@JvmStatic\\s+fun\\s+main\\s*\\("); + private static final Pattern KOTLIN_TOP_MAIN = Pattern.compile("(?m)^fun\\s+main\\s*\\("); + private static final Pattern KOTLIN_FILE_NAME = Pattern.compile("@file\\s*:\\s*JvmName\\s*\\(\\s*\"(\\w+)\""); + + /// Source text with its comments and string literals blanked, so a class + /// name in a comment is not read as a declaration. + static String code(String source) { + return NOISE.matcher(source).replaceAll(" "); + } + + private void scan(File dir, Map out) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + Arrays.sort(files); + for (File f : files) { + if (f.isDirectory()) { + scan(f, out); + } else if (f.getName().endsWith(".java") && !"module-info.java".equals(f.getName())) { + String name = f.getName().substring(0, f.getName().length() - ".java".length()); + scanJava(name, new String(Files.readAllBytes(f.toPath()), UTF8), out); + } else if (f.getName().endsWith(".kt")) { + String name = f.getName().substring(0, f.getName().length() - ".kt".length()); + scanKotlin(name, new String(Files.readAllBytes(f.toPath()), UTF8), out); + } + } + } + + private static Candidate candidate(Map out, String name) { + Candidate c = out.get(name); + if (c == null) { + c = new Candidate(name); + out.put(name, c); + } + return c; + } + + /// `fileClass` is the file's name without its extension, which is the + /// name of the public class a Java file declares. + static void scanJava(String fileClass, String source, Map out) { + String code = code(source); + Matcher pkg = PACKAGE.matcher(code); + String prefix = pkg.find() ? pkg.group(1) + "." : ""; + boolean imported = FX_IMPORT.matcher(code).find(); + Matcher ext = JAVA_EXTENDS.matcher(code); + while (ext.find()) { + if (ext.group(2).indexOf('.') < 0 && !imported) { + // Some other class called Application. + continue; + } + String cls = ext.group(1); + candidate(out, prefix + (cls.equals(fileClass) ? cls : fileClass + "$" + cls)).javafx = true; + } + if (JAVA_MAIN.matcher(code).find()) { + candidate(out, prefix + fileClass); + } + } + + static void scanKotlin(String fileClass, String source, Map out) { + // The annotation's argument is a string literal, which code() blanks. + Matcher named = KOTLIN_FILE_NAME.matcher(source); + String code = code(source); + Matcher pkg = PACKAGE.matcher(code); + String prefix = pkg.find() ? pkg.group(1) + "." : ""; + boolean imported = FX_IMPORT.matcher(code).find(); + Matcher ext = KOTLIN_EXTENDS.matcher(code); + while (ext.find()) { + if (ext.group(2).indexOf('.') < 0 && !imported) { + continue; + } + candidate(out, prefix + ext.group(1)).javafx = true; + } + if (KOTLIN_STATIC_MAIN.matcher(code).find()) { + candidate(out, prefix + fileClass); + } + if (KOTLIN_TOP_MAIN.matcher(code).find()) { + // A top-level function compiles into the file's facade class. + String facade = named.find() ? named.group(1) + : Character.toUpperCase(fileClass.charAt(0)) + fileClass.substring(1) + "Kt"; + candidate(out, prefix + facade); + } + } + + // ---- the source project's build -------------------------------------- + + private static String read(File f) throws IOException { + return f.isFile() ? new String(Files.readAllBytes(f.toPath()), UTF8) : ""; + } + + /// The build files that describe the module: its own, then the ones of the + /// project it is a module of. + private static List buildFiles(File source, File moduleDir, String... names) throws IOException { + List out = new ArrayList(); + for (String name : names) { + String text = read(new File(moduleDir, name)); + if (text.length() > 0) { + out.add(text); + } + } + if (!source.equals(moduleDir)) { + for (String name : names) { + String text = read(new File(source, name)); + if (text.length() > 0) { + out.add(text); + } + } + } + return out; + } + + private static final Pattern POM_MAIN = Pattern.compile( + "<(exec\\.mainClass|mainClass|main\\.class|Main-Class)>\\s*([^<\\s]+)\\s*"); + private static final Pattern GRADLE_MAIN = Pattern.compile( + "\\bmainClass(?:Name)?\\s*(?:=|\\.set\\s*\\(|\\()?\\s*[\"']([^\"']+)[\"']"); + private static final Pattern GRADLE_MANIFEST_MAIN = Pattern.compile( + "[\"']Main-Class[\"']\\s*(?::|to|,)\\s*[\"']([^\"']+)[\"']"); + private static final Pattern MANIFEST_MAIN = Pattern.compile("(?m)^Main-Class:\\s*(\\S+)"); + + /// The main classes the source project's build names, most specific + /// first: `exec.mainClass` and `mainClass` in a pom, `mainClass` in a + /// Gradle `application` or `javafx` block, and a manifest's `Main-Class`. + static List declaredMainClasses(File source, File moduleDir) throws IOException { + Set out = new LinkedHashSet(); + for (String pom : buildFiles(source, moduleDir, "pom.xml")) { + Matcher m = POM_MAIN.matcher(pom); + while (m.find()) { + add(out, resolve(m.group(2), pom)); + } + } + for (String gradle : buildFiles(source, moduleDir, "build.gradle.kts", "build.gradle")) { + Matcher m = GRADLE_MAIN.matcher(gradle); + while (m.find()) { + add(out, m.group(1)); + } + m = GRADLE_MANIFEST_MAIN.matcher(gradle); + while (m.find()) { + add(out, m.group(1)); + } + } + Matcher m = MANIFEST_MAIN.matcher(read(new File(moduleDir, "src/main/resources/META-INF/MANIFEST.MF"))); + if (m.find()) { + add(out, m.group(1)); + } + return new ArrayList(out); + } + + /// A pom value, with a `${property}` it consists of replaced by that + /// property's value where the same pom defines it. + private static String resolve(String value, String pom) { + if (value.startsWith("${") && value.endsWith("}")) { + String name = value.substring(2, value.length() - 1); + Matcher m = Pattern.compile("<" + Pattern.quote(name) + ">\\s*([^<\\s]+)\\s*") + .matcher(pom); + return m.find() ? m.group(1) : null; + } + return value; + } + + private static void add(Set out, String name) { + if (name == null || name.indexOf("${") >= 0) { + // A property this build file does not define. + return; + } + // A modular build writes module/class. + int slash = name.lastIndexOf('/'); + out.add(slash < 0 ? name : name.substring(slash + 1)); + } + + private static final Pattern POM_DEPENDENCY = Pattern.compile("(.*?)", Pattern.DOTALL); + private static final Pattern GRADLE_DEPENDENCY = Pattern.compile( + "(?:implementation|api|compileOnly|runtimeOnly)\\s*\\(?\\s*[\"']([^\"':]+:[^\"':]+)(?::[^\"']*)?[\"']"); + private static final Pattern GRADLE_FX_MODULES = Pattern.compile("\\bmodules\\s*(?:=|\\()([^\\n]*)"); + private static final Pattern GRADLE_FX_MODULE = Pattern.compile("[\"']javafx\\.(\\w+)[\"']"); + + private static String element(String xml, String name) { + Matcher m = Pattern.compile("<" + name + ">\\s*([^<\\s]+)\\s*").matcher(xml); + return m.find() ? m.group(1) : null; + } + + static void readDependencies(File moduleDir, Result r) throws IOException { + Set coords = new LinkedHashSet(); + boolean catalog = false; + // The module's own build only: what a parent declares for every module + // says nothing about what this one uses. + String pom = read(new File(moduleDir, "pom.xml")); + // Managed versions and plugin dependencies are not the module's. + pom = pom.replaceAll("(?s).*?", " ") + .replaceAll("(?s).*?", " "); + Matcher dep = POM_DEPENDENCY.matcher(pom); + while (dep.find()) { + String group = element(dep.group(1), "groupId"); + String artifact = element(dep.group(1), "artifactId"); + if (group != null && artifact != null && !"test".equals(element(dep.group(1), "scope"))) { + coords.add(group + ":" + artifact); + } + } + for (String name : new String[] {"build.gradle.kts", "build.gradle"}) { + String gradle = read(new File(moduleDir, name)); + Matcher g = GRADLE_DEPENDENCY.matcher(gradle); + while (g.find()) { + coords.add(g.group(1)); + } + // The JavaFX Gradle plugin's javafx { modules = [...] }. + Matcher modules = GRADLE_FX_MODULES.matcher(gradle); + while (modules.find()) { + Matcher module = GRADLE_FX_MODULE.matcher(modules.group(1)); + while (module.find()) { + coords.add("org.openjfx:javafx-" + module.group(1)); + } + } + catalog |= gradle.contains("libs."); + } + for (String coord : coords) { + boolean ignored = false; + for (String i : IGNORED) { + if (coord.startsWith(i)) { + ignored = true; + } + } + if (ignored) { + continue; + } + String covered = COVERED.get(coord); + if (covered == null) { + for (String[] prefix : COVERED_PREFIXES) { + if (coord.startsWith(prefix[0])) { + covered = prefix[1]; + } + } + } + if (covered != null) { + r.covered.add(coord + " (" + covered + ")"); + } else { + r.uncovered.add(coord); + } + } + if (catalog) { + r.uncovered.add("dependencies declared through a version catalog (libs.*) were not resolved; " + + "check them against the supported list by hand"); + } + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/maven/DesktopSources.java b/maven/build-engine/src/main/java/com/codename1/maven/DesktopSources.java new file mode 100644 index 00000000000..aa32020ce76 --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/maven/DesktopSources.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.builders.BuildException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/// The desktop sources of an application module: `src/main/desktop`, holding +/// a Swing or JavaFX application as its own build laid it out. +/// +/// ``` +/// src/main/desktop/java Java sources +/// src/main/desktop/kotlin Kotlin sources (optional) +/// src/main/desktop/resources classpath resources: .fxml, .css, .properties, images +/// src/main/desktop/cn1-desktop.properties the application's entry point +/// ``` +/// +/// The directory existing is what switches the desktop layers on: the project +/// templates add both runtime jars when it does, and the remap step ships +/// whichever of them the compiled sources use +/// ([CompatLayers#active(Iterable, Iterable)]). Both build plugins read the +/// layout from here, so they cannot disagree on it. +public final class DesktopSources { + + /// The file, directly inside the desktop source directory, that records + /// how the application starts. A properties file with two keys: + /// + /// - `mainClass`: the fully qualified name of the class that starts the + /// application, as its sources declare it (never a relocated name) -- + /// the `javafx.application.Application` subclass of a JavaFX + /// application, the class holding `public static void main(String[])` + /// of a Swing one. + /// - `kind`: `javafx` or `swing`, which says which of the two that is. + /// + /// `cn1:import-desktop-project` writes it; a developer who places sources + /// in the directory by hand writes it too. + public static final String ENTRY_RECORD = "cn1-desktop.properties"; + + /// The `kind` of an application started through a `main` method. + public static final String KIND_SWING = "swing"; + + /// The `kind` of an application started through its + /// `javafx.application.Application` subclass. + public static final String KIND_JAVAFX = "javafx"; + + private DesktopSources() { + } + + /// Whether the module has desktop sources, which is what enables the + /// desktop layers. + public static boolean isDesktopProject(File desktopDir) { + return desktopDir != null && desktopDir.isDirectory(); + } + + public static File javaDir(File desktopDir) { + return new File(desktopDir, "java"); + } + + public static File kotlinDir(File desktopDir) { + return new File(desktopDir, "kotlin"); + } + + public static File resourcesDir(File desktopDir) { + return new File(desktopDir, "resources"); + } + + /// Where the entry point is recorded; see [#ENTRY_RECORD]. + public static File entryRecord(File desktopDir) { + return new File(desktopDir, ENTRY_RECORD); + } + + /// The entry record's contents, or null when the module has none. + public static Properties readEntryRecord(File desktopDir) throws IOException { + File f = entryRecord(desktopDir); + if (!f.isFile()) { + return null; + } + Properties p = new Properties(); + InputStream in = new FileInputStream(f); + try { + p.load(in); + } finally { + in.close(); + } + return p; + } + + /// The desktop layers whose API the sources under `desktopDir` name, as + /// text: `""`, `"Swing"`, `"JavaFX"` or `"Swing,JavaFX"`. + /// + /// This is not what decides which runtime ships -- the compiled classes + /// do ([CompatLayers#active(Iterable, Iterable)]). It is a cheap value + /// that changes when that decision may have: Gradle makes it an input of + /// the task that takes the decision, so that the task runs again. + public static String toolkitsNamedIn(File desktopDir) { + boolean[] named = new boolean[DESKTOP_LAYERS.length]; + try { + scan(javaDir(desktopDir), named); + scan(kotlinDir(desktopDir), named); + } catch (IOException e) { + throw new java.io.UncheckedIOException(e); + } + StringBuilder out = new StringBuilder(); + for (int i = 0; i < named.length; i++) { + if (named[i]) { + out.append(out.length() == 0 ? "" : ",").append(DESKTOP_LAYERS[i].name()); + } + } + return out.toString(); + } + + private static final Relocation[] DESKTOP_LAYERS = {CompatLayers.SWING, CompatLayers.JAVAFX}; + + private static void scan(File dir, boolean[] named) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + scan(f, named); + } else if (f.getName().endsWith(".java") || f.getName().endsWith(".kt")) { + String text = new String(java.nio.file.Files.readAllBytes(f.toPath()), "UTF-8"); + for (int i = 0; i < named.length; i++) { + named[i] = named[i] || DESKTOP_LAYERS[i].namedInSource(text); + } + } + } + } + + /// Fails when the module has desktop sources but neither desktop runtime + /// among its dependencies: the sources could compile (Swing code compiles + /// against the JDK itself) and would then reach the compliance check as + /// plain unsupported API. `artifactIds` are the module's resolved + /// dependencies. + public static void requireRuntime(File desktopDir, Iterable artifactIds) throws BuildException { + if (!isDesktopProject(desktopDir)) { + return; + } + for (String id : artifactIds) { + if (CompatLayers.SWING.artifactId().equals(id) || CompatLayers.JAVAFX.artifactId().equals(id)) { + return; + } + } + throw new BuildException("src/main/desktop exists but neither the " + CompatLayers.SWING.artifactId() + + " nor the " + CompatLayers.JAVAFX.artifactId() + + " dependency is present; add both to the common module (scope provided)."); + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/maven/ImportedFiles.java b/maven/build-engine/src/main/java/com/codename1/maven/ImportedFiles.java new file mode 100644 index 00000000000..7a3fafcbd00 --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/maven/ImportedFiles.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.build.Log; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/// The files an import copied into an application, and the record that lets +/// the next import of the same project tell its own copies from the +/// developer's work. +/// +/// An import leaves one ` ` line per file it copied in a record +/// file inside the directory it copied into. Importing again then updates +/// only what is still exactly as the earlier import wrote it: a file the +/// developer changed since is kept, a file that left the source project is +/// removed, and a file the developer added was never in the record at all. +/// Both project importers ([AndroidProjectImporter], [DesktopProjectImporter]) +/// copy through this, so they behave the same on a repeated import. +final class ImportedFiles { + + private final File target; + private final String recordName; + private final String sourceName; + private final Log log; + private final Map previous; + private final Map imported = new TreeMap(); + private final List skipNames = new ArrayList(); + private final List skipped = new ArrayList(); + private String deferred; + + /// `target` is the directory copied into, `recordName` the record file's + /// name inside it, and `sourceName` what a message calls the project + /// imported from ("the Android project"). + ImportedFiles(File target, String recordName, String sourceName, Log log) throws IOException { + this.target = target; + this.recordName = recordName; + this.sourceName = sourceName; + this.log = log; + this.previous = read(new File(target, recordName)); + } + + /// Names a path whose hash [#copy] leaves unrecorded, because the caller + /// rewrites the file after copying it and records it itself ([#record]). + /// Such a file is always part of an import, so it is never stale either. + ImportedFiles deferring(String path) { + this.deferred = path; + return this; + } + + /// Files with this name are not copied; each one met is listed in + /// [#skipped]. + ImportedFiles skipping(String fileName) { + skipNames.add(fileName); + return this; + } + + /// The paths, relative to the target, of the files [#skipping] left out. + List skipped() { + return skipped; + } + + /// Whether this import recorded `path`, by copying it or by keeping the + /// developer's edited version of it. + boolean has(String path) { + return imported.containsKey(path); + } + + /// Records `path` as holding the bytes it has now. + void record(String path) throws IOException { + imported.put(path, sha256(new File(target, path.replace('/', File.separatorChar)))); + } + + /// Copies `src` to `dest`, recording each copied file's path (relative to + /// the target, `/` separated) and content hash. Answers the number of + /// files copied. + /// + /// A file an earlier import wrote and the developer changed since (its + /// bytes no longer match the earlier record) is kept, as + /// [#removeStale] keeps one: it is recorded with its earlier hash, so the + /// next import still recognizes the edit, and deleting it lets an import + /// copy it afresh. + int copy(File src, File dest, String path) throws IOException { + if (src.isDirectory()) { + int n = 0; + File[] files = src.listFiles(); + if (files != null) { + java.util.Arrays.sort(files); + for (File f : files) { + n += copy(f, new File(dest, f.getName()), path + "/" + f.getName()); + } + } + return n; + } + if (skipNames.contains(src.getName())) { + skipped.add(path); + return 0; + } + String recorded = previous.get(path); + if (recorded != null && dest.isFile() && !recorded.equals(sha256(dest)) + && !java.util.Arrays.equals(Files.readAllBytes(src.toPath()), Files.readAllBytes(dest.toPath()))) { + log.warn("Kept " + dest + ": it was changed after the earlier import, so " + sourceName + "'s copy " + + "was not imported; delete it and import again to take that copy"); + imported.put(path, recorded); + return 0; + } + File parent = dest.getParentFile(); + if (parent != null && !parent.isDirectory() && !parent.mkdirs()) { + throw new IOException("Cannot create " + parent); + } + Files.copy(src.toPath(), dest.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + if (!path.equals(deferred)) { + imported.put(path, sha256(dest)); + } + return 1; + } + + /// Removes what an earlier import copied and this one did not, so a file + /// deleted or renamed upstream does not stay behind to be compiled or + /// packaged. Only a file listed in the earlier import's record is a + /// candidate, and only while it still holds exactly the bytes that import + /// wrote: a file the developer added, or edited after importing it, is + /// kept. Directories the removal empties go too. + void removeStale() throws IOException { + for (Map.Entry e : previous.entrySet()) { + String path = e.getKey(); + if (imported.containsKey(path) || path.equals(deferred) || path.contains("..")) { + continue; + } + File f = new File(target, path.replace('/', File.separatorChar)); + if (!f.isFile()) { + continue; + } + if (!e.getValue().equals(sha256(f))) { + log.warn("Kept " + f + ": it is no longer in " + sourceName + ", but it was changed after " + + "the earlier import"); + continue; + } + Files.delete(f.toPath()); + log.info("Removed " + f + ": it is no longer in " + sourceName); + File dir = f.getParentFile(); + while (dir != null && !dir.equals(target)) { + String[] left = dir.list(); + if (left == null || left.length > 0 || !dir.delete()) { + break; + } + dir = dir.getParentFile(); + } + } + } + + /// Writes the record of this import. + void write() throws IOException { + StringBuilder b = new StringBuilder(); + for (Map.Entry e : imported.entrySet()) { + b.append(e.getValue()).append(' ').append(e.getKey()).append('\n'); + } + if (!target.isDirectory() && !target.mkdirs()) { + throw new IOException("Cannot create " + target); + } + Files.write(new File(target, recordName).toPath(), b.toString().getBytes(Charset.forName("UTF-8"))); + } + + private static Map read(File f) throws IOException { + Map out = new TreeMap(); + if (!f.isFile()) { + return out; + } + for (String line : Files.readAllLines(f.toPath(), Charset.forName("UTF-8"))) { + int sp = line.indexOf(' '); + if (sp > 0 && sp < line.length() - 1) { + out.put(line.substring(sp + 1), line.substring(0, sp)); + } + } + return out; + } + + static String sha256(File f) throws IOException { + try { + byte[] d = MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(f.toPath())); + StringBuilder b = new StringBuilder(d.length * 2); + for (byte x : d) { + b.append(Character.forDigit((x >> 4) & 0xf, 16)).append(Character.forDigit(x & 0xf, 16)); + } + return b.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 is unavailable", e); + } + } + + /// Whether any file under `dir` is Kotlin source. + static boolean hasKotlin(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + return false; + } + for (File f : files) { + if (f.isDirectory() ? hasKotlin(f) : f.getName().endsWith(".kt")) { + return true; + } + } + return false; + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java b/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java index a1f5f30e9c8..cadcff55e2b 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java @@ -180,6 +180,17 @@ boolean owns(String internalName) { return false; } + /// Whether `source`, the text of a Java or Kotlin file, spells one of + /// this layer's packages (`javax.swing.`), in an import or anywhere else. + boolean namedInSource(String source) { + for (String p : prefixes) { + if (source.indexOf(p.replace('/', '.')) >= 0) { + return true; + } + } + return false; + } + /// The name the application was compiled against, when `relocated` is one /// this layer produced from a prefix, else null. Build messages use it: /// a developer knows `javax.swing.JTable`, not where it ships. diff --git a/maven/build-engine/src/test/java/com/codename1/maven/CompatRemapperTest.java b/maven/build-engine/src/test/java/com/codename1/maven/CompatRemapperTest.java index 95a0df7ad95..f503dc68391 100644 --- a/maven/build-engine/src/test/java/com/codename1/maven/CompatRemapperTest.java +++ b/maven/build-engine/src/test/java/com/codename1/maven/CompatRemapperTest.java @@ -395,6 +395,107 @@ public void androidAloneIsExactlyTheAndroidStep() throws Exception { assertSameTree(snapshot(direct), snapshot(generic)); } + /// An application class that uses Swing and nothing else. + private static byte[] swingOnlyClass(String name) { + ClassWriter cw = new ClassWriter(0); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name, null, "javax/swing/JFrame", null); + cw.visitEnd(); + return cw.toByteArray(); + } + + /// The templates put both desktop jars on the classpath of a project with + /// desktop sources. A Swing application must not pay for the JavaFX + /// runtime: a desktop layer ships only when the classes refer to it. + @Test + public void aDesktopLayerShipsOnlyWhenTheClassesUseIt() throws Exception { + File classes = tmp.newFolder("classes"); + assertTrue(new File(classes, "com/x").mkdirs()); + Files.write(new File(classes, "com/x/Main.class").toPath(), swingOnlyClass("com/x/Main")); + List classpath = Arrays.asList(swingJar(), javafxJar(), jdkJar()); + + CompatRemapper remapper = new CompatRemapper(classes, classpath, null, LOG); + assertEquals(Collections.singletonList(CompatLayers.SWING), remapper.activeLayers()); + assertTrue(remapper.run()); + assertTrue(new File(classes, SWING + "javax/swing/JFrame.class").isFile()); + assertFalse(new File(classes, FX).exists()); + + // Read back from the relocated classes, the answer is the same, and + // the extracted Swing runtime is not mistaken for application code. + Map first = snapshot(classes); + CompatRemapper again = new CompatRemapper(classes, classpath, null, LOG); + assertEquals(Collections.singletonList(CompatLayers.SWING), again.activeLayers()); + assertTrue(again.run()); + assertSameTree(first, snapshot(classes)); + } + + /// With both jars present and neither used, nothing is active and the + /// classes are left alone. + @Test + public void unusedDesktopJarsSwitchNothingOn() throws Exception { + File classes = tmp.newFolder("classes"); + assertTrue(new File(classes, "com/x").mkdirs()); + Files.write(new File(classes, "com/x/Plain.class").toPath(), emptyClass("com/x/Plain", "java/lang/Object")); + Map before = snapshot(classes); + + CompatRemapper remapper = new CompatRemapper(classes, Arrays.asList(swingJar(), javafxJar(), jdkJar()), + null, LOG); + assertTrue(remapper.activeLayers().isEmpty()); + assertFalse(remapper.run()); + assertSameTree(before, snapshot(classes)); + } + + /// Kotlin's classes are a directory of their own, relocated separately; + /// a layer only they use still has to ship with the main pass. + @Test + public void aLayerUsedOnlyByAHandlerDirectoryStillShips() throws Exception { + File classes = tmp.newFolder("classes"); + assertTrue(new File(classes, "com/x").mkdirs()); + Files.write(new File(classes, "com/x/Main.class").toPath(), swingOnlyClass("com/x/Main")); + File kotlin = tmp.newFolder("kotlin"); + assertTrue(new File(kotlin, "com/x").mkdirs()); + ClassWriter cw = new ClassWriter(0); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/x/View", null, "java/lang/Object", null); + cw.visitField(Opcodes.ACC_PRIVATE, "stage", "Ljavafx/stage/Stage;", null, null).visitEnd(); + cw.visitEnd(); + Files.write(new File(kotlin, "com/x/View.class").toPath(), cw.toByteArray()); + List classpath = Arrays.asList(swingJar(), javafxJar(), jdkJar()); + + CompatRemapper remapper = new CompatRemapper(classes, classpath, null, LOG) + .withHandlerDirectories(Collections.singletonList(kotlin)); + assertEquals(Arrays.asList(CompatLayers.SWING, CompatLayers.JAVAFX), remapper.activeLayers()); + assertTrue(remapper.run()); + assertTrue(new File(classes, FX + "javafx/stage/Stage.class").isFile()); + + // The Kotlin directory, on its own, is relocated by what it uses. + CompatRemapper kotlinPass = new CompatRemapper(kotlin, classpath, null, LOG).relocateOnly(); + assertEquals(Collections.singletonList(CompatLayers.JAVAFX), kotlinPass.activeLayers()); + assertTrue(kotlinPass.run()); + assertTrue(refs(Files.readAllBytes(new File(kotlin, "com/x/View.class").toPath())) + .contains("L" + FX + "javafx/stage/Stage;")); + } + + /// The Android layer keeps its rule: its jar alone switches it on. + @Test + public void theAndroidLayerIsActiveByItsJarAlone() throws Exception { + File classes = tmp.newFolder("classes"); + assertTrue(new File(classes, "com/x").mkdirs()); + Files.write(new File(classes, "com/x/Plain.class").toPath(), emptyClass("com/x/Plain", "java/lang/Object")); + assertTrue(CompatLayers.activeByPresence(AndroidRemapper.RELOCATION)); + assertFalse(CompatLayers.activeByPresence(CompatLayers.SWING)); + assertFalse(CompatLayers.activeByPresence(CompatLayers.JAVAFX)); + assertEquals(Collections.singletonList(AndroidRemapper.RELOCATION), + CompatLayers.active(Arrays.asList(androidJar(), swingJar(), javafxJar(), jdkJar()), + Collections.singletonList(classes))); + } + + @Test + public void theDesktopEntryRecordReachesTheGenerators() throws Exception { + File record = new File(tmp.getRoot(), DesktopSources.ENTRY_RECORD); + CompatRemapper remapper = new CompatRemapper(tmp.newFolder("classes"), null, null, LOG) + .withDesktopEntryRecord(record); + assertEquals(record, remapper.desktopEntryRecord()); + } + @Test public void relocateOnlyShipsNoRuntime() throws Exception { File classes = classesWithApp("classes"); diff --git a/maven/build-engine/src/test/java/com/codename1/maven/DesktopProjectImporterTest.java b/maven/build-engine/src/test/java/com/codename1/maven/DesktopProjectImporterTest.java new file mode 100644 index 00000000000..847f87afdbe --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/maven/DesktopProjectImporterTest.java @@ -0,0 +1,456 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.builders.BuildException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/// cn1:import-desktop-project over small Maven and Gradle desktop projects: +/// what is copied where, how the entry point is found and recorded, what a +/// second import does, and how the project's dependencies are reported. +public class DesktopProjectImporterTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + private final List logged = new ArrayList(); + + private final com.codename1.build.Log log = new com.codename1.build.Log() { + public void debug(CharSequence c) { } + public void debug(CharSequence c, Throwable e) { } + public void debug(Throwable e) { } + public void info(CharSequence c) { logged.add("info: " + c); } + public void info(CharSequence c, Throwable e) { } + public void info(Throwable e) { } + public void warn(CharSequence c) { logged.add("warn: " + c); } + public void warn(CharSequence c, Throwable e) { } + public void warn(Throwable e) { } + public void error(CharSequence c) { } + public void error(CharSequence c, Throwable e) { } + public void error(Throwable e) { } + public boolean isDebugEnabled() { return false; } + public boolean isInfoEnabled() { return true; } + public boolean isWarnEnabled() { return true; } + public boolean isErrorEnabled() { return true; } + }; + + private static File write(File root, String path, String text) throws IOException { + File f = new File(root, path); + assertTrue(f.getParentFile().isDirectory() || f.getParentFile().mkdirs()); + Files.write(f.toPath(), text.getBytes("UTF-8")); + return f; + } + + private static String read(File f) throws IOException { + return new String(Files.readAllBytes(f.toPath()), "UTF-8"); + } + + private static final String FX_APP = "package com.acme.fx;\n\n" + + "import javafx.application.Application;\n" + + "import javafx.stage.Stage;\n\n" + + "// class Decoy extends Application\n" + + "public class App extends Application {\n" + + " @Override\n public void start(Stage stage) {\n stage.show();\n }\n\n" + + " public static void main(String[] args) {\n launch(args);\n }\n}\n"; + + private static final String SWING_MAIN = "package com.acme.swing;\n\n" + + "import javax.swing.JFrame;\n\n" + + "public class Main {\n" + + " public static void main(final String... args) {\n" + + " new JFrame(\"class Other extends Application\").setVisible(true);\n }\n}\n"; + + /// A JavaFX Maven project with a module descriptor, FXML and a manifest. + private File javafxMavenProject() throws IOException { + File p = tmp.newFolder("fxproject"); + write(p, "pom.xml", "\n \n com.acme.fx.App\n \n" + + " \n" + + " managedonly\n" + + " \n" + + " \n" + + " org.openjfxjavafx-controls" + + "21\n" + + " \n org.openjfx\n javafx-fxml\n" + + " \n" + + " org.openjfxjavafx-web\n" + + " com.google.code.gsongson\n" + + " org.junit.jupiterjunit-jupiter" + + "test\n" + + " org.exampletestkit" + + "test\n" + + " \n" + + " javafx-maven-plugin\n" + + " plugindep" + + "\n" + + " ${main.class}\n" + + " \n\n"); + write(p, "src/main/java/module-info.java", "module com.acme.fx {\n requires javafx.controls;\n}\n"); + write(p, "src/main/java/com/acme/fx/App.java", FX_APP); + write(p, "src/main/java/com/acme/fx/Model.java", "package com.acme.fx;\n\npublic class Model {\n}\n"); + write(p, "src/main/resources/com/acme/fx/main.fxml", "\n"); + write(p, "src/main/resources/style.css", ".root { }\n"); + write(p, "src/main/resources/META-INF/MANIFEST.MF", "Main-Class: com.acme.fx.App\n"); + write(p, "src/test/java/com/acme/fx/AppTest.java", "package com.acme.fx;\n\nclass AppTest {\n}\n"); + return p; + } + + private File common() throws IOException { + File common = new File(tmp.getRoot(), "app/common"); + if (!common.isDirectory()) { + assertTrue(common.mkdirs()); + } + return common; + } + + private static Map tree(File dir) throws IOException { + Map out = new TreeMap(); + tree(dir, dir, out); + return out; + } + + private static void tree(File root, File dir, Map out) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + tree(root, f, out); + } else { + out.put(root.toURI().relativize(f.toURI()).getPath(), read(f)); + } + } + } + + @Test + public void importsAJavaFxMavenProject() throws Exception { + File source = javafxMavenProject(); + File common = common(); + DesktopProjectImporter.Result r = new DesktopProjectImporter(log).importProject(source, null, common, null); + + File desktop = new File(common, "src/main/desktop"); + assertEquals(FX_APP, read(new File(desktop, "java/com/acme/fx/App.java"))); + assertTrue(new File(desktop, "java/com/acme/fx/Model.java").isFile()); + assertTrue(new File(desktop, "resources/com/acme/fx/main.fxml").isFile()); + assertTrue(new File(desktop, "resources/style.css").isFile()); + assertEquals(4, r.copiedFiles); + // Not a module, and packaged by the Codename One build. + assertFalse(new File(desktop, "java/module-info.java").exists()); + assertEquals(Collections.singletonList("java/module-info.java"), r.droppedModuleInfo); + assertFalse(new File(desktop, "resources/META-INF").exists()); + // Tests are not the application. + assertFalse(tree(desktop).toString(), tree(desktop).toString().contains("AppTest")); + + assertEquals("com.acme.fx.App", r.mainClass); + assertEquals("javafx", r.kind); + Properties record = DesktopSources.readEntryRecord(desktop); + assertNotNull(record); + assertEquals("com.acme.fx.App", record.getProperty("mainClass")); + assertEquals("javafx", record.getProperty("kind")); + assertEquals(new File(desktop, "cn1-desktop.properties"), DesktopSources.entryRecord(desktop)); + + assertEquals(Arrays.asList("org.openjfx:javafx-controls (JavaFX controls)", "org.openjfx:javafx-fxml (FXML)"), + r.covered); + // Reported, and nothing of theirs copied: no jar, no pom entry. + assertEquals(Arrays.asList("org.openjfx:javafx-web", "com.google.code.gson:gson"), r.uncovered); + assertFalse(r.kotlin); + assertFalse(new File(common, "src/main/kotlin").exists()); + assertFalse(new File(common, "pom.xml").exists()); + } + + @Test + public void importsASwingGradleModule() throws Exception { + File source = tmp.newFolder("swingproject"); + write(source, "settings.gradle", "include 'app'\n"); + write(source, "app/build.gradle", "plugins { id 'application' }\n" + + "application {\n mainClass = 'com.acme.swing.Main'\n}\n" + + "dependencies {\n" + + " implementation 'org.swinglabs.swingx:swingx-core:1.6.5-1'\n" + + " implementation(\"com.miglayout:miglayout-swing:11.3\")\n" + + " implementation 'com.formdev:flatlaf:3.4'\n" + + " testImplementation 'junit:junit:4.13.2'\n}\n"); + write(source, "app/src/main/java/com/acme/swing/Main.java", SWING_MAIN); + write(source, "app/src/main/resources/messages.properties", "title=Hello\n"); + File common = common(); + + DesktopProjectImporter.Result r = new DesktopProjectImporter(log).importProject(source, null, common, null); + File desktop = new File(common, "src/main/desktop"); + assertEquals(SWING_MAIN, read(new File(desktop, "java/com/acme/swing/Main.java"))); + assertEquals("title=Hello\n", read(new File(desktop, "resources/messages.properties"))); + assertEquals("com.acme.swing.Main", r.mainClass); + assertEquals("swing", r.kind); + assertEquals("swing", DesktopSources.readEntryRecord(desktop).getProperty("kind")); + assertEquals(Arrays.asList("org.swinglabs.swingx:swingx-core (SwingX)", + "com.miglayout:miglayout-swing (covered by the built-in MiG layout support)"), r.covered); + assertEquals(Collections.singletonList("com.formdev:flatlaf"), r.uncovered); + assertEquals(source.getName(), DesktopProjectImporter.moduleDir(source, null).getParentFile().getName()); + assertEquals("Swing", DesktopSources.toolkitsNamedIn(desktop)); + } + + /// A launcher beside the Application subclass is how a JavaFX project is + /// made runnable from a plain jar. The build names the launcher; the + /// application is the subclass. + @Test + public void aLauncherNamedByTheBuildMeansTheApplicationClass() throws Exception { + File source = tmp.newFolder("launcher"); + write(source, "build.gradle.kts", "plugins { application; id(\"org.openjfx.javafxplugin\") }\n" + + "javafx {\n modules = listOf(\"javafx.controls\", \"javafx.media\")\n}\n" + + "application {\n mainClass.set(\"com.acme.fx.Launcher\")\n}\n" + + "dependencies {\n implementation(libs.commons.lang)\n}\n"); + write(source, "src/main/java/com/acme/fx/App.java", FX_APP); + write(source, "src/main/java/com/acme/fx/Launcher.java", "package com.acme.fx;\n\npublic class Launcher {\n" + + " public static void main(String[] args) {\n App.main(args);\n }\n}\n"); + + DesktopProjectImporter.Result r = new DesktopProjectImporter(log).importProject(source, null, common(), null); + assertEquals("com.acme.fx.App", r.mainClass); + assertEquals("javafx", r.kind); + assertEquals(Collections.singletonList("org.openjfx:javafx-controls (JavaFX controls)"), r.covered); + assertEquals(2, r.uncovered.size()); + assertEquals("org.openjfx:javafx-media", r.uncovered.get(0)); + assertTrue(r.uncovered.get(1), r.uncovered.get(1).contains("version catalog")); + assertEquals("JavaFX", DesktopSources.toolkitsNamedIn(new File(common(), "src/main/desktop"))); + } + + @Test + public void severalEntryPointsAndNoMainClassSettingFailBeforeCopying() throws Exception { + File source = tmp.newFolder("two"); + write(source, "pom.xml", "\n"); + write(source, "src/main/java/com/acme/swing/Main.java", SWING_MAIN); + write(source, "src/main/java/com/acme/swing/Tool.java", "package com.acme.swing;\n\npublic class Tool {\n" + + " static public void main(String args[]) {\n }\n}\n"); + File common = common(); + try { + new DesktopProjectImporter(log).importProject(source, null, common, null); + fail("two main classes and nothing to choose by"); + } catch (BuildException e) { + assertTrue(e.getMessage(), e.getMessage().contains("com.acme.swing.Main (swing)")); + assertTrue(e.getMessage(), e.getMessage().contains("com.acme.swing.Tool (swing)")); + assertTrue(e.getMessage(), e.getMessage().contains("-Dcn1.desktop.mainClass=")); + } + assertFalse("nothing is imported", new File(common, "src").exists()); + + DesktopProjectImporter.Result r = new DesktopProjectImporter(log) + .importProject(source, null, common, "com.acme.swing.Tool"); + assertEquals("com.acme.swing.Tool", r.mainClass); + // Imported again without the override, the recorded choice stands. + r = new DesktopProjectImporter(log).importProject(source, null, common, null); + assertEquals("com.acme.swing.Tool", r.mainClass); + + try { + new DesktopProjectImporter(log).importProject(source, null, common, "com.acme.swing.Missing"); + fail("the override names no entry point"); + } catch (BuildException e) { + assertTrue(e.getMessage(), e.getMessage().contains("com.acme.swing.Missing")); + assertTrue(e.getMessage(), e.getMessage().contains("com.acme.swing.Main")); + } + } + + @Test + public void theBuildsMainClassDecidesBetweenSeveral() throws Exception { + File source = tmp.newFolder("two"); + write(source, "pom.xml", "com.acme.swing/com.acme.swing.Tool" + + "\n"); + write(source, "src/main/java/com/acme/swing/Main.java", SWING_MAIN); + write(source, "src/main/java/com/acme/swing/Tool.java", "package com.acme.swing;\n\npublic class Tool {\n" + + " public static void main(String[] args) {\n }\n}\n"); + assertEquals(Collections.singletonList("com.acme.swing.Tool"), + DesktopProjectImporter.declaredMainClasses(source, source)); + DesktopProjectImporter.Result r = new DesktopProjectImporter(log).importProject(source, null, common(), null); + assertEquals("com.acme.swing.Tool", r.mainClass); + assertEquals("swing", r.kind); + } + + @Test + public void aProjectWithNothingToStartIsRefused() throws Exception { + File source = tmp.newFolder("library"); + write(source, "src/main/java/com/acme/Util.java", "package com.acme;\n\npublic class Util {\n}\n"); + try { + new DesktopProjectImporter(log).importProject(source, null, common(), null); + fail("no entry point"); + } catch (BuildException e) { + assertTrue(e.getMessage(), e.getMessage().contains("javafx.application.Application")); + } + assertFalse(new File(common(), "src").exists()); + } + + @Test + public void namesTheModulesWhenSeveralHaveSources() throws Exception { + File source = tmp.newFolder("multi"); + write(source, "ui/src/main/java/com/acme/swing/Main.java", SWING_MAIN); + write(source, "core/src/main/java/com/acme/Core.java", "package com.acme;\n\npublic class Core {\n}\n"); + try { + DesktopProjectImporter.moduleDir(source, null); + fail("ambiguous"); + } catch (BuildException e) { + assertTrue(e.getMessage(), e.getMessage().contains("core, ui")); + assertTrue(e.getMessage(), e.getMessage().contains("-Dcn1.desktop.module=")); + } + assertEquals(new File(source, "ui"), DesktopProjectImporter.moduleDir(source, "ui")); + try { + DesktopProjectImporter.moduleDir(source, "missing"); + fail("no such module"); + } catch (BuildException e) { + assertTrue(e.getMessage(), e.getMessage().contains("missing")); + } + DesktopProjectImporter.Result r = new DesktopProjectImporter(log).importProject(source, "ui", common(), null); + assertEquals("com.acme.swing.Main", r.mainClass); + assertFalse(new File(common(), "src/main/desktop/java/com/acme/Core.java").exists()); + } + + /// Importing again updates what the first import wrote and nothing else: + /// the developer's edits and additions stay, and a file that left the + /// source project is removed. + @Test + public void aSecondImportUpdatesOnlyUntouchedFiles() throws Exception { + File source = javafxMavenProject(); + File common = common(); + new DesktopProjectImporter(log).importProject(source, null, common, null); + File desktop = new File(common, "src/main/desktop"); + Map first = tree(desktop); + + // Unchanged source, unchanged result. + DesktopProjectImporter.Result again = new DesktopProjectImporter(log).importProject(source, null, common, null); + assertEquals(first, tree(desktop)); + assertEquals("com.acme.fx.App", again.mainClass); + + File edited = new File(desktop, "java/com/acme/fx/Model.java"); + Files.write(edited.toPath(), "package com.acme.fx;\n\npublic class Model {\n int mine;\n}\n".getBytes("UTF-8")); + File added = write(desktop, "java/com/acme/fx/Mine.java", "package com.acme.fx;\n\nclass Mine {\n}\n"); + write(source, "src/main/java/com/acme/fx/Model.java", "package com.acme.fx;\n\npublic class Model {\n" + + " int upstream;\n}\n"); + write(source, "src/main/resources/style.css", ".root { -fx-font-size: 12; }\n"); + assertTrue(new File(source, "src/main/resources/com/acme/fx/main.fxml").delete()); + write(source, "src/main/resources/com/acme/fx/other.fxml", "\n"); + + logged.clear(); + new DesktopProjectImporter(log).importProject(source, null, common, null); + assertTrue("the developer's edit is kept", read(edited).contains("int mine;")); + assertTrue(logged.toString(), logged.toString().contains("Kept " + edited)); + assertTrue("the developer's own file is kept", added.isFile()); + assertEquals(".root { -fx-font-size: 12; }\n", read(new File(desktop, "resources/style.css"))); + assertFalse("removed upstream, untouched here", new File(desktop, "resources/com/acme/fx/main.fxml").exists()); + assertTrue(new File(desktop, "resources/com/acme/fx/other.fxml").isFile()); + + // Deleting the kept file takes the project's copy on the next import. + assertTrue(edited.delete()); + new DesktopProjectImporter(log).importProject(source, null, common, null); + assertTrue(read(edited).contains("int upstream;")); + } + + @Test + public void kotlinSourcesSwitchTheKotlinBuildOn() throws Exception { + File source = tmp.newFolder("kt"); + write(source, "src/main/kotlin/com/acme/kt/App.kt", "package com.acme.kt\n\n" + + "import javafx.application.Application\nimport javafx.stage.Stage\n\n" + + "class App : Application() {\n override fun start(stage: Stage) {\n stage.show()\n }\n}\n"); + write(source, "src/main/kotlin/com/acme/kt/main.kt", "package com.acme.kt\n\n" + + "fun main(args: Array) {\n Application.launch(App::class.java, *args)\n}\n"); + write(source, "build.gradle.kts", "application {\n mainClass.set(\"com.acme.kt.MainKt\")\n}\n" + + "dependencies {\n implementation(\"org.jetbrains.kotlin:kotlin-stdlib:2.0.0\")\n}\n"); + File common = common(); + DesktopProjectImporter.Result r = new DesktopProjectImporter(log).importProject(source, null, common, null); + assertTrue(r.kotlin); + assertTrue(new File(common, "src/main/kotlin/.keep").isFile()); + assertTrue(new File(common, "src/main/desktop/kotlin/com/acme/kt/App.kt").isFile()); + // The facade class is a launcher; the application is the subclass. + assertEquals("com.acme.kt.App", r.mainClass); + assertEquals("javafx", r.kind); + assertEquals(Collections.singletonList("org.jetbrains.kotlin:kotlin-stdlib (Kotlin standard library)"), + r.covered); + } + + @Test + public void findsEntryPointsInSourceText() { + Map out = new TreeMap(); + DesktopProjectImporter.scanJava("App", FX_APP, out); + assertEquals(Collections.singleton("com.acme.fx.App"), out.keySet()); + assertTrue(out.get("com.acme.fx.App").javafx); + + // Another class called Application is not JavaFX's. + out.clear(); + DesktopProjectImporter.scanJava("Server", "package a;\nimport org.other.Application;\n" + + "public class Server extends Application {\n}\n", out); + assertTrue(out.toString(), out.isEmpty()); + + out.clear(); + DesktopProjectImporter.scanJava("Outer", "import javafx.application.*;\n" + + "public class Outer {\n public static class Inner extends Application {\n }\n" + + " void main(String[] a) {\n }\n}\n", out); + assertEquals(Collections.singleton("Outer$Inner"), out.keySet()); + + out.clear(); + DesktopProjectImporter.scanJava("Q", "package q;\npublic class Q extends javafx.application.Application {\n}\n", + out); + assertTrue(out.get("q.Q").javafx); + + out.clear(); + DesktopProjectImporter.scanKotlin("Tool", "@file:JvmName(\"Start\")\npackage k\n\nfun main() {\n}\n", out); + assertEquals(Collections.singleton("k.Start"), out.keySet()); + assertFalse(out.get("k.Start").javafx); + + out.clear(); + DesktopProjectImporter.scanKotlin("Boot", "package k\n\nobject Boot {\n @JvmStatic fun main(a: Array) {\n" + + " }\n}\n", out); + assertEquals(Collections.singleton("k.Boot"), out.keySet()); + assertNull(out.get("k.BootKt")); + } + + @Test + public void desktopSourcesWithoutARuntimeAreRefused() throws Exception { + File desktop = tmp.newFolder("desktop"); + try { + DesktopSources.requireRuntime(desktop, Arrays.asList("codenameone-core", "java-runtime")); + fail("neither runtime is a dependency"); + } catch (BuildException e) { + assertTrue(e.getMessage(), e.getMessage().contains("codenameone-swing-compat")); + assertTrue(e.getMessage(), e.getMessage().contains("codenameone-javafx-compat")); + } + DesktopSources.requireRuntime(desktop, Arrays.asList("codenameone-core", "codenameone-javafx-compat")); + DesktopSources.requireRuntime(desktop, Collections.singletonList("codenameone-swing-compat")); + // No desktop sources, nothing to require. + DesktopSources.requireRuntime(new File(desktop, "missing"), Collections.emptyList()); + assertFalse(DesktopSources.isDesktopProject(new File(desktop, "missing"))); + assertTrue(DesktopSources.isDesktopProject(desktop)); + assertEquals("", DesktopSources.toolkitsNamedIn(desktop)); + assertNull(DesktopSources.readEntryRecord(desktop)); + } +} diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml index eba76774886..5edfc3141ea 100644 --- a/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml @@ -96,7 +96,7 @@ + and remap-compat goals below are no-ops without this directory. --> android-compat @@ -112,6 +112,32 @@ + + + desktop-compat + + + ${basedir}/src/main/desktop + + + + + com.codenameone + codenameone-swing-compat + provided + + + com.codenameone + codenameone-javafx-compat + provided + + + @@ -194,6 +220,10 @@ ${project.basedir}/src/main/android/kotlin ${project.basedir}/src/main/android/java ${project.build.directory}/generated-sources/android + + ${project.basedir}/src/main/desktop/kotlin + ${project.basedir}/src/main/desktop/java -no-reflect @@ -403,6 +433,15 @@ compile-android-res + + + prepare-desktop-sources + generate-sources + + prepare-desktop-sources + + generate-gui-sources process-sources @@ -414,9 +453,10 @@ cn1-process-classes process-classes - - remap-android + + remap-compat bytecode-compliance css + + + + + + @@ -53,10 +62,10 @@ - - + diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java index 121ae0785db..217b892a936 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/AppSupport.java @@ -93,6 +93,17 @@ static void apply(final Project project, final ProjectLayout layout, final Coden // step copies the runtime itself into the application's classes. addFramework(project, "compileOnly", version, "codenameone-android-compat"); } + // Swing and JavaFX compatibility: src/main/desktop existing is what + // enables the layers. Both jars, because the directory does not say + // which toolkit its sources use; the remap step ships only the + // runtime the compiled classes refer to. Swing code compiles against + // the JDK's own java.awt and javax.swing, JavaFX code against the + // javafx.* API in codenameone-javafx-compat. + final File desktopDir = layout.desktopSourceDir(); + final boolean desktopProject = com.codename1.maven.DesktopSources.isDesktopProject(desktopDir); + if (desktopProject) { + addFramework(project, "compileOnly", version, "codenameone-swing-compat", "codenameone-javafx-compat"); + } addFramework(project, "testImplementation", version, "codenameone-core", "codenameone-javase"); Configuration framework = resolvable(project, "cn1Framework", "codenameone-core and java-runtime, for local builds"); framework.setTransitive(false); @@ -161,6 +172,20 @@ static void apply(final Project project, final ProjectLayout layout, final Coden main.getResources().srcDir(androidRes.flatMap( com.codename1.gradle.tasks.CompileAndroidResTask::getResourcesDirectory)); + // The desktop sources, laid out like a desktop project's src/main. + // Declared always, like the Android directories: Gradle is happy with + // a directory that does not exist. + // + // EXTENSION POINT: the build-time FXML and CSS compile step belongs + // here, as a task registered the way compileAndroidRes is above -- + // inputs resourcesDir(desktopDir), outputs a generated-sources and a + // generated-resources directory added to `main` with srcDir(task.flatMap(...)). + // The Maven build's counterpart, with the full contract, is + // PrepareDesktopSourcesMojo.compileDesktopResources. + main.getJava().srcDir(com.codename1.maven.DesktopSources.javaDir(desktopDir)); + main.getResources().srcDir(com.codename1.maven.DesktopSources.resourcesDir(desktopDir)); + final File desktopEntry = com.codename1.maven.DesktopSources.entryRecord(desktopDir); + TaskProvider css = project.getTasks().register("cn1Css", Cn1CssTask.class, t -> { common(t, project, layout, ext, userProperties); t.setDescription("Compiles src/main/css into theme.res"); @@ -203,13 +228,25 @@ static void apply(final Project project, final ProjectLayout layout, final Coden // compileJava up to date and uploaded classes nobody had checked. final String skipInput = String.valueOf(skip); project.getTasks().named(main.getCompileJavaTaskName(), JavaCompile.class, compile -> { - if (androidProject) { + if (androidProject || desktopProject) { // Relocation rewrites this task's output descriptors. A source edit // must compile all Java sources against the original Android API, // never against unchanged, relocated classes from the last build. // Unchanged builds still use Gradle's task up-to-date checks. compile.getOptions().setIncremental(false); } + if (desktopProject) { + // Which desktop runtime ships is read from the compiled classes, + // Kotlin's included, by an action of this task -- and a Kotlin + // edit that leaves its ABI alone leaves this task up to date. + // The toolkits the sources name are therefore an input: the + // first Kotlin line to use JavaFX reruns this task, and the + // action ships the runtime. The entry record is one for the + // same reason: the entry point is generated from it. + compile.getInputs().property("cn1DesktopToolkits", project.provider( + () -> com.codename1.maven.DesktopSources.toolkitsNamedIn(desktopDir))); + compile.getInputs().files(desktopEntry).withPropertyName("cn1DesktopEntry").optional(); + } compile.getInputs().property("cn1SkipComplianceCheck", skipInput); processingInputs(compile, layout, userProperties); Provider> roots = project.provider(() -> sourceRoots(main, layout)); @@ -218,12 +255,15 @@ static void apply(final Project project, final ProjectLayout layout, final Coden // poms bind process-classes in. // Kotlin's classes (compiled first, into a directory of their own) are // the Java pass's siblings, so Java calling Kotlin resolves. - // Android code is relocated onto the compatibility runtime before - // the compliance check sees it, as remap-android precedes - // bytecode-compliance in the Maven build. - compile.doLast("cn1RemapAndroid", new com.codename1.gradle.tasks.RemapAndroidAction( + // Android, Swing and JavaFX code is relocated onto the + // compatibility runtimes before the compliance check sees it, as + // remap-compat precedes bytecode-compliance in the Maven build. + // For Swing no other order works: its names resolve against + // nothing until they are relocated. + compile.doLast("cn1RemapCompat", new com.codename1.gradle.tasks.RemapCompatAction( compile.getDestinationDirectory().getAsFile().get(), main.getCompileClasspath(), - onClickNamesFile(androidState), false, main.getOutput().getClassesDirs())); + onClickNamesFile(androidState), false, main.getOutput().getClassesDirs()) + .withDesktopEntryRecord(desktopEntry)); compile.doLast("cn1Compliance", new com.codename1.gradle.tasks.ComplianceAction(layout.rootDir(), layout.projectDir(), compile.getDestinationDirectory().getAsFile(), project.getName(), main.getCompileClasspath(), compileArtifacts, complianceProperties) @@ -262,6 +302,8 @@ static void apply(final Project project, final ProjectLayout layout, final Coden Object kotlinSources = ((org.gradle.api.plugins.ExtensionAware) main).getExtensions().findByName("kotlin"); if (kotlinSources instanceof org.gradle.api.file.SourceDirectorySet) { ((org.gradle.api.file.SourceDirectorySet) kotlinSources).srcDir(new File(androidDir, "kotlin")); + ((org.gradle.api.file.SourceDirectorySet) kotlinSources).srcDir( + com.codename1.maven.DesktopSources.kotlinDir(desktopDir)); } // Kotlin's classes are relocated after javac rather than right after // Kotlin compiles: they are javac's classpath, and Java calling a @@ -275,9 +317,25 @@ static void apply(final Project project, final ProjectLayout layout, final Coden // SourceSet, so the action must not reach the classpath through one. final org.gradle.api.file.FileCollection compileClasspath = main.getCompileClasspath(); final File onClickNames = onClickNamesFile(androidState); + // A module with desktop sources may have no Java source at all, + // and then javac's pass, which ships the runtimes, never runs: + // Kotlin's directory ships them instead. An Android module always + // has Java (its generated R classes), so nothing changes for it. + final org.gradle.api.file.FileCollection javaSources = desktopProject ? main.getJava().getAsFileTree() : null; + // The Swing layer's classes exist only under their relocated + // names, so Kotlin's classes cannot be checked as Kotlin wrote + // them (see the compileKotlin actions below): for a module with + // desktop sources the check runs here instead, after relocation. + final com.codename1.gradle.tasks.ComplianceAction kotlinCompliance = !desktopProject ? null + : new com.codename1.gradle.tasks.ComplianceAction(layout.rootDir(), layout.projectDir(), + kotlinDir, project.getName(), compileClasspath, compileArtifacts, complianceProperties) + .withBuildDirectory(project.getLayout().getBuildDirectory().getAsFile()) + .withSiblingClasses(main.getOutput().getClassesDirs()); registerKotlinRelocation(project.getTasks(), main.getCompileJavaTaskName(), main.getClassesTaskName(), - t -> new com.codename1.gradle.tasks.RemapAndroidAction( - kotlinDir.get(), compileClasspath, onClickNames, true, null).execute(t)); + kotlinRelocation(t -> new com.codename1.gradle.tasks.RemapCompatAction( + kotlinDir.get(), compileClasspath, onClickNames, true, null) + .withDesktopEntryRecord(desktopEntry).shippingWhenEmpty(javaSources).execute(t), + kotlinCompliance)); }); project.getPluginManager().withPlugin("org.jetbrains.kotlin.jvm", kotlin -> project.getTasks().named("compileKotlin").configure(compile -> { @@ -287,12 +345,16 @@ static void apply(final Project project, final ProjectLayout layout, final Coden Provider> roots = project.provider(() -> sourceRoots(main, layout)); // Kotlin's classes are relocated after javac (see below), not // here; this check accepts their unrelocated names, which the - // compatibility jar provides. - compile.doLast("cn1Compliance", new com.codename1.gradle.tasks.ComplianceAction( - layout.rootDir(), layout.projectDir(), kotlinClasses, project.getName(), - main.getCompileClasspath(), compileArtifacts, complianceProperties) - .withBuildDirectory(project.getLayout().getBuildDirectory().getAsFile()) - .withPendingJavaSources(main.getJava().getSrcDirs())); + // compatibility jar provides. The Swing jar provides none -- + // it is written under the relocated names -- so a module with + // desktop sources is checked by the relocation task instead. + if (!desktopProject) { + compile.doLast("cn1Compliance", new com.codename1.gradle.tasks.ComplianceAction( + layout.rootDir(), layout.projectDir(), kotlinClasses, project.getName(), + main.getCompileClasspath(), compileArtifacts, complianceProperties) + .withBuildDirectory(project.getLayout().getBuildDirectory().getAsFile()) + .withPendingJavaSources(main.getJava().getSrcDirs())); + } compile.doLast("processCn1Annotations", new ProcessAnnotationsAction(kotlinClasses, stubsDir(layout), layout.projectDir(), layout.settingsFile(), roots, "UTF-8", userProperties, @@ -713,7 +775,7 @@ static Configuration resolvable(Project project, String name, String description static TaskProvider registerKotlinRelocation(org.gradle.api.tasks.TaskContainer tasks, String compileJavaName, String classesName, org.gradle.api.Action remap) { TaskProvider relocate = tasks.register("cn1RemapAndroidKotlin", t -> { - t.setDescription("Relocates Android references in Kotlin's classes onto the compatibility runtime"); + t.setDescription("Relocates Kotlin's classes onto the compatibility runtimes"); t.dependsOn("compileKotlin"); t.mustRunAfter(compileJavaName); t.doLast(remap); @@ -723,6 +785,21 @@ static TaskProvider registerKotlinRelocation(org.gradle.api return relocate; } + /// What the Kotlin relocation task does: relocates Kotlin's classes, then, + /// when `compliance` is given, checks them. In that order, which is the + /// only one the Swing layer allows -- its API exists nowhere under the + /// names an application is compiled against. + static org.gradle.api.Action kotlinRelocation( + final org.gradle.api.Action remap, + final org.gradle.api.Action compliance) { + return t -> { + remap.execute(t); + if (compliance != null) { + compliance.execute(t); + } + }; + } + static Provider kotlinDestinationProvider(final org.gradle.api.Task compile, final ProjectLayout layout) { try { Object dir = compile.getClass().getMethod("getDestinationDirectory").invoke(compile); diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapAndroidAction.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapCompatAction.java similarity index 53% rename from maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapAndroidAction.java rename to maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapCompatAction.java index 3934f13b92a..2ecea9d8d77 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapAndroidAction.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/RemapCompatAction.java @@ -25,7 +25,8 @@ import com.codename1.build.Log; import com.codename1.builders.BuildException; import com.codename1.gradle.GradleLog; -import com.codename1.maven.AndroidRemapper; +import com.codename1.maven.CompatLayers; +import com.codename1.maven.CompatRemapper; import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Task; @@ -34,21 +35,28 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Set; -/// Relocates Android code in a compiled class directory onto the -/// compatibility runtime; see [AndroidRemapper]. Attached to the compile task -/// before the compliance check, as the Maven build binds `remap-android` -/// before `bytecode-compliance`. -public class RemapAndroidAction implements Action { +/// Relocates a compiled class directory onto whichever compatibility layers +/// the application has switched on -- Android, Swing, JavaFX -- and ships +/// their runtimes; see [CompatRemapper]. Attached to the compile task before +/// the compliance check, as the Maven build binds `remap-compat` before +/// `bytecode-compliance`. +/// +/// For an application with only Android sources it does what the Android +/// step alone does, file for file; `CompatRemapperTest` holds that. +public class RemapCompatAction implements Action { private final File classesDir; private final FileCollection compileClasspath; private final File onClickNames; private final boolean relocateOnly; private final FileCollection handlerDirs; + private File desktopEntry; + private FileCollection shipWhenEmpty; - public RemapAndroidAction(File classesDir, FileCollection compileClasspath, File onClickNames, - boolean relocateOnly, FileCollection handlerDirs) { + public RemapCompatAction(File classesDir, FileCollection compileClasspath, File onClickNames, + boolean relocateOnly, FileCollection handlerDirs) { this.classesDir = classesDir; this.compileClasspath = compileClasspath; this.onClickNames = onClickNames; @@ -56,23 +64,36 @@ public RemapAndroidAction(File classesDir, FileCollection compileClasspath, File this.handlerDirs = handlerDirs; } + /// The record of the desktop application's entry point + /// (`src/main/desktop/cn1-desktop.properties`), for the entry point + /// generators. + public RemapCompatAction withDesktopEntryRecord(File record) { + this.desktopEntry = record; + return this; + } + + /// Makes a relocate-only action a full one when `javaSources` turns out + /// to be empty. Kotlin's directory is relocated only, because javac's + /// pass ships the runtimes and generates what has to be generated -- but + /// `compileJava` does nothing at all for a module with no Java source, + /// and then this directory is the only one there is to ship them in. + public RemapCompatAction shippingWhenEmpty(FileCollection javaSources) { + this.shipWhenEmpty = javaSources; + return this; + } + @Override public void execute(Task task) { - File jar = null; - for (File f : compileClasspath.getFiles()) { - if (f.getName().startsWith(com.codename1.maven.AndroidResourceRunner.COMPAT_ARTIFACT + "-") - && f.getName().endsWith(".jar")) { - jar = f; - } - } - if (jar == null || !classesDir.isDirectory()) { + Set classpath = compileClasspath.getFiles(); + // By the jars alone, as the first test: with none there is nothing to + // read the classes for. + if (CompatLayers.active(classpath).isEmpty() || !classesDir.isDirectory()) { return; } Log log = new GradleLog(task.getLogger()); - AndroidRemapper r = new AndroidRemapper(classesDir, jar, onClickNames, log); - r.withSupportJars(java.util.Collections.singletonList( - com.codename1.maven.CompatLayers.jdkJar(compileClasspath.getFiles()))); - if (relocateOnly) { + CompatRemapper r = new CompatRemapper(classesDir, classpath, onClickNames, log) + .withDesktopEntryRecord(desktopEntry); + if (relocateOnly && !(shipWhenEmpty != null && shipWhenEmpty.isEmpty())) { r.relocateOnly(); } else if (handlerDirs != null) { List dirs = new ArrayList(handlerDirs.getFiles()); diff --git a/maven/codenameone-gradle-plugin/src/test/java/com/codename1/gradle/KotlinRelocationWiringTest.java b/maven/codenameone-gradle-plugin/src/test/java/com/codename1/gradle/KotlinRelocationWiringTest.java index fe6a89cd941..183c6cbf6db 100644 --- a/maven/codenameone-gradle-plugin/src/test/java/com/codename1/gradle/KotlinRelocationWiringTest.java +++ b/maven/codenameone-gradle-plugin/src/test/java/com/codename1/gradle/KotlinRelocationWiringTest.java @@ -158,4 +158,38 @@ void kotlinIsRelocatedByATaskThatDoesNotDependOnJavacRunning() { assertTrue(recorded.get("classes").dependsOn.contains(relocate), "every Codename One task depends on classes, so it waits for relocation"); } + + /// The Swing runtime is written under the names it ships with, so + /// Kotlin's classes can only be checked once they are relocated. The + /// relocation task then does both, in that order, and a module that + /// checks Kotlin where it is compiled passes no check here at all. + @Test + void complianceFollowsRelocationInTheSameTask() { + final List ran = new ArrayList(); + Action remap = t -> ran.add("remap"); + Action compliance = t -> ran.add("compliance"); + + AppSupport.kotlinRelocation(remap, compliance).execute(task("cn1RemapAndroidKotlin")); + assertEquals(Arrays.asList("remap", "compliance"), ran); + + ran.clear(); + AppSupport.kotlinRelocation(remap, null).execute(task("cn1RemapAndroidKotlin")); + assertEquals(Arrays.asList("remap"), ran); + } + + /// A failed relocation leaves classes the check would misreport. + @Test + void aFailedRelocationSkipsTheCheck() { + final List ran = new ArrayList(); + Action remap = t -> { + throw new IllegalStateException("remap"); + }; + Action compliance = t -> ran.add("compliance"); + try { + AppSupport.kotlinRelocation(remap, compliance).execute(task("cn1RemapAndroidKotlin")); + throw new AssertionError("the failure must reach Gradle"); + } catch (IllegalStateException expected) { + assertTrue(ran.isEmpty()); + } + } } diff --git a/maven/codenameone-maven-plugin/pom.xml b/maven/codenameone-maven-plugin/pom.xml index a1ada47b0ab..0dac23abcd9 100644 --- a/maven/codenameone-maven-plugin/pom.xml +++ b/maven/codenameone-maven-plugin/pom.xml @@ -290,6 +290,22 @@ provided true + + + com.codenameone + codenameone-swing-compat + ${project.version} + provided + true + + + com.codenameone + codenameone-javafx-compat + ${project.version} + provided + true + com.codenameone diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/AndroidPomUpdater.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/AndroidPomUpdater.java index 27a576ea4aa..0b418ff680a 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/AndroidPomUpdater.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/AndroidPomUpdater.java @@ -22,17 +22,16 @@ */ package com.codename1.maven; -import java.util.ArrayList; import java.util.List; /// Wires Android compatibility into an existing application's common /// `pom.xml`, the way a new project from the archetype already is: the -/// runtime dependency, `compile-android-res`, and `remap-android` ahead of +/// runtime dependency, `compile-android-res`, and the remap goal ahead of /// `bytecode-compliance` (which must see the relocated classes). /// /// The pom is edited as text, at anchors every archetype-generated common pom -/// has; a pom without them is reported with the snippets to add by hand -/// rather than guessed at. +/// has ([PomWiring]); a pom without them is reported with the snippets to add +/// by hand rather than guessed at. final class AndroidPomUpdater { static final String PROFILE = " \n" @@ -61,64 +60,33 @@ final class AndroidPomUpdater { + " \n" + " \n"; - static final String REMAP_GOAL = "remap-android"; - private static final String COMPLIANCE_GOAL = "bytecode-compliance"; - private static final String PLUGIN = "codenameone-maven-plugin"; + /// The goal a pom without one receives; one that binds the goal under its + /// earlier name, `remap-android`, is already wired. + static final String REMAP_GOAL = PomWiring.REMAP_GOAL; /// The updated pom, or the same text when it is already wired. final String pom; /// What could not be added automatically, each with the XML to add. - final List manual = new ArrayList(); - boolean changed; + final List manual; + final boolean changed; AndroidPomUpdater(String pom, boolean kotlin) { - String p = pom; - if (p.indexOf("codenameone-android-compat") < 0) { - int profiles = p.lastIndexOf(""); - if (profiles < 0) { - manual.add("the codenameone-android-compat dependency (scope provided), e.g. as this profile:\n" + PROFILE); - } else { - int lineStart = p.lastIndexOf('\n', profiles) + 1; - p = p.substring(0, lineStart) + PROFILE + p.substring(lineStart); - changed = true; - } + PomWiring w = new PomWiring(pom); + if (!w.has("codenameone-android-compat")) { + w.addProfile("the codenameone-android-compat dependency (scope provided), e.g. as this profile:\n" + + PROFILE, PROFILE); } - if (p.indexOf("compile-android-res") < 0) { - int plugin = p.indexOf(PLUGIN); - // Only this plugin's own executions block: a later plugin's would - // otherwise receive a goal it does not have. - int pluginEnd = plugin < 0 ? -1 : p.indexOf("", plugin); - int executions = pluginEnd < 0 ? -1 : p.indexOf("", plugin); - if (executions > pluginEnd) { - executions = -1; - } - if (executions < 0) { - manual.add("an execution of compile-android-res in the codenameone-maven-plugin:\n" + EXECUTION); - } else { - int at = executions + "".length(); - p = p.substring(0, at) + "\n" + EXECUTION.substring(0, EXECUTION.length() - 1) + p.substring(at); - changed = true; - } - } - if (p.indexOf(REMAP_GOAL) < 0) { - int compliance = p.indexOf(COMPLIANCE_GOAL); - if (compliance < 0 || p.indexOf(COMPLIANCE_GOAL, compliance + 1) >= 0) { - manual.add("the remap-android goal, in the process-classes execution, before bytecode-compliance:\n" - + " " + REMAP_GOAL + "\n"); - } else { - int lineStart = p.lastIndexOf('\n', compliance) + 1; - String indent = p.substring(lineStart, compliance); - p = p.substring(0, compliance) + REMAP_GOAL + "\n" + indent + p.substring(compliance); - changed = true; - } - } - if (kotlin && p.indexOf("src/main/android/java") < 0) { - manual.add("the Android source directories in the Kotlin profile's compile execution, which must also " + w.addExecution("compile-android-res", EXECUTION); + w.addRemapGoal(); + if (kotlin && !w.has("src/main/android/java")) { + w.manual.add("the Android source directories in the Kotlin profile's compile execution, which must also " + "run in the process-resources phase so it precedes javac:\n" + " ${project.basedir}/src/main/android/kotlin\n" + " ${project.basedir}/src/main/android/java\n" + " ${project.build.directory}/generated-sources/android\n"); } - this.pom = p; + this.pom = w.pom; + this.manual = w.manual; + this.changed = w.changed; } } diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DesktopPomUpdater.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DesktopPomUpdater.java new file mode 100644 index 00000000000..4977399e2e1 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DesktopPomUpdater.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import java.util.List; + +/// Wires the desktop compatibility layers into an existing application's +/// common `pom.xml`, the way a new project from the archetype already is: the +/// two runtime dependencies, `prepare-desktop-sources`, and the remap goal +/// ahead of `bytecode-compliance` (which must see the relocated classes). +/// +/// Edited as text at the anchors every archetype-generated common pom has, +/// exactly as [AndroidPomUpdater] does and through the same code +/// ([PomWiring]); a pom without them is reported with the snippets to add by +/// hand rather than guessed at. +final class DesktopPomUpdater { + + static final String PROFILE = " \n" + + " \n" + + " desktop-compat\n" + + " \n" + + " \n" + + " ${basedir}/src/main/desktop\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " com.codenameone\n" + + " codenameone-swing-compat\n" + + " ${cn1.version}\n" + + " provided\n" + + " \n" + + " \n" + + " com.codenameone\n" + + " codenameone-javafx-compat\n" + + " ${cn1.version}\n" + + " provided\n" + + " \n" + + " \n" + + " \n"; + + static final String EXECUTION = " \n" + + " prepare-desktop-sources\n" + + " generate-sources\n" + + " \n" + + " prepare-desktop-sources\n" + + " \n" + + " \n"; + + /// The updated pom, or the same text when it is already wired. + final String pom; + /// What could not be added automatically, each with the XML to add. + final List manual; + final boolean changed; + + DesktopPomUpdater(String pom, boolean kotlin) { + PomWiring w = new PomWiring(pom); + // Either jar says the profile is there; a pom carrying only one was + // edited by hand and is left as its author wrote it. + if (!w.has("codenameone-swing-compat") + && !w.has("codenameone-javafx-compat")) { + w.addProfile("the codenameone-swing-compat and codenameone-javafx-compat dependencies (scope provided), " + + "e.g. as this profile:\n" + PROFILE, PROFILE); + } + w.addExecution("prepare-desktop-sources", EXECUTION); + w.addRemapGoal(); + if (kotlin && !w.has("src/main/desktop/java")) { + w.manual.add("the desktop source directories in the Kotlin profile's compile execution, which must also " + + "run in the process-resources phase so it precedes javac:\n" + + " ${project.basedir}/src/main/desktop/kotlin\n" + + " ${project.basedir}/src/main/desktop/java\n"); + } + this.pom = w.pom; + this.manual = w.manual; + this.changed = w.changed; + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportDesktopProjectMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportDesktopProjectMojo.java new file mode 100644 index 00000000000..55f1cf02b4b --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/ImportDesktopProjectMojo.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import java.io.File; + +/// Imports a Swing or JavaFX project into this Codename One application. +/// +/// ``` +/// mvn cn1:import-desktop-project -Dcn1.desktop.import=/path/to/MyDesktopApp +/// [-Dcn1.desktop.module=app] [-Dcn1.desktop.mainClass=com.example.Main] +/// ``` +/// +/// Copies the project's (or the named module's) `src/main/java`, +/// `src/main/kotlin` and `src/main/resources` into +/// `common/src/main/desktop`, records how the application starts in +/// `cn1-desktop.properties` there, and lists the project's dependencies the +/// desktop layers do and do not cover. Build and run as usual afterwards. +/// See [DesktopProjectImporter]. +@Mojo(name = "import-desktop-project", requiresProject = true, aggregator = true) +public class ImportDesktopProjectMojo extends AbstractCN1Mojo { + + /// A Maven or Gradle desktop project, or one module of it. + @Parameter(property = "cn1.desktop.import", required = true) + private File source; + + /// The module to import, for a project whose root has no sources and + /// more than one module that does. + @Parameter(property = "cn1.desktop.module") + private String module; + + /// The class the application starts through, when the sources and the + /// project's build leave more than one to choose from. + @Parameter(property = "cn1.desktop.mainClass") + private String mainClass; + + @Override + protected void executeImpl() throws MojoExecutionException, MojoFailureException { + File common = getCN1ProjectDir(); + if (common == null) { + throw new MojoExecutionException("Run this from a Codename One application project"); + } + DesktopProjectImporter.Result r; + try { + wireDesktopCompat(common, DesktopProjectImporter.moduleDir(source, module)); + r = new DesktopProjectImporter(MavenLog.of(getLog())).importProject(source, module, common, mainClass); + } catch (com.codename1.builders.BuildException e) { + throw new MojoFailureException(e.getMessage(), e); + } + getLog().info("Imported " + r.copiedFiles + " files into " + new File(common, "src/main/desktop")); + if (r.mainClass != null) { + getLog().info("The application starts through " + r.mainClass + " (" + r.kind + ")"); + } + for (String dropped : r.droppedModuleInfo) { + getLog().info(" left out: " + dropped + " (a Codename One application is not a Java module)"); + } + for (String c : r.covered) { + getLog().info(" supported: " + c); + } + for (String u : r.uncovered) { + getLog().warn(" NOT supplied by the desktop compatibility layers, and not copied: " + u); + } + } + + /// A project generated before the desktop layers existed has none of + /// their wiring, and the imported sources would not build. Adds it to the + /// common pom, or stops before copying anything, naming what to add. + private void wireDesktopCompat(File common, File moduleDir) throws MojoExecutionException, MojoFailureException { + File pomFile = new File(common, "pom.xml"); + String pom; + try { + pom = new String(java.nio.file.Files.readAllBytes(pomFile.toPath()), "UTF-8"); + } catch (java.io.IOException e) { + throw new MojoExecutionException("Cannot read " + pomFile, e); + } + DesktopPomUpdater u = new DesktopPomUpdater(pom, hasKotlin(new File(moduleDir, "src/main"))); + if (!u.manual.isEmpty()) { + StringBuilder sb = new StringBuilder("Nothing was imported: " + pomFile + + " needs the desktop compatibility layers wired in by hand. Add:\n"); + for (String m : u.manual) { + sb.append("\n- ").append(m); + } + throw new MojoFailureException(sb.toString()); + } + if (u.changed) { + try { + java.nio.file.Files.write(pomFile.toPath(), u.pom.getBytes("UTF-8")); + } catch (java.io.IOException e) { + throw new MojoExecutionException("Cannot write " + pomFile, e); + } + getLog().info("Added the desktop compatibility layers to " + pomFile); + } + } + + private static boolean hasKotlin(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + return false; + } + for (File f : files) { + if (f.isDirectory() ? hasKotlin(f) : f.getName().endsWith(".kt")) { + return true; + } + } + return false; + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/PomWiring.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/PomWiring.java new file mode 100644 index 00000000000..b1482357c96 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/PomWiring.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import java.util.ArrayList; +import java.util.List; + +/// The text edits that wire a compatibility layer into an application's +/// common `pom.xml`, shared by [AndroidPomUpdater] and [DesktopPomUpdater]. +/// +/// The pom is edited as text, at anchors every archetype-generated common pom +/// has, so its comments and formatting survive; where an anchor is missing +/// nothing is guessed, and the snippet to add by hand is recorded instead. +final class PomWiring { + + /// The goal that relocates every active layer, and the name it had while + /// Android was the only one. Either in a pom means the step is bound. + static final String REMAP_GOAL = "remap-compat"; + static final String LEGACY_REMAP_GOAL = "remap-android"; + private static final String COMPLIANCE_GOAL = "bytecode-compliance"; + private static final String PLUGIN = "codenameone-maven-plugin"; + + /// The pom as edited so far. + String pom; + /// What could not be added automatically, each with the XML to add. + final List manual = new ArrayList(); + boolean changed; + + PomWiring(String pom) { + this.pom = pom; + } + + boolean has(String text) { + return pom.indexOf(text) >= 0; + } + + /// Adds `profile` as the last of the pom's profiles; `manualText` is what + /// to report when the pom has no `` to add it to. + void addProfile(String manualText, String profile) { + int profiles = pom.lastIndexOf(""); + if (profiles < 0) { + manual.add(manualText); + } else { + int lineStart = pom.lastIndexOf('\n', profiles) + 1; + pom = pom.substring(0, lineStart) + profile + pom.substring(lineStart); + changed = true; + } + } + + /// Adds `execution` to the Codename One plugin's executions unless some + /// execution already runs `goal`. + void addExecution(String goal, String execution) { + if (has("" + goal + "")) { + return; + } + int plugin = pom.indexOf(PLUGIN); + // Only this plugin's own executions block: a later plugin's would + // otherwise receive a goal it does not have. + int pluginEnd = plugin < 0 ? -1 : pom.indexOf("", plugin); + int executions = pluginEnd < 0 ? -1 : pom.indexOf("", plugin); + if (executions > pluginEnd) { + executions = -1; + } + if (executions < 0) { + manual.add("an execution of " + goal + " in the codenameone-maven-plugin:\n" + execution); + } else { + int at = executions + "".length(); + pom = pom.substring(0, at) + "\n" + execution.substring(0, execution.length() - 1) + pom.substring(at); + changed = true; + } + } + + /// Binds the remap goal immediately before `bytecode-compliance`, unless + /// the pom already binds it under either of its names. + void addRemapGoal() { + if (has(REMAP_GOAL) || has(LEGACY_REMAP_GOAL)) { + return; + } + int compliance = pom.indexOf(COMPLIANCE_GOAL); + if (compliance < 0 || pom.indexOf(COMPLIANCE_GOAL, compliance + 1) >= 0) { + manual.add("the remap-compat goal, in the process-classes execution, before bytecode-compliance:\n" + + " " + REMAP_GOAL + "\n"); + } else { + int lineStart = pom.lastIndexOf('\n', compliance) + 1; + String indent = pom.substring(lineStart, compliance); + pom = pom.substring(0, compliance) + REMAP_GOAL + "\n" + indent + pom.substring(compliance); + changed = true; + } + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/PrepareDesktopSourcesMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/PrepareDesktopSourcesMojo.java new file mode 100644 index 00000000000..04c7579a819 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/PrepareDesktopSourcesMojo.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Resource; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/// Brings the desktop sources of an application module into its build: +/// `src/main/desktop`, holding a Swing or JavaFX application as its own +/// project laid it out (see [DesktopSources]). +/// +/// Adds `src/main/desktop/java` as a compile source root and +/// `src/main/desktop/resources` as a resource root, so the sources compile +/// unchanged -- Swing code against the JDK's own `java.awt` and `javax.swing`, +/// JavaFX code against `codenameone-javafx-compat` -- and the resources reach +/// the classes directory under the names the code loads them by. A silent +/// no-op for a project without `src/main/desktop`. See `remap-compat` for the +/// step that follows compilation. +@Mojo(name = "prepare-desktop-sources", defaultPhase = LifecyclePhase.GENERATE_SOURCES, + requiresDependencyResolution = ResolutionScope.COMPILE) +public class PrepareDesktopSourcesMojo extends AbstractCN1Mojo { + + @Parameter(property = "cn1.desktop.sourceDir", defaultValue = "${project.basedir}/src/main/desktop") + private File desktopSourceDir; + + /// Where a build-time compile step writes Java sources; a compile source + /// root once it exists. + @Parameter(property = "cn1.desktop.outputDir", defaultValue = "${project.build.directory}/generated-sources/desktop") + private File desktopOutputDir; + + /// Where a build-time compile step writes classpath resources; a resource + /// root once it exists. + @Parameter(property = "cn1.desktop.resourcesOutputDir", + defaultValue = "${project.build.directory}/generated-resources/desktop") + private File desktopResourcesOutputDir; + + @Override + protected void executeImpl() throws MojoExecutionException, MojoFailureException { + if (!DesktopSources.isDesktopProject(desktopSourceDir)) { + return; + } + List artifactIds = new ArrayList(); + List classpath = new ArrayList(); + for (Artifact a : project.getArtifacts()) { + artifactIds.add(a.getArtifactId()); + if (a.getFile() != null) { + classpath.add(a.getFile()); + } + } + try { + DesktopSources.requireRuntime(desktopSourceDir, artifactIds); + } catch (com.codename1.builders.BuildException ex) { + throw new MojoFailureException(ex.getMessage(), ex); + } + // The last build relocated the classes in the output directory, and + // javac compiles against that directory: a Kotlin class the + // incremental compiler does not recompile would be seen relocated, so + // the Java code calling it would not compile. Its unrelocated original + // is in Kotlin's own output tree; put it back before anything compiles. + copyKotlinIncrementalCompileOutputToOutputDir(true); + File javaDir = DesktopSources.javaDir(desktopSourceDir); + if (javaDir.isDirectory()) { + registerSourceRoot(javaDir); + } + File resourcesDir = DesktopSources.resourcesDir(desktopSourceDir); + if (resourcesDir.isDirectory()) { + registerResourceRoot(resourcesDir); + } + compileDesktopResources(desktopSourceDir, resourcesDir, classpath, desktopOutputDir, desktopResourcesOutputDir); + if (desktopOutputDir.isDirectory()) { + registerSourceRoot(desktopOutputDir); + } + if (desktopResourcesOutputDir.isDirectory()) { + registerResourceRoot(desktopResourcesOutputDir); + } + } + + /// EXTENSION POINT, deliberately empty: the build-time FXML and CSS + /// compile step goes here. + /// + /// A device has no XML parser to spare and no reflection, so what a + /// desktop JavaFX runtime does when it loads an `.fxml` document or a + /// stylesheet -- parse it, look the controller class and its `fx:id` + /// fields and `#handler` methods up by name -- has to happen while + /// building, the way `compile-android-res` turns Android's XML into + /// generated classes and a binary table. The compiler itself lives in its + /// own module; this method is the one place the Maven build calls it. + /// + /// Called once per build, in `generate-sources`, only for a project with + /// desktop sources, after its source and resource roots are registered + /// and before anything compiles. + /// + /// - `desktopDir`: `src/main/desktop`. + /// - `resourcesDir`: `src/main/desktop/resources`, the documents to + /// compile (`**/*.fxml`, `**/*.css`); it may not exist. + /// - `classpath`: the module's resolved compile classpath, which holds + /// `codenameone-javafx-compat` when the JavaFX layer is available. + /// - `javaOut`: where to write generated Java sources. Whatever is + /// written there is compiled with the application: the directory is + /// registered as a source root right after this returns, if it exists. + /// - `resourcesOut`: where to write generated classpath resources (a + /// binary form of the documents); registered as a resource root the + /// same way. + /// + /// Generated sources are compiled before the application is relocated, so + /// they name the JavaFX API as an application does (`javafx.scene...`), + /// and `remap-compat` relocates them with everything else. An + /// implementation should leave both directories untouched when its inputs + /// have not changed, as `compile-android-res` does, so an unchanged build + /// recompiles nothing. + /// + /// The Gradle build needs the same step as a task with declared inputs + /// and outputs; see the matching note in the Gradle plugin's `AppSupport`. + protected void compileDesktopResources(File desktopDir, File resourcesDir, List classpath, File javaOut, + File resourcesOut) throws MojoExecutionException, MojoFailureException { + getLog().debug("No build-time FXML/CSS compile step is installed; " + resourcesDir + " is shipped as it is"); + } + + private void registerResourceRoot(File dir) { + String path = dir.getAbsolutePath(); + for (Resource r : project.getResources()) { + if (r.getDirectory() != null && new File(r.getDirectory()).getAbsolutePath().equals(path)) { + return; + } + } + Resource r = new Resource(); + r.setDirectory(path); + project.addResource(r); + getLog().debug("Added resource root " + path); + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapAndroidMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapAndroidMojo.java index 63c4a497263..fecc9d5a153 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapAndroidMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapAndroidMojo.java @@ -22,48 +22,15 @@ */ package com.codename1.maven; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; -import java.io.File; - -/// Relocates the compiled Android code onto the compatibility runtime and -/// ships the runtime with the application; see [AndroidRemapper]. Must run in -/// `process-classes` before `bytecode-compliance`, which then checks the -/// relocated runtime along with the application. A no-op for a project that -/// does not depend on `codenameone-android-compat`. +/// The name `remap-compat` had when Android was the only compatibility layer. +/// It is the same goal: the poms of existing projects bind `remap-android`, +/// and through it they relocate every layer the project switches on, without +/// being edited. See [RemapCompatMojo]. @Mojo(name = "remap-android", defaultPhase = LifecyclePhase.PROCESS_CLASSES, requiresDependencyResolution = ResolutionScope.COMPILE) -public class RemapAndroidMojo extends AbstractCN1Mojo { - - @Override - protected void executeImpl() throws MojoExecutionException, MojoFailureException { - File jar = CompileAndroidResMojo.compatJar(project.getArtifacts()); - if (jar == null) { - return; - } - File classes = new File(project.getBuild().getOutputDirectory()); - if (!classes.isDirectory()) { - return; - } - // Freshly compiled Kotlin (incremental compilation keeps its own - // output tree) must be in place before it is relocated. - copyKotlinIncrementalCompileOutputToOutputDir(); - File onClick = AndroidResourceRunner.onClickNamesFile(new File(project.getBuild().getDirectory())); - try { - java.util.List classpath = new java.util.ArrayList(); - for (org.apache.maven.artifact.Artifact artifact : project.getArtifacts()) { - if (artifact.getFile() != null) { - classpath.add(artifact.getFile()); - } - } - new AndroidRemapper(classes, jar, onClick, MavenLog.of(getLog())) - .withSupportJars(java.util.Collections.singletonList(CompatLayers.jdkJar(classpath))).run(); - } catch (com.codename1.builders.BuildException ex) { - throw new MojoFailureException(ex.getMessage(), ex); - } - } +public class RemapAndroidMojo extends RemapCompatMojo { } diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapCompatMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapCompatMojo.java new file mode 100644 index 00000000000..0103b309f3f --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/RemapCompatMojo.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/// Relocates the compiled application onto whichever compatibility layers it +/// has switched on -- Android, Swing, JavaFX, in any mix -- and ships their +/// runtimes with it; see [CompatRemapper]. Must run in `process-classes` +/// before `bytecode-compliance`, which then checks the relocated runtimes +/// along with the application. For the Swing layer that order is the only one +/// that works at all: its application code is compiled against the JDK's own +/// `javax.swing`, and nothing the check could resolve those names against +/// exists until they have been relocated. +/// +/// A no-op for a project that depends on none of the layers' runtimes, so it +/// is bound unconditionally. +@Mojo(name = "remap-compat", defaultPhase = LifecyclePhase.PROCESS_CLASSES, + requiresDependencyResolution = ResolutionScope.COMPILE) +public class RemapCompatMojo extends AbstractCN1Mojo { + + @Parameter(property = "cn1.desktop.sourceDir", defaultValue = "${project.basedir}/src/main/desktop") + private File desktopSourceDir; + + @Override + protected void executeImpl() throws MojoExecutionException, MojoFailureException { + List classpath = new ArrayList(); + for (Artifact artifact : project.getArtifacts()) { + if (artifact.getFile() != null) { + classpath.add(artifact.getFile()); + } + } + // By the jars alone: whether a desktop layer is also used can only be + // read from the classes, and Kotlin's are not all in place yet. + if (CompatLayers.active(classpath).isEmpty()) { + return; + } + File classes = new File(project.getBuild().getOutputDirectory()); + if (!classes.isDirectory()) { + return; + } + // Freshly compiled Kotlin (incremental compilation keeps its own + // output tree) must be in place before it is relocated. + copyKotlinIncrementalCompileOutputToOutputDir(); + File onClick = AndroidResourceRunner.onClickNamesFile(new File(project.getBuild().getDirectory())); + try { + new CompatRemapper(classes, classpath, onClick, MavenLog.of(getLog())) + .withDesktopEntryRecord(desktopSourceDir == null ? null + : DesktopSources.entryRecord(desktopSourceDir)) + .run(); + } catch (com.codename1.builders.BuildException ex) { + throw new MojoFailureException(ex.getMessage(), ex); + } + } +} diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/AndroidPomUpdaterTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/AndroidPomUpdaterTest.java index d064d697fb6..e758637219e 100644 --- a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/AndroidPomUpdaterTest.java +++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/AndroidPomUpdaterTest.java @@ -50,7 +50,7 @@ public void wiresALegacyArchetypePom() throws Exception { assertTrue(u.pom.contains("compile-android-res")); int remap = u.pom.indexOf(AndroidPomUpdater.REMAP_GOAL); int compliance = u.pom.indexOf("bytecode-compliance"); - assertTrue(remap > 0 && remap < compliance, "remap-android must precede bytecode-compliance"); + assertTrue(remap > 0 && remap < compliance, "remap-compat must precede bytecode-compliance"); assertTrue(u.pom.indexOf("") >= 0 && u.pom.lastIndexOf("") > u.pom.indexOf("android-compat")); // Wired once: a second import changes nothing. AndroidPomUpdater again = new AndroidPomUpdater(u.pom, false); @@ -67,6 +67,19 @@ public void leavesTheCurrentArchetypeAlone() throws Exception { assertEquals(current, u.pom); } + /// A project generated while the goal was called remap-android is wired: + /// that goal relocates every layer, and its pom is not edited. + @Test + public void acceptsTheGoalUnderItsEarlierName() throws Exception { + String current = read("../cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml"); + String earlier = current.replace("remap-compat", "remap-android"); + assertTrue(earlier.contains("remap-android")); + AndroidPomUpdater u = new AndroidPomUpdater(earlier, true); + assertFalse(u.changed); + assertTrue(u.manual.isEmpty(), u.manual.toString()); + assertEquals(earlier, u.pom); + } + @Test public void neverWiresTheGoalIntoALaterPlugin() { String pom = "\n" diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/DesktopPomUpdaterTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/DesktopPomUpdaterTest.java new file mode 100644 index 00000000000..9d7fb565bd7 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/DesktopPomUpdaterTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// cn1:import-desktop-project in a project generated before the desktop +/// layers existed: its common pom has neither runtime dependency and none of +/// the goals, and the imported sources would not build. +public class DesktopPomUpdaterTest { + + private static String read(String path) throws Exception { + return new String(Files.readAllBytes(new File(path).toPath()), "UTF-8"); + } + + @Test + public void wiresALegacyArchetypePom() throws Exception { + String legacy = read("src/test/resources/android-pom/legacy-common-pom.xml"); + DesktopPomUpdater u = new DesktopPomUpdater(legacy, false); + assertTrue(u.manual.isEmpty(), u.manual.toString()); + assertTrue(u.changed); + assertTrue(u.pom.contains("codenameone-swing-compat")); + assertTrue(u.pom.contains("codenameone-javafx-compat")); + assertTrue(u.pom.contains("${basedir}/src/main/desktop")); + assertTrue(u.pom.contains("prepare-desktop-sources")); + int remap = u.pom.indexOf(PomWiring.REMAP_GOAL); + int compliance = u.pom.indexOf("bytecode-compliance"); + assertTrue(remap > 0 && remap < compliance, "remap-compat must precede bytecode-compliance"); + assertTrue(u.pom.lastIndexOf("") > u.pom.indexOf("desktop-compat")); + // Wired once: a second import changes nothing. + DesktopPomUpdater again = new DesktopPomUpdater(u.pom, false); + assertFalse(again.changed); + assertTrue(again.manual.isEmpty()); + assertEquals(u.pom, again.pom); + } + + @Test + public void leavesTheCurrentArchetypeAlone() throws Exception { + String current = read("../cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml"); + DesktopPomUpdater u = new DesktopPomUpdater(current, true); + assertFalse(u.changed); + assertTrue(u.manual.isEmpty(), u.manual.toString()); + assertEquals(current, u.pom); + } + + /// A project that already imported an Android application binds the + /// remap goal under its earlier name. That goal relocates every layer, so + /// it is kept, and only the desktop wiring is added. + @Test + public void addsTheDesktopLayersToAProjectWiredForAndroid() throws Exception { + String legacy = read("src/test/resources/android-pom/legacy-common-pom.xml"); + String android = new AndroidPomUpdater(legacy, false).pom + .replace(PomWiring.REMAP_GOAL, PomWiring.LEGACY_REMAP_GOAL); + DesktopPomUpdater u = new DesktopPomUpdater(android, false); + assertTrue(u.manual.isEmpty(), u.manual.toString()); + assertTrue(u.changed); + assertFalse(u.pom.contains(PomWiring.REMAP_GOAL), "the goal is bound once, under the name it already has"); + assertTrue(u.pom.contains(PomWiring.LEGACY_REMAP_GOAL)); + assertTrue(u.pom.contains("android-compat")); + assertTrue(u.pom.contains("desktop-compat")); + assertTrue(u.pom.contains("compile-android-res")); + assertTrue(u.pom.contains("prepare-desktop-sources")); + } + + @Test + public void neverWiresTheGoalIntoALaterPlugin() { + String pom = "\n" + + " \n" + + " com.codenameone\n" + + " codenameone-maven-plugin\n" + + " \n" + + " \n" + + " maven-antrun-plugin\n" + + " \n" + + " \n" + + " \n" + + "\n"; + DesktopPomUpdater u = new DesktopPomUpdater(pom, false); + assertFalse(u.pom.contains("prepare-desktop-sources"), u.pom); + boolean named = false; + for (String m : u.manual) { + named |= m.contains("prepare-desktop-sources"); + } + assertTrue(named, u.manual.toString()); + } + + @Test + public void namesWhatAPomWithoutTheAnchorsNeeds() { + DesktopPomUpdater u = new DesktopPomUpdater("", true); + assertFalse(u.changed); + assertEquals(4, u.manual.size(), u.manual.toString()); + assertTrue(u.manual.get(0).contains("codenameone-swing-compat"), u.manual.get(0)); + assertTrue(u.manual.get(0).contains("codenameone-javafx-compat"), u.manual.get(0)); + assertTrue(u.manual.get(3).contains("src/main/desktop/kotlin"), u.manual.get(3)); + } + + /// A Kotlin project wired before the desktop layers has the Android + /// source directories in its Kotlin execution and not the desktop ones. + @Test + public void asksForTheKotlinSourceDirectoriesOnlyWhenThereIsKotlin() throws Exception { + String current = read("../cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml"); + String earlier = current.replace("${project.basedir}/src/main/desktop/java", ""); + assertTrue(new DesktopPomUpdater(earlier, false).manual.isEmpty()); + DesktopPomUpdater u = new DesktopPomUpdater(earlier, true); + assertEquals(1, u.manual.size(), u.manual.toString()); + assertFalse(u.changed); + } +} diff --git a/maven/integration-tests/android-compat-test.sh b/maven/integration-tests/android-compat-test.sh index f15f33a853f..a1e2767cdfc 100755 --- a/maven/integration-tests/android-compat-test.sh +++ b/maven/integration-tests/android-compat-test.sh @@ -19,47 +19,34 @@ SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" set -e source "$SCRIPTPATH/inc/env.sh" source "$SCRIPTPATH/inc/gradle.sh" +source "$SCRIPTPATH/inc/compat.sh" SAMPLES="$SCRIPTPATH/../../scripts/android-compat-samples" WORKDIR="$SCRIPTPATH/build/android-compat" rm -rf "$WORKDIR" mkdir -p "$WORKDIR" -# Class files that still name an unrelocated Android package. A relocated name -# is preceded by "/" (com/codename1/androidcompat/android/...), an unrelocated -# one by the constant pool's length bytes or by the "L" of a descriptor. -unrelocated() { - local dir="$1" - find "$dir" -name '*.class' -print0 | while IFS= read -r -d '' f; do - if LC_ALL=C grep -aEq '(^|[^/a-zA-Z0-9_$])L?(android|androidx|com/google/android/material)/[A-Za-z]' "$f"; then - echo "${f#$dir/}" - fi - done -} +FAILED=0 -staged_jar() { - sed -n 's/.*codename1.stageOnly is set: staged \(.*\) ([0-9]* bytes) for .*/\1/p' "$1" | tail -1 +# What every target's upload has to carry, whichever tool staged it. +check_android_upload() { + local jar="$1" + assert_zip_has "$jar" '(^|/)com/codename1/generated/android/AndroidAppImpl\.class$' + assert_zip_has "$jar" '(^|/)com/codename1/androidcompat/android/app/Activity\.class$' + assert_zip_has "$jar" '(^|/)com/example/droid/R\.class$' + assert_zip_has "$jar" '(^|/)cn1_android_res\.bin$' + # At the root: the relocated runtime legitimately lives under + # com/codename1/androidcompat/android/. + assert_zip_lacks "$jar" '^(android|androidx)/.*\.class$' } -# Bookkeeping, never read from the upload: directory entries and Maven's pom -# metadata and manifest (as gradle-maven-parity-test.sh); Kotlin's module file, -# which each tool names after its own project (app-common, DroidApp) and only -# Kotlin reflection reads; and the ORM enhancer's per-directory list, which -# Gradle keeps in each of its classes directories and the enhancer only ever -# reads from the directory. -IGNORED='/$|^META-INF/maven/|^META-INF/MANIFEST\.MF$|^META-INF/[^/]*\.kotlin_module$|^META-INF/cn1/orm-enhanced-dependencies\.list$' -FAILED=0 - for sample_dir in "$SAMPLES"/*/; do sample=$(basename "$sample_dir") echo "== $sample" W="$WORKDIR/$sample" mkdir -p "$W" PKG=com.acme.droid - (cd "$W" && mvn_local archetype:generate -DarchetypeArtifactId=cn1app-archetype -DarchetypeGroupId=com.codenameone \ - -DarchetypeVersion="$CN1_VERSION" -DartifactId=app -DgroupId=$PKG -Dpackage=$PKG \ - -Dversion=1.0-SNAPSHOT -DmainName=DroidApp -DjavaVersion=17 -DinteractiveMode=false) > "$W/archetype.log" 2>&1 \ - || { tail -40 "$W/archetype.log"; fail "$sample: archetype:generate"; } + compat_generate_app "$sample" "$W" $PKG DroidApp MAPP="$W/app" (cd "$MAPP" && mvn_local "com.codenameone:codenameone-maven-plugin:$CN1_VERSION:import-android-project" \ "-Dcn1.android.import=$sample_dir") > "$W/import.log" 2>&1 \ @@ -70,8 +57,7 @@ for sample_dir in "$SAMPLES"/*/; do grep -q 'extends com.codename1.androidcompat.runtime.AndroidLifecycle' "$MAPP/common/src/main/java/com/acme/droid/DroidApp.java" \ || fail "$sample: the entry point does not start the Android application" - (cd "$MAPP" && JAVA_HOME="$GRADLE_JDK" mvn_local install -DskipTests -pl common -am) > "$W/mvn-common.log" 2>&1 \ - || { tail -60 "$W/mvn-common.log"; fail "$sample: Maven build"; } + compat_build_common "$sample" "$W" "$MAPP" classes="$MAPP/common/target/classes" for expected in com/codename1/generated/android/AndroidAppImpl.class cn1_android_res.bin cn1_android_framework.bin \ com/codename1/androidcompat/rt/OnClickDispatch.class; do @@ -79,50 +65,18 @@ for sample_dir in "$SAMPLES"/*/; do done ls "$classes"/com/codename1/androidcompat/android/app/Activity.class > /dev/null 2>&1 \ || fail "$sample: the relocated runtime is not in the application's classes" - bad=$(unrelocated "$classes") + bad=$(compat_unrelocated "$classes" 'android|androidx|com/google/android/material') if [ -n "$bad" ]; then echo "FAIL: $sample: classes still name android.* after the remap:" echo "$bad" | head -20 FAILED=1 fi - (cd "$MAPP" && mvn_local "com.codenameone:codenameone-maven-plugin:$CN1_VERSION:convert-to-gradle") > "$W/convert.log" 2>&1 \ - || { cat "$W/convert.log"; fail "$sample: convert-to-gradle"; } + compat_convert_to_gradle "$sample" "$W" "$MAPP" GAPP="$W/app-gradle" - use_local_plugin "$GAPP" [ -f "$GAPP/src/main/android/AndroidManifest.xml" ] || fail "$sample: the Gradle conversion dropped src/main/android" - for spec in "android android-device buildAndroid" "ios ios-device buildIos" "javascript javascript buildJavascript" \ - "javase mac-os-x-desktop buildMacDesktop"; do - read -r platform target task <<< "$spec" - mlog="$W/maven-$target.log" - glog="$W/gradle-$target.log" - (cd "$MAPP" && JAVA_HOME="$GRADLE_JDK" mvn_local package -DskipTests -Dopen=false -Dcodename1.platform=$platform \ - -Dcodename1.buildTarget=$target -Dcodename1.stageOnly=true < /dev/null) > "$mlog" 2>&1 \ - || { tail -40 "$mlog"; fail "$sample: Maven staging $target"; } - run_gradle "$GAPP" "$task" -Pcodename1.stageOnly=true > "$glog" 2>&1 || { tail -60 "$glog"; fail "$sample: Gradle staging $target"; } - mjar=$(staged_jar "$mlog") - gjar=$(staged_jar "$glog") - [ -f "$mjar" ] && [ -f "$gjar" ] || fail "$sample: $target: a staged jar is missing (maven=$mjar gradle=$gjar)" - for jar in "$mjar" "$gjar"; do - assert_zip_has "$jar" '(^|/)com/codename1/generated/android/AndroidAppImpl\.class$' - assert_zip_has "$jar" '(^|/)com/codename1/androidcompat/android/app/Activity\.class$' - assert_zip_has "$jar" '(^|/)com/example/droid/R\.class$' - assert_zip_has "$jar" '(^|/)cn1_android_res\.bin$' - # At the root: the relocated runtime legitimately lives under - # com/codename1/androidcompat/android/. - assert_zip_lacks "$jar" '^(android|androidx)/.*\.class$' - done - unzip -Z1 "$mjar" | grep -Ev "$IGNORED" | sort > "$W/maven-$target.txt" - unzip -Z1 "$gjar" | grep -Ev "$IGNORED" | sort > "$W/gradle-$target.txt" - if diff "$W/maven-$target.txt" "$W/gradle-$target.txt" > "$W/diff-$target.txt"; then - echo " $target: $(wc -l < "$W/maven-$target.txt" | tr -d ' ') entries, Maven and Gradle identical" - else - echo "FAIL: $sample: $target uploads differ (< Maven only, > Gradle only):" - head -40 "$W/diff-$target.txt" - FAILED=1 - fi - done + compat_stage_all "$sample" "$W" "$MAPP" "$GAPP" check_android_upload bash "$SCRIPTPATH/android-gradle-incremental-test.sh" "$GAPP" done diff --git a/maven/integration-tests/desktop-compat-test.sh b/maven/integration-tests/desktop-compat-test.sh new file mode 100755 index 00000000000..3e125b2acde --- /dev/null +++ b/maven/integration-tests/desktop-compat-test.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# An unmodified Swing or JavaFX project (scripts/desktop-compat-samples/*) is +# imported into an archetype application and built for every target, by Maven +# and by Gradle. Checks, for each sample: +# +# - cn1:import-desktop-project copies the sources and records the +# application's entry point in src/main/desktop/cn1-desktop.properties; +# - the build relocated every reference to the toolkits: no class the +# application ships may name java/awt/, javax/swing/, java/beans/, +# org/jdesktop/ or javafx/, because no device has those classes and the +# first two packages cannot be defined by anything but a JDK; +# - the runtime of the toolkit the sample uses is in the application's +# classes, under its relocated name; +# - every target's upload jar (staged, never sent) carries all of it, so the +# iOS, Android, JavaScript and desktop builders all get the remapped code; +# - the Gradle build uploads the same entries as the Maven build. +# +# Needs the reactor installed (mvn install) and a JDK 17+; see inc/gradle.sh. +SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" +set -e +SAMPLES="$SCRIPTPATH/../../scripts/desktop-compat-samples" +if [ ! -d "$SAMPLES" ] || [ -z "$(find "$SAMPLES" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)" ]; then + echo "desktop-compat-test: SKIPPED, no samples under scripts/desktop-compat-samples" + exit 0 +fi +source "$SCRIPTPATH/inc/env.sh" +source "$SCRIPTPATH/inc/gradle.sh" +source "$SCRIPTPATH/inc/compat.sh" + +WORKDIR="$SCRIPTPATH/build/desktop-compat" +rm -rf "$WORKDIR" +mkdir -p "$WORKDIR" + +# The packages of both toolkits, as an application is compiled against them. +TOOLKITS='java/awt|javax/swing|java/beans|org/jdesktop|javafx' +FAILED=0 + +# What every target's upload has to carry, whichever tool staged it. MAIN_ENTRY +# and RUNTIME are set per sample, below. +check_desktop_upload() { + local jar="$1" + assert_zip_has "$jar" "(^|/)$MAIN_ENTRY\\.class\$" + assert_zip_has "$jar" "(^|/)$RUNTIME/.*\\.class\$" + # At the root: the relocated runtimes legitimately live under + # com/codename1/desktopcompat/ and com/codename1/fxcompat/. + assert_zip_lacks "$jar" "^($TOOLKITS)/.*\\.class\$" +} + +for sample_dir in "$SAMPLES"/*/; do + sample=$(basename "$sample_dir") + echo "== $sample" + W="$WORKDIR/$sample" + mkdir -p "$W" + PKG=com.acme.desktop + compat_generate_app "$sample" "$W" $PKG DesktopApp + MAPP="$W/app" + (cd "$MAPP" && mvn_local "com.codenameone:codenameone-maven-plugin:$CN1_VERSION:import-desktop-project" \ + "-Dcn1.desktop.import=$sample_dir") > "$W/import.log" 2>&1 \ + || { tail -40 "$W/import.log"; fail "$sample: import-desktop-project"; } + desktop="$MAPP/common/src/main/desktop" + record="$desktop/cn1-desktop.properties" + [ -f "$record" ] || fail "$sample: the import recorded no entry point" + main_class=$(sed -n 's/^mainClass=//p' "$record" | tr -d '\r' | tail -1) + kind=$(sed -n 's/^kind=//p' "$record" | tr -d '\r' | tail -1) + [ -n "$main_class" ] || fail "$sample: the entry record names no mainClass" + case "$kind" in + swing) RUNTIME=com/codename1/desktopcompat ;; + javafx) RUNTIME=com/codename1/fxcompat ;; + *) fail "$sample: the entry record's kind is '$kind', neither swing nor javafx" ;; + esac + MAIN_ENTRY=$(echo "$main_class" | tr . /) + [ -d "$desktop/java" ] || [ -d "$desktop/kotlin" ] || fail "$sample: the import copied no sources" + [ -z "$(find "$desktop" -name module-info.java)" ] || fail "$sample: the import kept a module descriptor" + echo " $kind application, started by $main_class" + + compat_build_common "$sample" "$W" "$MAPP" + classes="$MAPP/common/target/classes" + [ -f "$classes/$MAIN_ENTRY.class" ] || fail "$sample: the build produced no $MAIN_ENTRY.class" + [ -n "$(find "$classes/$RUNTIME" -name '*.class' 2>/dev/null | head -1)" ] \ + || fail "$sample: the relocated $kind runtime is not in the application's classes" + bad=$(compat_unrelocated "$classes" "$TOOLKITS") + if [ -n "$bad" ]; then + echo "FAIL: $sample: classes still name a desktop toolkit after the remap:" + echo "$bad" | head -20 + FAILED=1 + fi + + compat_convert_to_gradle "$sample" "$W" "$MAPP" + GAPP="$W/app-gradle" + [ -f "$GAPP/src/main/desktop/cn1-desktop.properties" ] || fail "$sample: the Gradle conversion dropped src/main/desktop" + + compat_stage_all "$sample" "$W" "$MAPP" "$GAPP" check_desktop_upload +done + +[ $FAILED -eq 0 ] || exit 1 +echo "desktop-compat-test: OK" diff --git a/maven/integration-tests/inc/compat.sh b/maven/integration-tests/inc/compat.sh new file mode 100644 index 00000000000..7b665865179 --- /dev/null +++ b/maven/integration-tests/inc/compat.sh @@ -0,0 +1,104 @@ +# What the compatibility-layer tests (android-compat-test.sh, +# desktop-compat-test.sh) share: generating the application a sample is +# imported into, finding classes a relocation missed, and staging every +# target's upload jar with Maven and with Gradle to compare them. +# +# Source inc/env.sh and inc/gradle.sh first; this file uses mvn_local, +# run_gradle, fail and GRADLE_JDK from them. A script sets FAILED=0 before its +# loop and exits by it afterwards: the functions here set it to 1 for a failure +# worth reporting alongside the others, and call fail for one that leaves +# nothing further to check. + +# compat_unrelocated +# +# Class files that still name one of , an ERE alternation of +# slash-separated package names without the trailing slash +# ("android|androidx"). A relocated name is preceded by "/" +# (com/codename1/androidcompat/android/...), an unrelocated one by the constant +# pool's length bytes or by the "L" of a descriptor. +compat_unrelocated() { + local dir="$1" packages="$2" + find "$dir" -name '*.class' -print0 | while IFS= read -r -d '' f; do + if LC_ALL=C grep -aEq "(^|[^/a-zA-Z0-9_\$])L?($packages)/[A-Za-z]" "$f"; then + echo "${f#$dir/}" + fi + done +} + +# The jar a build run with codename1.stageOnly=true reported in . +compat_staged_jar() { + sed -n 's/.*codename1.stageOnly is set: staged \(.*\) ([0-9]* bytes) for .*/\1/p' "$1" | tail -1 +} + +# Bookkeeping, never read from the upload: directory entries and Maven's pom +# metadata and manifest (as gradle-maven-parity-test.sh); Kotlin's module file, +# which each tool names after its own project (app-common, DroidApp) and only +# Kotlin reflection reads; and the ORM enhancer's per-directory list, which +# Gradle keeps in each of its classes directories and the enhancer only ever +# reads from the directory. +COMPAT_IGNORED='/$|^META-INF/maven/|^META-INF/MANIFEST\.MF$|^META-INF/[^/]*\.kotlin_module$|^META-INF/cn1/orm-enhanced-dependencies\.list$' + +# compat_generate_app