diff --git a/.all-contributorsrc b/.all-contributorsrc index e9e4764d85..95122bc1fd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1473,7 +1473,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/138858208?v=4", "profile": "https://github.com/Rishab87", "contributions": [ - "code" + "code", + "test" ] }, { @@ -1688,6 +1689,7 @@ "doc" ] }, + { "login": "avinxshKD", "name": "Avinash Kumar Deepak", "avatar_url": "https://avatars.githubusercontent.com/u/152387616?v=4", @@ -1695,6 +1697,42 @@ "contributions": [ "code" ] + }, + { + "login": "Sumamasonia", + "name": "Sumama Sonia", + "avatar_url": "https://avatars.githubusercontent.com/u/214366437?v=4", + "profile": "https://www.linkedin.com/in/sumamasonia/", + "contributions": [ + "code" + ] + }, + { + "login": "SalmaneKhalili", + "name": "Salmane Khalili", + "avatar_url": "https://avatars.githubusercontent.com/u/141567440?v=4", + "profile": "https://www.linkedin.com/in/salmane-khalili-4223562aa/", + "contributions": [ + "bug" + ] + }, + { + "login": "nickmcintyre", + "name": "Nick McIntyre", + "avatar_url": "https://avatars.githubusercontent.com/u/3719176?v=4", + "profile": "https://mcintyre.io", + "contributions": [ + "test" + ] + }, + { + "login": "Ebaron96", + "name": "Elijah Baron", + "avatar_url": "https://avatars.githubusercontent.com/u/180047692?v=4", + "profile": "https://github.com/Ebaron96", + "contributions": [ + "bug" + ] } ], "repoType": "github", diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000000..cc65b83095 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,18 @@ +name: "Setup Processing" +description: "Setup the required contents for Processing to build" +inputs: + arch: + description: 'Architecture of the JDK to download' + required: false +runs: + using: "composite" + steps: + - name: Install Java + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + architecture: ${{ inputs.arch }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 \ No newline at end of file diff --git a/.github/workflows/build-gradle.yml b/.github/workflows/build-gradle.yml deleted file mode 100644 index 254433edd0..0000000000 --- a/.github/workflows/build-gradle.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Branch Builds -on: - push: - paths-ignore: - - '**/*.md' - - '.all-contributorsrc' - -jobs: - test: - runs-on: ubuntu-latest - name: Test Processing - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Install Java - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Build with Gradle - run: ./gradlew test - build: - name: (${{ matrix.os_prefix }}/${{ matrix.arch }}) Create Processing Build - runs-on: ${{ matrix.os }} - needs: test - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-24.04-arm - os_prefix: linux - arch: aarch64 - binary: processing*.snap - - os: ubuntu-latest - os_prefix: linux - arch: x64 - binary: processing*.snap - - os: windows-latest - os_prefix: windows - arch: x64 - binary: msi/Processing-*.msi - - os: macos-latest - os_prefix: macos - arch: x64 - binary: dmg/Processing-*.dmg - - os: macos-latest - os_prefix: macos - arch: aarch64 - binary: dmg/Processing-*.dmg - steps: - - name: Install Snapcraft - if: runner.os == 'Linux' - uses: samuelmeuli/action-snapcraft@v3 - - name: Install LXD - if: runner.os == 'Linux' - uses: canonical/setup-lxd@main - - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Install Java - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - architecture: ${{ matrix.arch }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Build with Gradle - run: ./gradlew packageDistributionForCurrentOS - - - name: Add artifact - uses: actions/upload-artifact@v4 - with: - name: processing-${{ matrix.os_prefix }}-${{ matrix.arch }}-br_${{ github.ref_name }} - retention-days: 1 - path: app/build/compose/binaries/main/${{ matrix.binary }} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1b7f9ad62..d4355a69c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,77 +1,105 @@ -name: Branch Builds (Legacy) -on: +name: Branch Builds +on: push: paths-ignore: - '**/*.md' - '.all-contributorsrc' + pull_request: + paths-ignore: + - '**/*.md' + branches: + - main + jobs: test: runs-on: ubuntu-latest - name: Run Tests + name: Test Processing steps: - name: Checkout Repository uses: actions/checkout@v4 - - name: Install Java - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - architecture: x64 - - name: Build - run: > - cd build; - ant -noinput build test; - build: - name: Create Pre-release for ${{ matrix.os_prefix }} (${{ matrix.arch }}) - needs: test + + - name: Setup Processing + uses: ./.github/actions/setup + + - name: Build with Gradle + run: ./gradlew test + + # - name: Add comment with binaries + # if: ${{ github.event_name == 'pull_request' }} + # uses: marocchino/sticky-pull-request-comment@v2 + # with: + # header: artifacts + # message: | + # Tests completed successfully. Build artifacts for this pull request will appear below once ready. + # + # ### Build Artifacts + # | Platform | Link | + # |:--|------------------------| + + build: + name: (${{ matrix.os_prefix }}/${{ matrix.arch }}) Create Processing Build runs-on: ${{ matrix.os }} + needs: test permissions: - contents: write + pull-requests: write strategy: fail-fast: false matrix: include: - # compiling for arm32 needs a self-hosted runner on Raspi OS (32-bit) - - os: [self-hosted, linux, ARM] + - os: ubuntu-24.04-arm os_prefix: linux - arch: arm + arch: aarch64 + binary: deb/processing*.deb - os: ubuntu-latest os_prefix: linux arch: x64 + binary: deb/processing*.deb - os: windows-latest os_prefix: windows arch: x64 + binary: msi/Processing-*.msi - os: macos-latest os_prefix: macos arch: x64 + binary: dmg/Processing-*.dmg - os: macos-latest os_prefix: macos arch: aarch64 - - os: macos-latest - os_prefix: linux - arch: aarch64 + binary: dmg/Processing-*.dmg steps: - name: Checkout Repository uses: actions/checkout@v4 - - name: Checkout Examples Repository - uses: actions/checkout@v4 - with: - repository: processing/processing-examples - path: processing-examples - - name: Install Java - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - architecture: ${{ matrix.arch }} - - name: Setup Ant - uses: cedx/setup-ant@v3 - - name: Build Release - run: ant -noinput -buildfile build/build.xml ${{ matrix.os_prefix }}-dist -Dversion="${{ github.sha }}" + + - name: Setup Processing + uses: ./.github/actions/setup + + - name: Package Processing with Gradle + run: ./gradlew packageDistributionForCurrentOS + - name: Add artifact uses: actions/upload-artifact@v4 + if: ${{ github.event_name != 'pull_request' }} with: - name: processing-${{ github.ref_name }}-${{github.sha}}-${{ matrix.os_prefix }}-${{ matrix.arch }}-ant - path: ./build/${{ matrix.os_prefix }}/processing-${{github.sha}}-${{ matrix.os_prefix}}-* + name: processing-${{ matrix.os_prefix }}-${{ matrix.arch }}-br_${{ github.ref_name }} retention-days: 1 + path: app/build/compose/binaries/main/${{ matrix.binary }} + + - name: Add artifact for PR + if: ${{ github.event_name == 'pull_request' }} + id: upload-artifact + uses: actions/upload-artifact@v4 + with: + name: processing-${{ matrix.os_prefix }}-${{ matrix.arch }}-pr_${{ github.event.pull_request.number }} + retention-days: 5 + path: app/build/compose/binaries/main/${{ matrix.binary }} + + # - name: Add comment with binaries + # if: ${{ github.event_name == 'pull_request' }} + # uses: marocchino/sticky-pull-request-comment@v2 + # with: + # append: true + # header: artifacts + # message: | + # |(${{ matrix.os_prefix }}/${{ matrix.arch }})|[Download processing-${{ matrix.os_prefix }}-${{ matrix.arch }}-pr_${{ github.event.pull_request.number }}](${{ steps.upload-artifact.outputs.artifact-url }})| + diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml deleted file mode 100644 index 1997a5a4e9..0000000000 --- a/.github/workflows/lock.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: 'Lock Threads' - -on: - schedule: - - cron: '0 6 * * *' - -permissions: - contents: read - -jobs: - lock: - permissions: - issues: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@v4 - with: - github-token: ${{ github.token }} - issue-lock-inactive-days: '30' - issue-lock-comment: > - This issue has been automatically locked. To avoid confusion - with reports that have already been resolved, closed issues - are automatically locked 30 days after the last comment. - Please open a new issue for related bugs. - pr-lock-comment: > - This pull request has been automatically locked. - Pull requests that have been closed are automatically - locked 30 days after the last comment. diff --git a/.github/workflows/pull_request-gradle.yml b/.github/workflows/pull_request-gradle.yml deleted file mode 100644 index 4ea0bcc9db..0000000000 --- a/.github/workflows/pull_request-gradle.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Pull Requests with Gradle -on: - pull_request: - paths-ignore: - - '**/*.md' - branches: - - main - -jobs: - test: - runs-on: ubuntu-latest - name: Test Processing - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Install Java - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Build with Gradle - run: ./gradlew test - build: - name: (${{ matrix.os_prefix }}/${{ matrix.arch }}) Create Processing Build - runs-on: ${{ matrix.os }} - needs: test - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-24.04-arm - os_prefix: linux - arch: aarch64 - binary: processing*.snap - - os: ubuntu-latest - os_prefix: linux - arch: x64 - binary: processing*.snap - - os: windows-latest - os_prefix: windows - arch: x64 - binary: msi/Processing-*.msi - - os: macos-latest - os_prefix: macos - arch: x64 - binary: dmg/Processing-*.dmg - - os: macos-latest - os_prefix: macos - arch: aarch64 - binary: dmg/Processing-*.dmg - steps: - - name: Install Snapcraft - if: runner.os == 'Linux' - uses: samuelmeuli/action-snapcraft@v3 - - name: Install LXD - if: runner.os == 'Linux' - uses: canonical/setup-lxd@main - - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Install Java - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - architecture: ${{ matrix.arch }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Build with Gradle - run: ./gradlew packageDistributionForCurrentOS - - - name: Add artifact - uses: actions/upload-artifact@v4 - with: - name: processing-${{ matrix.os_prefix }}-${{ matrix.arch }}-pr_${{ github.event.pull_request.number }} - retention-days: 5 - path: app/build/compose/binaries/main/${{ matrix.binary }} \ No newline at end of file diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml deleted file mode 100644 index 5831a166a4..0000000000 --- a/.github/workflows/pull_request.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Pull Requests (Legacy) -on: - pull_request: - paths-ignore: - - '**/*.md' - branches: - - main - -jobs: - build: - name: Create Pull Request Build for ${{ matrix.os_prefix }} (${{ matrix.arch }}) - runs-on: ${{ matrix.os }} - permissions: - pull-requests: write - contents: read - strategy: - fail-fast: false - matrix: - include: - # compiling for arm32 needs a self-hosted runner on Raspi OS (32-bit) - - os: [self-hosted, linux, ARM] - os_prefix: linux - arch: arm - - os: ubuntu-latest - os_prefix: linux - arch: x64 - - os: windows-latest - os_prefix: windows - arch: x64 - - os: macos-latest - os_prefix: macos - arch: x64 - - os: macos-latest - os_prefix: macos - arch: aarch64 - - os: macos-latest - os_prefix: linux - arch: aarch64 - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Checkout Examples Repository - uses: actions/checkout@v4 - with: - repository: processing/processing-examples - path: processing-examples - - name: Install Java - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - architecture: ${{ matrix.arch }} - - name: Setup Ant - uses: cedx/setup-ant@v3 - - name: Build Release - run: ant -noinput -buildfile build/build.xml ${{ matrix.os_prefix }}-dist -Dversion="${{ github.sha }}" -Dplatform=${{ matrix.os_prefix }} - - name: Add artifact - uses: actions/upload-artifact@v4 - id: upload - with: - name: processing-pr${{ github.event.pull_request.number }}-${{github.sha}}-${{ matrix.os_prefix }}-${{ matrix.arch }}-ant - path: ./build/${{ matrix.os_prefix }}/processing-${{github.sha}}-${{ matrix.os_prefix}}-* - retention-days: 5 diff --git a/.github/workflows/release-gradle.yml b/.github/workflows/release-gradle.yml deleted file mode 100644 index 8ec45cad0b..0000000000 --- a/.github/workflows/release-gradle.yml +++ /dev/null @@ -1,194 +0,0 @@ -name: Releases -on: - release: - types: [published] - -jobs: - version: - runs-on: ubuntu-latest - outputs: - revision: ${{ steps.tag_info.outputs.revision }} - version: ${{ steps.tag_info.outputs.version }} - steps: - - name: Extract version and revision - id: tag_info - shell: bash - run: | - TAG_NAME="${GITHUB_REF#refs/tags/}" - REVISION=$(echo "$TAG_NAME" | cut -d'-' -f2) - VERSION=$(echo "$TAG_NAME" | cut -d'-' -f3) - - # Set outputs for use in later jobs or steps - echo "revision=$REVISION" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - reference: - name: Publish Processing Reference to release - runs-on: ubuntu-latest - permissions: - contents: write - needs: version - steps: - - name: Checkout Website Repository - uses: actions/checkout@v4 - with: - repository: processing/processing-website - - name: Use Node.js 16 - uses: actions/setup-node@v3 - with: - node-version: 16 - - name: Install dependencies - run: npm ci - - name: Build - run: npm run build - - name: Make reference.zip - run: npm run zip - - name: Upload reference to release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - asset_name: processing-${{ needs.version.outputs.version }}-reference.zip - file: reference.zip - - publish: - name: Publish Processing Libraries to Maven Central - runs-on: ubuntu-latest - needs: version - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: 17 - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Build with Gradle - run: ./gradlew publish - env: - MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - - ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - - ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} - ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} - - ORG_GRADLE_PROJECT_version: ${{ needs.version.outputs.version }} - ORG_GRADLE_PROJECT_group: ${{ vars.GRADLE_GROUP }} - build: - name: (${{ matrix.os_prefix }}/${{ matrix.arch }}) Create Processing Release - runs-on: ${{ matrix.os }} - needs: version - permissions: - contents: write - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-24.04-arm - os_prefix: linux - arch: aarch64 - binary: ${{ vars.SNAP_NAME }}_${{ needs.version.outputs.version }}_arm64 - extension: snap - - os: ubuntu-latest - os_prefix: linux - arch: x64 - binary: ${{ vars.SNAP_NAME }}_${{ needs.version.outputs.version }}_amd64 - extension: snap - - os: windows-latest - os_prefix: windows - arch: x64 - binary: msi/Processing-${{ needs.version.outputs.version }} - extension: msi - - os: macos-latest - os_prefix: macos - arch: x64 - binary: dmg/Processing-${{ needs.version.outputs.version }} - extension: dmg - - os: macos-latest - os_prefix: macos - arch: aarch64 - binary: dmg/Processing-${{ needs.version.outputs.version }} - extension: dmg - steps: - - name: Install Certificates for Code Signing - if: runner.os == 'macOS' - continue-on-error: true - uses: apple-actions/import-codesign-certs@v3 - with: - p12-file-base64: ${{ secrets.CERTIFICATES_P12 }} - p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }} - - - name: Install Snapcraft - if: runner.os == 'Linux' - uses: samuelmeuli/action-snapcraft@v3 - - name: Install LXD - if: runner.os == 'Linux' - uses: canonical/setup-lxd@main - - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Install Java - uses: actions/setup-java@v4 - with: - java-version: '17.0.8' - distribution: 'temurin' - architecture: ${{ matrix.arch }} - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Build with Gradle - run: ./gradlew packageDistributionForCurrentOS - env: - ORG_GRADLE_PROJECT_version: ${{ needs.version.outputs.version }} - ORG_GRADLE_PROJECT_group: ${{ vars.GRADLE_GROUP }} - ORG_GRADLE_PROJECT_revision: ${{ needs.version.outputs.revision }} - ORG_GRADLE_PROJECT_compose.desktop.verbose: true - ORG_GRADLE_PROJECT_compose.desktop.mac.sign: ${{ secrets.PROCESSING_SIGNING }} - ORG_GRADLE_PROJECT_compose.desktop.mac.signing.identity: ${{ secrets.PROCESSING_SIGNING_IDENTITY }} - ORG_GRADLE_PROJECT_compose.desktop.mac.notarization.appleID: ${{ secrets.PROCESSING_APPLE_ID }} - ORG_GRADLE_PROJECT_compose.desktop.mac.notarization.password: ${{ secrets.PROCESSING_APP_PASSWORD }} - ORG_GRADLE_PROJECT_compose.desktop.mac.notarization.teamID: ${{ secrets.PROCESSING_TEAM_ID }} - ORG_GRADLE_PROJECT_snapname: ${{ vars.SNAP_NAME }} - ORG_GRADLE_PROJECT_snapconfinement: ${{ vars.SNAP_CONFINEMENT }} - - - name: Sign files with Trusted Signing - if: runner.os == 'Windows' - uses: azure/trusted-signing-action@v0 - with: - azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} - azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} - azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} - endpoint: https://eus.codesigning.azure.net/ - trusted-signing-account-name: ${{ secrets.AZURE_SIGNING_ACCOUNT_NAME }} - certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }} - files-folder: app/build/compose/binaries/main/msi - files-folder-filter: msi - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - - - name: Upload portables to release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - asset_name: processing-${{ needs.version.outputs.version }}-${{ matrix.os_prefix }}-${{ matrix.arch }}-portable.zip - file: app/build/compose/binaries/main/Processing-${{ needs.version.outputs.version }}.zip - - - name: Upload installers to release - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - asset_name: processing-${{ needs.version.outputs.version }}-${{ matrix.os_prefix }}-${{ matrix.arch }}.${{ matrix.extension }} - file: app/build/compose/binaries/main/${{ matrix.binary }}.${{ matrix.extension }} - - - name: Upload snap to Snap Store - if: runner.os == 'Linux' - run: snapcraft upload --release=beta app/build/compose/binaries/main/${{ matrix.binary }}.${{ matrix.extension }} - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.PROCESSING_SNAPCRAFT_TOKEN }} - - \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf4d33cd89..e21fce7a40 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Releases (Legacy) +name: Releases on: release: types: [published] @@ -7,34 +7,59 @@ jobs: version: runs-on: ubuntu-latest outputs: - build_number: ${{ steps.tag_info.outputs.build_number }} + revision: ${{ steps.tag_info.outputs.revision }} version: ${{ steps.tag_info.outputs.version }} steps: - - name: Extract version and build number + - name: Extract version and revision id: tag_info shell: bash run: | TAG_NAME="${GITHUB_REF#refs/tags/}" - BUILD_NUMBER=$(echo "$TAG_NAME" | cut -d'-' -f2) + REVISION=$(echo "$TAG_NAME" | cut -d'-' -f2) VERSION=$(echo "$TAG_NAME" | cut -d'-' -f3) # Set outputs for use in later jobs or steps - echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT + echo "revision=$REVISION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT - publish: - name: Publish Processing Core to Maven Central + reference: + name: Publish Processing Reference to release runs-on: ubuntu-latest + permissions: + contents: write needs: version steps: - - name: Checkout Repository + - name: Checkout Website Repository uses: actions/checkout@v4 - - name: Setup Java - uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: 17 - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + repository: processing/processing-website + - name: Use Node.js 16 + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: Install dependencies + run: npm ci + - name: Build + run: npm run build + - name: Make reference.zip + run: npm run zip + - name: Upload reference to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: processing-${{ needs.version.outputs.version }}-reference.zip + file: reference.zip + + publish-maven: + name: Publish Processing Libraries to Maven Central + runs-on: ubuntu-latest + needs: version + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Processing + uses: ./.github/actions/setup + - name: Build with Gradle run: ./gradlew publish env: @@ -48,8 +73,46 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} ORG_GRADLE_PROJECT_version: ${{ needs.version.outputs.version }} - build: - name: Publish Release for ${{ matrix.os_prefix }} (${{ matrix.arch }}) + ORG_GRADLE_PROJECT_group: ${{ vars.GRADLE_GROUP }} + + publish-gradle: + name: Publish Processing Plugins to Gradle Plugin Portal + runs-on: ubuntu-latest + needs: version + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Processing + uses: ./.github/actions/setup + + - name: Publish plugins to Gradle Plugin Portal + run: ./gradlew publishPlugins + env: + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} + + ORG_GRADLE_PROJECT_version: ${{ needs.version.outputs.version }} + ORG_GRADLE_PROJECT_group: ${{ vars.GRADLE_GROUP }} + + - name: Publish internal plugins to Gradle Plugin Portal + run: ./gradlew -c gradle/plugins/settings.gradle.kts publishPlugins + env: + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} + + ORG_GRADLE_PROJECT_version: ${{ needs.version.outputs.version }} + ORG_GRADLE_PROJECT_group: ${{ vars.GRADLE_GROUP }} + ORG_GRADLE_PROJECT_publishingGroup: ${{ vars.GRADLE_GROUP }} + + release-windows: + name: (windows/${{ matrix.arch }}) Create Processing Release runs-on: ${{ matrix.os }} needs: version permissions: @@ -58,61 +121,263 @@ jobs: fail-fast: false matrix: include: - # compiling for arm32 needs a self-hosted runner on Raspi OS (32-bit) - - os: [self-hosted, linux, ARM] - os_prefix: linux - arch: arm - - os: ubuntu-latest - os_prefix: linux - arch: x64 - - os: windows-latest - os_prefix: windows - arch: x64 - - os: macos-latest - os_prefix: macos - arch: x64 - - os: macos-latest - os_prefix: macos - arch: aarch64 - - os: macos-latest - os_prefix: linux - arch: aarch64 + - arch: x64 + os: windows-latest +# - arch: aarch64 +# os: windows-11-arm steps: - name: Checkout Repository uses: actions/checkout@v4 - - name: Checkout Examples Repository - uses: actions/checkout@v4 + + - name: Setup Processing + uses: ./.github/actions/setup + + - name: Build with Gradle + run: ./gradlew packageDistributionForCurrentOS + env: + ORG_GRADLE_PROJECT_version: ${{ needs.version.outputs.version }} + ORG_GRADLE_PROJECT_group: ${{ vars.GRADLE_GROUP }} + ORG_GRADLE_PROJECT_revision: ${{ needs.version.outputs.revision }} + + - name: Sign files with Trusted Signing + uses: azure/trusted-signing-action@v0 with: - repository: processing/processing-examples - path: processing-examples - - name: Install Java - uses: actions/setup-java@v4 + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: https://eus.codesigning.azure.net/ + trusted-signing-account-name: ${{ secrets.AZURE_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }} + files-folder: app/build/compose/binaries/main/msi + files-folder-filter: msi + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + + - name: Upload portable version + uses: svenstaro/upload-release-action@v2 with: - java-version: '17' - distribution: 'temurin' - architecture: ${{ matrix.arch }} - - name: Setup Ant - uses: cedx/setup-ant@v3 - - name: Write build_number and revision to todo.txt and Base.java - run: | - echo "${{ needs.version.outputs.build_number }} (${{ needs.version.outputs.version }})" > todo.txt - perl -pi -e 's/static private final int REVISION = \d+;/static private final int REVISION = ${{ needs.version.outputs.build_number }};/g; s/static private String VERSION_NAME = "\d+";\s*\/\/\$NON-NLS-1\$/static private String VERSION_NAME = "${{ needs.version.outputs.build_number }}"; \/\/\$NON-NLS-1\$/g' app/src/processing/app/Base.java + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: processing-${{ needs.version.outputs.version }}-windows-${{ matrix.arch }}-portable.zip + file: app/build/compose/binaries/main/Processing-${{ needs.version.outputs.version }}.zip + + - name: Upload installer + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: processing-${{ needs.version.outputs.version }}-windows-${{ matrix.arch }}.msi + file: app/build/compose/binaries/main/msi/Processing-${{ needs.version.outputs.version }}.msi + + release-macos: + name: (macOS/${{ matrix.arch }}) Create Processing Release + runs-on: macos-latest + needs: version + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + - arch: aarch64 + steps: - name: Install Certificates for Code Signing - if: ${{ matrix.os_prefix == 'macos' }} + continue-on-error: true uses: apple-actions/import-codesign-certs@v3 - with: + with: p12-file-base64: ${{ secrets.CERTIFICATES_P12 }} p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }} - - name: Build Release - run: ant -noinput -buildfile build/build.xml ${{ matrix.os_prefix }}-dist -Dversion="${{ needs.version.outputs.version }}" -Dplatform=${{ matrix.os_prefix }} + + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Processing + uses: ./.github/actions/setup + with: + arch: ${{ matrix.arch }} + + - name: Build with Gradle + run: ./gradlew packageDistributionForCurrentOS + env: + ORG_GRADLE_PROJECT_version: ${{ needs.version.outputs.version }} + ORG_GRADLE_PROJECT_group: ${{ vars.GRADLE_GROUP }} + ORG_GRADLE_PROJECT_revision: ${{ needs.version.outputs.revision }} + ORG_GRADLE_PROJECT_compose.desktop.verbose: true + ORG_GRADLE_PROJECT_compose.desktop.mac.sign: ${{ secrets.PROCESSING_SIGNING }} + ORG_GRADLE_PROJECT_compose.desktop.mac.signing.identity: ${{ secrets.PROCESSING_SIGNING_IDENTITY }} + ORG_GRADLE_PROJECT_compose.desktop.mac.notarization.appleID: ${{ secrets.PROCESSING_APPLE_ID }} + ORG_GRADLE_PROJECT_compose.desktop.mac.notarization.password: ${{ secrets.PROCESSING_APP_PASSWORD }} + ORG_GRADLE_PROJECT_compose.desktop.mac.notarization.teamID: ${{ secrets.PROCESSING_TEAM_ID }} + + - name: Upload portables to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: processing-${{ needs.version.outputs.version }}-macos-${{ matrix.arch }}-portable.zip + file: app/build/compose/binaries/main/Processing-${{ needs.version.outputs.version }}.zip + + - name: Upload installers to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: processing-${{ needs.version.outputs.version }}-macos-${{ matrix.arch }}.dmg + file: app/build/compose/binaries/main/dmg/Processing-${{ needs.version.outputs.version }}.dmg + + release-linux: + name: (linux/${{ matrix.arch }}) Create Processing Release + runs-on: ${{ matrix.os }} + needs: version + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04-arm + arch: aarch64 + deb: arm64 + - os: ubuntu-latest + arch: x64 + deb: amd64 + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Processing + uses: ./.github/actions/setup + + - name: Build with Gradle + run: ./gradlew packageDistributionForCurrentOS env: - PROCESSING_APP_SIGNING: true - PROCESSING_APP_PASSWORD: ${{ secrets.PROCESSING_APP_PASSWORD }} - PROCESSING_APPLE_ID: ${{ secrets.PROCESSING_APPLE_ID }} - PROCESSING_TEAM_ID: ${{ secrets.PROCESSING_TEAM_ID }} - - name: Upload binaries to release + ORG_GRADLE_PROJECT_version: ${{ needs.version.outputs.version }} + ORG_GRADLE_PROJECT_group: ${{ vars.GRADLE_GROUP }} + ORG_GRADLE_PROJECT_revision: ${{ needs.version.outputs.revision }} + ORG_GRADLE_PROJECT_compose.desktop.verbose: true + + - name: Upload portable to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: processing-${{ needs.version.outputs.version }}-linux-${{ matrix.arch }}-portable.zip + file: app/build/compose/binaries/main/Processing-${{ needs.version.outputs.version }}.zip + + - name: Upload installer to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: processing-${{ needs.version.outputs.version }}-linux-${{ matrix.arch }}.deb + file: app/build/compose/binaries/main/deb/processing_${{ needs.version.outputs.version }}-1_${{ matrix.deb }}.deb + + - name: Add artifact + uses: actions/upload-artifact@v4 + with: + name: processing_${{ needs.version.outputs.version }}-1_${{ matrix.deb }}.deb + retention-days: 1 + path: app/build/compose/binaries/main/deb/processing_${{ needs.version.outputs.version }}-1_${{ matrix.deb }}.deb + + release-linux-snap: + name: (linux/${{ matrix.arch }}) Create Processing Snap Release + runs-on: ${{ matrix.os }} + needs: [version, release-linux] + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04-arm + arch: aarch64 + deb: arm64 + - os: ubuntu-latest + arch: x64 + deb: amd64 + steps: + - name: Install Snapcraft + uses: samuelmeuli/action-snapcraft@v3 + + - name: Install LXD + uses: canonical/setup-lxd@main + + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: processing_${{ needs.version.outputs.version }}-1_${{ matrix.deb }}.deb + path: app/build/compose/binaries/main/deb/ + + - name: Build with Gradle + run: ./gradlew packageSnap + env: + ORG_GRADLE_PROJECT_version: ${{ needs.version.outputs.version }} + ORG_GRADLE_PROJECT_group: ${{ vars.GRADLE_GROUP }} + ORG_GRADLE_PROJECT_revision: ${{ needs.version.outputs.revision }} + ORG_GRADLE_PROJECT_snapname: ${{ vars.SNAP_NAME }} + ORG_GRADLE_PROJECT_snapconfinement: ${{ vars.SNAP_CONFINEMENT }} + + - name: Upload snap to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + asset_name: processing-${{ needs.version.outputs.version }}-linux-${{ matrix.arch }}.snap + file: app/build/compose/binaries/main/${{ vars.SNAP_NAME }}_${{ needs.version.outputs.version }}_${{ matrix.deb }}.snap + + - name: Upload snap to Snap Store + run: snapcraft upload --release=beta app/build/compose/binaries/main/${{ vars.SNAP_NAME }}_${{ needs.version.outputs.version }}_${{ matrix.deb }}.snap + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.PROCESSING_SNAPCRAFT_TOKEN }} + release-linux-flatpak: + name: (linux/${{ matrix.arch }}) Create Processing Flatpak Release + runs-on: ${{ matrix.os }} + needs: [ version, release-linux ] + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48 + options: --privileged + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04-arm + arch: aarch64 + deb: arm64 + farch: aarch64 + - os: ubuntu-latest + arch: x64 + deb: amd64 + farch: x86_64 + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: processing_${{ needs.version.outputs.version }}-1_${{ matrix.deb }}.deb + path: app/build/compose/binaries/main/deb/ + + - name: Setup Processing + uses: ./.github/actions/setup + + - name: Build with Gradle + run: ./gradlew generateFlatpakConfiguration + env: + ORG_GRADLE_PROJECT_version: ${{ needs.version.outputs.version }} + ORG_GRADLE_PROJECT_group: ${{ vars.GRADLE_GROUP }} + ORG_GRADLE_PROJECT_revision: ${{ needs.version.outputs.revision }} + + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: processing.flatpak + manifest-path: app/build/compose/binaries/main/flatpak/org.processing.pde.yml + cache-key: flatpak-builder-${{ github.sha }} + arch: ${{ matrix.farch }} + + - name: Upload Flatpak to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ./build/${{ matrix.os_prefix }}/processing-${{ needs.version.outputs.version }}-${{ matrix.os_prefix}}-* - file_glob: true + asset_name: processing-${{ needs.version.outputs.version }}-linux-${{ matrix.arch }}.flatpak + file: processing.flatpak \ No newline at end of file diff --git a/.gitignore b/.gitignore index 40c07a3035..a6e0752889 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,19 @@ gen-external-apklibs hs_err_pid* replay_pid* +# Maven ignores +.kotlin +.gradle +.build/ +/core/build/ +/build/publish/ +/app/build +/java/build/ +/build/reports +/java/bin +/java/libraries/svg/bin +/java/preprocessor/build +/java/lsp/build ### Gradle ### .gradle **/build/ @@ -124,4 +137,16 @@ generated/ !java/libraries/serial/library/jssc.jar /app/windows/obj /java/gradle/build +/core/examples/build /java/gradle/example/.processing +/app/windows/obj +/java/android/example/build +/java/android/example/.processing +/java/gradle/example/build +/java/gradle/example/gradle/wrapper/gradle-wrapper.jar +/java/gradle/example/gradle/wrapper/gradle-wrapper.properties +/java/gradle/example/gradlew +/java/gradle/example/gradlew.bat +/java/gradle/example/.kotlin/errors +/java/gradle/hotreload/build +*.iml diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8328a40c38..ad532703d8 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -218,7 +218,7 @@ _Note: due to GitHub's limitations, this repository's [Contributors](https://git Benjamin Fox
Benjamin Fox

💻 e1dem
e1dem

💻 Aditya Chaudhary
Aditya Chaudhary

💻 - Rishab Kumar Jha
Rishab Kumar Jha

💻 + Rishab Kumar Jha
Rishab Kumar Jha

💻 ⚠️ Yehia Rasheed
Yehia Rasheed

💻 @@ -251,6 +251,12 @@ _Note: due to GitHub's limitations, this repository's [Contributors](https://git Madhav Majumdar
Madhav Majumdar

💻 Dino_Ww
Dino_Ww

📖 Avinash Kumar Deepak
Avinash Kumar Deepak

💻 + Sumama Sonia
Sumama Sonia

💻 + + + Salmane Khalili
Salmane Khalili

🐛 + Nick McIntyre
Nick McIntyre

⚠️ + Elijah Baron
Elijah Baron

🐛 diff --git a/README.md b/README.md index 0a8a6d4369..c71c919019 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,7 @@ Processing is a flexible software sketchbook and a programming language designed This repository contains the source code for the [Processing](https://processing.org/) project for people who want to help improve the code. -## Welcome to Processing 4.4! - -We’re excited to announce the release of Processing 4.4! This update modernizes Processing under the hood to make future development easier. Key changes include switching the build system from Ant to Gradle, starting the transition from Swing to Jetpack Compose Multiplatform for the UI, and adding Kotlin support to the codebase. To learn more, check out [Changes in 4.4.0](https://github.com/processing/processing4/wiki/Changes-in-4.4). - -We hope these updates will make it easier for more people to contribute to Processing. If you'd like to get involved, have a look at our [Contributor Guide](CONTRIBUTING.md). +We welcome new contributors! Join us on the [Discord Server](https://discord.processing.org), and see the [Contributor Guide](CONTRIBUTING.md) for guidelines and tips on getting started. ## Acknowledgement Processing was initiated in 2001 by Ben Fry and Casey Reas, who led the development and maintenance of the project until 2023. We are grateful for their vision and dedication to the project. Processing is also indebted to over two decades of contributions from the broader Processing community. @@ -73,4 +69,4 @@ Copyright (c) 2015-now The Processing Foundation ## Contributors See [CONTRIBUTORS.md](./CONTRIBUTORS.md) for a list of all contributors to the project. -This project follows the [all-contributors specification](https://github.com/all-contributors/all-contributors) and the [Emoji Key](https://all-contributors.github.io/emoji-key/) ✨ for contribution types. Detailed instructions on how to add yourself or add contribution emojis to your name are [here](https://github.com/processing/processing4/issues/839). You can also post an issue or comment on a pull request with the text: `@all-contributors please add @YOUR-USERNAME for THINGS` (where `THINGS` is a comma-separated list of entries from the [list of possible contribution types](https://all-contributors.github.io/emoji-key/)) and our nice bot will add you to [CONTRIBUTORS.md](./CONTRIBUTORS.md) automatically! \ No newline at end of file +This project follows the [all-contributors specification](https://github.com/all-contributors/all-contributors) and the [Emoji Key](https://all-contributors.github.io/emoji-key/) ✨ for contribution types. Detailed instructions on how to add yourself or add contribution emojis to your name are [here](https://github.com/processing/processing4/issues/839). You can also post an issue or comment on a pull request with the text: `@all-contributors please add @YOUR-USERNAME for THINGS` (where `THINGS` is a comma-separated list of entries from the [list of possible contribution types](https://all-contributors.github.io/emoji-key/)) and our nice bot will add you to [CONTRIBUTORS.md](./CONTRIBUTORS.md) automatically! diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 48d49eea20..4f91e6d98c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -65,6 +65,14 @@ compose.desktop { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "Processing" + + + + fileAssociation("application/x-processing","pde", "Processing Source Code",rootProject.file("build/shared/lib/icons/pde-512.png"), rootProject.file("build/windows/pde.ico"), rootProject.file("build/macos/pde.icns")) + fileAssociation("application/x-processing","pyde", "Processing Python Source Code",rootProject.file("build/shared/lib/icons/pde-512.png"), rootProject.file("build/windows/pde.ico"), rootProject.file("build/macos/pde.icns")) + fileAssociation("application/x-processing","pdez", "Processing Sketch Bundle",rootProject.file("build/shared/lib/icons/pde-512.png"), rootProject.file("build/windows/pdze.ico"), rootProject.file("build/macos/pdez.icns")) + fileAssociation("application/x-processing","pdex", "Processing Contribution Bundle", rootProject.file("build/shared/lib/icons/pde-512.png"), rootProject.file("build/windows/pdex.ico"), rootProject.file("build/macos/pdex.icns")) + macOS{ bundleID = "${rootProject.group}.app" iconFile = rootProject.file("build/macos/processing.icns") @@ -82,16 +90,13 @@ compose.desktop { upgradeUuid = "89d8d7fe-5602-4b12-ba10-0fe78efbd602" } linux { - appCategory = "Programming" + debMaintainer = "hello@processing.org" menuGroup = "Development;Programming;" + appCategory = "Programming" iconFile = rootProject.file("build/linux/processing.png") // Fix fonts on some Linux distributions jvmArgs("-Dawt.useSystemAAFontSettings=on") - fileAssociation("pde", "Processing Source Code", "application/x-processing") - fileAssociation("pyde", "Processing Python Source Code", "application/x-processing") - fileAssociation("pdez", "Processing Sketch Bundle", "application/x-processing") - fileAssociation("pdex", "Processing Contribution Bundle", "application/x-processing") } } } @@ -166,6 +171,12 @@ tasks.register("lsp-develop"){ } val version = if(project.version == "unspecified") "1.0.0" else project.version +val distributable = { tasks.named("createDistributable").get() } +val arch = when (System.getProperty("os.arch")) { + "amd64", "x86_64" -> "amd64" + "aarch64" -> "arm64" + else -> System.getProperty("os.arch") +} tasks.register("installCreateDmg") { onlyIf { OperatingSystem.current().isMacOsX } @@ -175,11 +186,10 @@ tasks.register("packageCustomDmg"){ onlyIf { OperatingSystem.current().isMacOsX } group = "compose desktop" - val distributable = tasks.named("createDistributable").get() - dependsOn(distributable, "installCreateDmg") + dependsOn(distributable(), "installCreateDmg") - val packageName = distributable.packageName.get() - val dir = distributable.destinationDir.get() + val packageName = distributable().packageName.get() + val dir = distributable().destinationDir.get() val dmg = dir.file("../dmg/$packageName-$version.dmg").asFile val app = dir.file("$packageName.app").asFile @@ -234,64 +244,123 @@ tasks.register("packageCustomMsi"){ ) } - tasks.register("generateSnapConfiguration"){ - onlyIf { OperatingSystem.current().isLinux } - - val distributable = tasks.named("createDistributable").get() - dependsOn(distributable) - val name = findProperty("snapname") as String? ?: rootProject.name - val arch = when (System.getProperty("os.arch")) { - "amd64", "x86_64" -> "amd64" - "aarch64" -> "arm64" - else -> System.getProperty("os.arch") + val confinement = (findProperty("snapconfinement") as String?).takeIf { !it.isNullOrBlank() } ?: "strict" + val dir = distributable().destinationDir.get() + val base = layout.projectDirectory.file("linux/snapcraft.yml") + + doFirst { + replaceVariablesInFile( + base, + dir.file("../snapcraft.yaml"), + mapOf( + "name" to name, + "arch" to arch, + "version" to version as String, + "confinement" to confinement, + "deb" to "deb/${rootProject.name}_${version}-1_${arch}.deb" + ), + if (confinement == "classic") listOf("PLUGS") else emptyList() + ) } - val confinement = findProperty("snapconfinement") as String? ?: "strict" - val dir = distributable.destinationDir.get() - val base = layout.projectDirectory.file("linux/snapcraft.base.yml") +} +tasks.register("generateFlatpakConfiguration"){ + val identifier = findProperty("flathubidentifier") as String? ?: "org.processing.pde" + + val dir = distributable().destinationDir.get() + val base = layout.projectDirectory.file("linux/flathub.yml") doFirst { + replaceVariablesInFile( + base, + dir.file("../flatpak/$identifier.yml"), + mapOf( + "identifier" to identifier, + "deb" to dir.file("../deb/${rootProject.name}_${version}-1_${arch}.deb").asFile.absolutePath + ), + emptyList() + ) + } +} - var content = base - .asFile - .readText() - .replace("\$name", name) - .replace("\$arch", arch) - .replace("\$version", version as String) - .replace("\$confinement", confinement) - .let { - if (confinement != "classic") return@let it - // If confinement is not strict, remove the PLUGS section - val start = it.indexOf("# PLUGS START") - val end = it.indexOf("# PLUGS END") - if (start != -1 && end != -1) { - val before = it.substring(0, start) - val after = it.substring(end + "# PLUGS END".length) - return@let before + after - } - return@let it +fun replaceVariablesInFile( + source: RegularFile, + target: RegularFile, + variables: Map, + sections: List +){ + var content = source.asFile.readText() + for ((key, value) in variables) { + content = content.replace("\$$key", value) + } + if (sections.isNotEmpty()) { + for (section in sections) { + val start = content.indexOf("# $section START") + val end = content.indexOf("# $section END") + if (start != -1 && end != -1) { + val before = content.substring(0, start) + val after = content.substring(end + "# $section END".length) + content = before + after } - dir.file("../snapcraft.yaml").asFile.writeText(content) + } } + target.asFile.parentFile.mkdirs() + target.asFile.writeText(content) } tasks.register("packageSnap"){ onlyIf { OperatingSystem.current().isLinux } - dependsOn("packageDeb", "generateSnapConfiguration") + dependsOn("generateSnapConfiguration") group = "compose desktop" - val distributable = tasks.named("createDistributable").get() - workingDir = distributable.destinationDir.dir("../").get().asFile + workingDir = distributable().destinationDir.dir("../").get().asFile commandLine("snapcraft") } + +tasks.register("buildFlatpak"){ + onlyIf { OperatingSystem.current().isLinux } + dependsOn("generateFlatpakConfiguration") + group = "compose desktop" + + val dir = distributable().destinationDir.get() + val identifier = findProperty("flathubidentifier") as String? ?: "org.processing.pde" + + workingDir = dir.file("../flatpak").asFile + commandLine( + "flatpak-builder", + "--install-deps-from=https://flathub.org/repo/flathub.flatpakrepo", + "--user", + "--force-clean", + "--repo=repo", + "output", + "$identifier.yml" + ) +} + +tasks.register("packageFlatpak"){ + onlyIf { OperatingSystem.current().isLinux } + dependsOn("buildFlatpak") + group = "compose desktop" + + val dir = distributable().destinationDir.get() + val identifier = findProperty("flathubidentifier") as String? ?: "org.processing.pde" + + workingDir = dir.file("../flatpak").asFile + commandLine( + "flatpak", + "build-bundle", + "./repo", + "$identifier.flatpak", + identifier + ) +} tasks.register("zipDistributable"){ dependsOn("createDistributable", "setExecutablePermissions") group = "compose desktop" - val distributable = tasks.named("createDistributable").get() - val dir = distributable.destinationDir.get() - val packageName = distributable.packageName.get() + val dir = distributable().destinationDir.get() + val packageName = distributable().packageName.get() from(dir){ eachFile{ permissions{ unix("755") } } } archiveBaseName.set(packageName) @@ -317,7 +386,7 @@ afterEvaluate{ ){ dependsOn("notarizeDmg") } - dependsOn("packageSnap", "zipDistributable") + dependsOn("zipDistributable") } } @@ -342,6 +411,7 @@ tasks.register("includeJavaMode") { from(java.configurations.runtimeClasspath) into(composeResources("modes/java/mode")) duplicatesStrategy = DuplicatesStrategy.EXCLUDE + dirPermissions { unix("rwx------") } } tasks.register("includeJdk") { from(Jvm.current().javaHome.absolutePath) diff --git a/app/linux/flathub.yml b/app/linux/flathub.yml new file mode 100644 index 0000000000..c92ab17d9d --- /dev/null +++ b/app/linux/flathub.yml @@ -0,0 +1,38 @@ +id: $identifier +runtime: org.freedesktop.Platform +runtime-version: '24.08' +sdk: org.freedesktop.Sdk +command: Processing +finish-args: + - --socket=x11 + - --share=ipc + - --device=all + - --filesystem=home + - --socket=pulseaudio + - --share=network + +modules: + - name: Processing + buildsystem: simple + build-commands: + # Unpack Processing deb + - ar x processing.deb + - tar --zstd -xf data.tar.zst + - mv opt/processing/* /app/ + - find /app/lib/app/resources/jdk/bin -type f -exec chmod +x {} + + + # Install the desktop file and icon + - install -D /app/lib/processing-Processing.desktop /app/share/applications/$identifier.desktop + - sed -i 's/^Icon=.*/Icon=$identifier/' /app/share/applications/$identifier.desktop + - sed -i 's/^Exec=.*/Exec=\/app\/bin\/Processing/' /app/share/applications/$identifier.desktop + + # Install the mimetype info + - install -D /app/lib/processing-Processing-MimeInfo.xml /app/share/mime/packages/$identifier.xml + +# - install -D /app/lib/Processing.png /app/share/icons/hicolor/512x512/apps/$identifier.png + - install -D /app/lib/application-x-processing.png /app/share/icons/hicolor/512x512/mimetypes/$identifier-text-x-processing.png + + sources: + - type: file + path: $deb + dest-filename: processing.deb \ No newline at end of file diff --git a/app/linux/snapcraft.base.yml b/app/linux/snapcraft.yml similarity index 95% rename from app/linux/snapcraft.base.yml rename to app/linux/snapcraft.yml index 4847f0a7c8..8206681ef0 100644 --- a/app/linux/snapcraft.base.yml +++ b/app/linux/snapcraft.yml @@ -32,7 +32,7 @@ apps: parts: processing: plugin: dump - source: deb/processing_$version-1_$arch.deb + source: $deb source-type: deb stage-packages: - openjdk-17-jre diff --git a/app/macos/info.plist b/app/macos/info.plist index 974db7d45b..f4569e85b2 100644 --- a/app/macos/info.plist +++ b/app/macos/info.plist @@ -9,65 +9,6 @@ - CFBundleDocumentTypes - - - CFBundleTypeExtensions - - pde - - LSTypeIsPackage - - CFBundleTypeIconFile - macos/pde.icns - CFBundleTypeName - Processing Source Code - CFBundleTypeRole - Editor - - - CFBundleTypeExtensions - - pyde - - LSTypeIsPackage - - CFBundleTypeIconFile - macos/pde.icns - CFBundleTypeName - Processing Python Source Code - CFBundleTypeRole - Editor - - - CFBundleTypeExtensions - - pdez - - LSTypeIsPackage - - CFBundleTypeIconFile - macos/pdez.icns - CFBundleTypeName - Processing Sketch Bundle - CFBundleTypeRole - Editor - - - CFBundleTypeExtensions - - pdex - - LSTypeIsPackage - - CFBundleTypeIconFile - macos/pdex.icns - CFBundleTypeName - Processing Contribution Bundle - CFBundleTypeRole - Viewer - - NSCameraUsageDescription The sketch you're running needs access to your video camera. NSMicrophoneUsageDescription diff --git a/app/src/main/resources/languages/PDE.properties b/app/src/main/resources/languages/PDE.properties index 3c1ad2ab70..c9189efa3a 100644 --- a/app/src/main/resources/languages/PDE.properties +++ b/app/src/main/resources/languages/PDE.properties @@ -135,7 +135,7 @@ menu.tools.color_selector = Color Selector... menu.tools.create_font = Create Font... menu.tools.archive_sketch = Archive Sketch menu.tools.fix_the_serial_lbrary = Fix the Serial Library -menu.tools.install_processing_java = Install “processing-java” +menu.tools.install_processing_java=Install “processing” # menu.tools.add_tool = Add Tool... menu.tools.manage_tools = Manage Tools… diff --git a/app/src/main/resources/languages/PDE_ar.properties b/app/src/main/resources/languages/PDE_ar.properties index 2f25f33e4d..2365f40fea 100644 --- a/app/src/main/resources/languages/PDE_ar.properties +++ b/app/src/main/resources/languages/PDE_ar.properties @@ -105,7 +105,7 @@ menu.tools.color_selector = أداة اختيار الألوان... menu.tools.create_font = أداة صناعة الخطوط menu.tools.archive_sketch = أرشفة المخطوط menu.tools.fix_the_serial_lbrary = إصلاح مكتبة الاتصالات التسلسلية -menu.tools.install_processing_java = تثبيت"processing-java" +menu.tools.install_processing_java = تثبيت"processing" menu.tools.add_tool = أضف أداة # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_ca.properties b/app/src/main/resources/languages/PDE_ca.properties index cdff9ec65b..77187986ba 100644 --- a/app/src/main/resources/languages/PDE_ca.properties +++ b/app/src/main/resources/languages/PDE_ca.properties @@ -126,7 +126,7 @@ menu.tools.color_selector = Selector de color... menu.tools.create_font = Creació de fonts tipogràfiques... menu.tools.archive_sketch = Arxiva l'sketch menu.tools.fix_the_serial_lbrary = Arregla la «Serial Library» -menu.tools.install_processing_java = Instal·la «processing-java» +menu.tools.install_processing_java = Instal·la «processing» # menu.tools.add_tool = Afegeix una eina... menu.tools.manage_tools = Gestiona les eines... diff --git a/app/src/main/resources/languages/PDE_de.properties b/app/src/main/resources/languages/PDE_de.properties index 1b0c4b48d3..53dadf64bb 100644 --- a/app/src/main/resources/languages/PDE_de.properties +++ b/app/src/main/resources/languages/PDE_de.properties @@ -100,7 +100,7 @@ menu.tools.color_selector = Farbauswahl ... menu.tools.create_font = Schrift erstellen ... menu.tools.archive_sketch = Sketch archivieren ... menu.tools.fix_the_serial_lbrary = "Serial Library" beheben ... -menu.tools.install_processing_java = "processing-java" installieren ... +menu.tools.install_processing_java="processing" installieren ... #menu.tools.add_tool = Tool hinzufügen ... menu.tools.manage_tools = Tools verwalten... diff --git a/app/src/main/resources/languages/PDE_el.properties b/app/src/main/resources/languages/PDE_el.properties index d4c794bde8..e7e29cb509 100644 --- a/app/src/main/resources/languages/PDE_el.properties +++ b/app/src/main/resources/languages/PDE_el.properties @@ -100,7 +100,7 @@ menu.tools.color_selector = Επιλογή Χρώματος... menu.tools.create_font = Δημιουργία Γραμματοσειράς... menu.tools.archive_sketch = Αρχειοθέτηση Σχεδίου menu.tools.fix_the_serial_lbrary = Διόρθωση Σειριακής Βιβλιοθήκης -menu.tools.install_processing_java = Εγκατάσταση της "processing-java" +menu.tools.install_processing_java = Εγκατάσταση της "processing" menu.tools.add_tool = Προσθήκη Εργαλείου... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_es.properties b/app/src/main/resources/languages/PDE_es.properties index 781a5a261a..1df196955b 100644 --- a/app/src/main/resources/languages/PDE_es.properties +++ b/app/src/main/resources/languages/PDE_es.properties @@ -126,7 +126,7 @@ menu.tools.color_selector = Selector de colores... menu.tools.create_font = Crear fuente... menu.tools.archive_sketch = Archivar sketch menu.tools.fix_the_serial_lbrary = Corregir «Serial library» -menu.tools.install_processing_java = Instalar «processing-java» +menu.tools.install_processing_java = Instalar «processing» # menu.tools.add_tool = Añadir herramienta... menu.tools.manage_tools = Gestionar herramientas... diff --git a/app/src/main/resources/languages/PDE_fr.properties b/app/src/main/resources/languages/PDE_fr.properties index d519f2e1d8..17a222a671 100644 --- a/app/src/main/resources/languages/PDE_fr.properties +++ b/app/src/main/resources/languages/PDE_fr.properties @@ -83,7 +83,7 @@ menu.tools.color_selector = Sélecteur de couleurs... menu.tools.create_font = Générer la police... menu.tools.archive_sketch = Archiver le sketch... menu.tools.fix_the_serial_lbrary = Réparer la "Serial Library"... -menu.tools.install_processing_java = Installer "processing-java"... +menu.tools.install_processing_java = Installer "processing"... menu.tools.add_tool = Ajouter un outil... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_it.properties b/app/src/main/resources/languages/PDE_it.properties index 374232430e..c7931526ac 100644 --- a/app/src/main/resources/languages/PDE_it.properties +++ b/app/src/main/resources/languages/PDE_it.properties @@ -100,7 +100,7 @@ menu.tools.color_selector = Selezionatore dei colori... menu.tools.create_font = Crea Font... menu.tools.archive_sketch = Archivia Sketch menu.tools.fix_the_serial_lbrary = Ripara la "Serial Library" -menu.tools.install_processing_java = Installa "processing-java" +menu.tools.install_processing_java = Installa "processing" menu.tools.add_tool = Aggiungi Strumento... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_ja.properties b/app/src/main/resources/languages/PDE_ja.properties index 3297f9ced1..e506f9c7e2 100644 --- a/app/src/main/resources/languages/PDE_ja.properties +++ b/app/src/main/resources/languages/PDE_ja.properties @@ -105,7 +105,7 @@ menu.tools.color_selector = 色選択... menu.tools.create_font = フォント作成... menu.tools.archive_sketch = スケッチをアーカイブ menu.tools.fix_the_serial_lbrary = シリアルライブラリを修正 -menu.tools.install_processing_java = "processing-java" をインストール +menu.tools.install_processing_java = "processing" をインストール menu.tools.add_tool = ツールを追加... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_ko.properties b/app/src/main/resources/languages/PDE_ko.properties index 1e449dacc0..f3491ccc96 100644 --- a/app/src/main/resources/languages/PDE_ko.properties +++ b/app/src/main/resources/languages/PDE_ko.properties @@ -77,7 +77,7 @@ menu.tools.create_font = 글꼴 생성... menu.tools.color_selector = 색상 선택 menu.tools.archive_sketch = .zip으로 압축하기 menu.tools.fix_the_serial_lbrary = 시리얼 라이브러리 오류 수정 -menu.tools.install_processing_java = "processing-java" 설치 +menu.tools.install_processing_java = "processing" 설치 menu.tools.add_tool = 추가도구 생성... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_nl.properties b/app/src/main/resources/languages/PDE_nl.properties index 7c43e5d68b..b8278d1c24 100644 --- a/app/src/main/resources/languages/PDE_nl.properties +++ b/app/src/main/resources/languages/PDE_nl.properties @@ -72,7 +72,7 @@ menu.tools.color_selector = Kleur Selecteren... menu.tools.create_font = Lettertype Maken... menu.tools.archive_sketch = Schets Archiveren menu.tools.fix_the_serial_lbrary = Seriële Bibliotheek Herstellen -menu.tools.install_processing_java = Installeren "processing-java" +menu.tools.install_processing_java = Installeren "processing" menu.tools.add_tool = Tool Toevoegen... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_pt.properties b/app/src/main/resources/languages/PDE_pt.properties index 44b5c7f639..2c9e3bc527 100644 --- a/app/src/main/resources/languages/PDE_pt.properties +++ b/app/src/main/resources/languages/PDE_pt.properties @@ -71,7 +71,7 @@ menu.tools.color_selector = Selector de Cor... menu.tools.create_font = Criar Fonte... menu.tools.archive_sketch = Arquivar Sketch menu.tools.fix_the_serial_lbrary = Corrijir a Biblioteca Serial -menu.tools.install_processing_java = Instalar "processing-java" +menu.tools.install_processing_java = Instalar "processing" menu.tools.add_tool = Adicionar Ferramenta... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_ru.properties b/app/src/main/resources/languages/PDE_ru.properties index ff71d4ce49..b5df6de76f 100644 --- a/app/src/main/resources/languages/PDE_ru.properties +++ b/app/src/main/resources/languages/PDE_ru.properties @@ -100,7 +100,7 @@ menu.tools.color_selector = Выбрать цвет... menu.tools.create_font = Создать шрифты... menu.tools.archive_sketch = Архивировать набросок menu.tools.fix_the_serial_lbrary = Исправить библиотеку Serial -menu.tools.install_processing_java = Установить "processing-java" +menu.tools.install_processing_java = Установить "processing" menu.tools.add_tool = Добавить инструмент... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_tr.properties b/app/src/main/resources/languages/PDE_tr.properties index 8b8b7f7f55..8d0782939b 100644 --- a/app/src/main/resources/languages/PDE_tr.properties +++ b/app/src/main/resources/languages/PDE_tr.properties @@ -72,7 +72,7 @@ menu.tools.color_selector = Renk Seçici menu.tools.create_font = Yazı Tipi Oluştur... menu.tools.archive_sketch = Sketch'i Arşivle menu.tools.fix_the_serial_lbrary = "Serial Kütüphanesi"ni Onar... -menu.tools.install_processing_java = "Processing-Java"yı Yükle... +menu.tools.install_processing_java = "Processing"yı Yükle... menu.tools.add_tool = Araç Ekle... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_uk.properties b/app/src/main/resources/languages/PDE_uk.properties index c4d4d6baf6..78c8b07050 100644 --- a/app/src/main/resources/languages/PDE_uk.properties +++ b/app/src/main/resources/languages/PDE_uk.properties @@ -126,7 +126,7 @@ menu.tools.color_selector = Вибрати колір... menu.tools.create_font = Створити шрифт... menu.tools.archive_sketch = Архівувати ескіз menu.tools.fix_the_serial_lbrary = Виправити Serial Library -menu.tools.install_processing_java = Встановити "processing-java" +menu.tools.install_processing_java = Встановити "processing" # menu.tools.add_tool = Додати інструмент... menu.tools.manage_tools = Керувати інструментами... diff --git a/app/src/main/resources/languages/PDE_zh-CN.properties b/app/src/main/resources/languages/PDE_zh-CN.properties index 77d3c15acc..d475571a58 100644 --- a/app/src/main/resources/languages/PDE_zh-CN.properties +++ b/app/src/main/resources/languages/PDE_zh-CN.properties @@ -92,7 +92,7 @@ menu.tools.color_selector = 颜色选择器... menu.tools.create_font = 创建字体... menu.tools.archive_sketch = 速写本压缩输出 menu.tools.fix_the_serial_lbrary = 修复串口库文件 -menu.tools.install_processing_java = 安装 "processing-java" +menu.tools.install_processing_java = 安装 "processing" menu.tools.add_tool = 添加工具... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/main/resources/languages/PDE_zh-TW.properties b/app/src/main/resources/languages/PDE_zh-TW.properties index e3642d0999..33a7e48e99 100644 --- a/app/src/main/resources/languages/PDE_zh-TW.properties +++ b/app/src/main/resources/languages/PDE_zh-TW.properties @@ -126,7 +126,7 @@ menu.tools.color_selector = 顏色選擇器(Color Selector)... menu.tools.create_font = 建字型檔(Create Font)... menu.tools.archive_sketch = 壓縮程式專案(Archive Sketch) menu.tools.fix_the_serial_library = 修復Serial函式庫(Fix the Serial Library) -menu.tools.install_processing_java = 安裝 "processing-java" +menu.tools.install_processing_java = 安裝 "processing" menu.tools.add_tool = 新增工具(Add Tool)... # | File | Edit | Sketch | Debug | Tools | Help | diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt index 1b344a5e11..4e139b08e1 100644 --- a/app/src/processing/app/Preferences.kt +++ b/app/src/processing/app/Preferences.kt @@ -35,6 +35,9 @@ class ReactiveProperties : Properties() { operator fun set(key: String, value: String) { setProperty(key, value) } + fun remove() { + TODO("Not yet implemented") + } } /* @@ -77,7 +80,7 @@ fun PreferencesProvider(content: @Composable () -> Unit) { val preferencesFileOverride: File? = System.getProperty("processing.app.preferences.file")?.let { File(it) } val preferencesDebounceOverride: Long? = System.getProperty("processing.app.preferences.debounce")?.toLongOrNull() - val settingsFolder = Settings.getFolder() + val settingsFolder = Base.getSettingsOverride() ?: Settings.getFolder() val preferencesFile = preferencesFileOverride ?: settingsFolder.resolve(PREFERENCES_FILE_NAME) if (!preferencesFile.exists()) { diff --git a/app/src/processing/app/Util.java b/app/src/processing/app/Util.java index 4c94af5fe5..f87a6fdcef 100644 --- a/app/src/processing/app/Util.java +++ b/app/src/processing/app/Util.java @@ -60,16 +60,17 @@ static public int countLines(String what) { */ static public byte[] loadBytesRaw(File file) throws IOException { int size = (int) file.length(); - FileInputStream input = new FileInputStream(file); - byte[] buffer = new byte[size]; - int offset = 0; - int bytesRead; - while ((bytesRead = input.read(buffer, offset, size-offset)) != -1) { - offset += bytesRead; - if (bytesRead == 0) break; - } - input.close(); // weren't properly being closed - return buffer; + byte[] buffer; + try (FileInputStream input = new FileInputStream(file)) { + buffer = new byte[size]; + int offset = 0; + int bytesRead; + while ((bytesRead = input.read(buffer, offset, size - offset)) != -1) { + offset += bytesRead; + if (bytesRead == 0) break; + } + } + return buffer; } @@ -143,7 +144,7 @@ static public StringDict readSettings(String filename, String[] lines, boolean a line = line.substring(0, line.indexOf('#')).trim(); } - if (line.length() != 0 && line.charAt(0) != '#') { + if (!line.isEmpty() && line.charAt(0) != '#') { int equals = line.indexOf('='); if (equals == -1) { if (filename != null) { @@ -161,26 +162,20 @@ static public StringDict readSettings(String filename, String[] lines, boolean a } - static public void copyFile(File sourceFile, - File targetFile) throws IOException { - BufferedInputStream from = - new BufferedInputStream(new FileInputStream(sourceFile)); - BufferedOutputStream to = - new BufferedOutputStream(new FileOutputStream(targetFile)); + static public void copyFile(File sourceFile, File targetFile) throws IOException { + try ( + BufferedInputStream from = new BufferedInputStream(new FileInputStream(sourceFile)); + BufferedOutputStream to = new BufferedOutputStream(new FileOutputStream(targetFile))) { byte[] buffer = new byte[16 * 1024]; int bytesRead; while ((bytesRead = from.read(buffer)) != -1) { to.write(buffer, 0, bytesRead); } - from.close(); - - to.flush(); - to.close(); - //noinspection ResultOfMethodCallIgnored targetFile.setLastModified(sourceFile.lastModified()); //noinspection ResultOfMethodCallIgnored targetFile.setExecutable(sourceFile.canExecute()); + } } @@ -218,13 +213,15 @@ static public void saveFile(String text, File file) throws IOException { file.getAbsolutePath()); } // Could use saveStrings(), but we wouldn't be able to checkError() - PrintWriter writer = PApplet.createWriter(temp); - for (String line : lines) { - writer.println(line); - } - boolean error = writer.checkError(); // calls flush() - writer.close(); // attempt to close regardless - if (error) { + boolean error; + try (PrintWriter writer = PApplet.createWriter(temp)) { + for (String line : lines) { + writer.println(line); + } + // calls flush() + error = writer.checkError(); + } + if (error) { throw new IOException("Error while trying to save " + file); } @@ -589,7 +586,7 @@ static public StringList packageListFromClassPath(String path) { for (String piece : pieces) { //System.out.println("checking piece '" + pieces[i] + "'"); - if (piece.length() != 0) { + if (!piece.isEmpty()) { if (piece.toLowerCase().endsWith(".jar") || piece.toLowerCase().endsWith(".zip")) { //System.out.println("checking " + pieces[i]); @@ -623,8 +620,7 @@ static public StringList packageListFromClassPath(String path) { static private void packageListFromZip(String filename, StringList list) { - try { - ZipFile file = new ZipFile(filename); + try (ZipFile file = new ZipFile(filename);) { Enumeration entries = file.entries(); while (entries.hasMoreElements()) { ZipEntry entry = (ZipEntry) entries.nextElement(); @@ -643,7 +639,6 @@ static private void packageListFromZip(String filename, StringList list) { } } } - file.close(); } catch (IOException e) { System.err.println("Ignoring " + filename + " (" + e.getMessage() + ")"); //e.printStackTrace(); @@ -688,9 +683,7 @@ static private void packageListFromFolder(File dir, String sofar, * Ignores (does not extract) any __MACOSX files from macOS archives. */ static public void unzip(File zipFile, File dest) throws IOException { - FileInputStream fis = new FileInputStream(zipFile); - CheckedInputStream checksum = new CheckedInputStream(fis, new Adler32()); - ZipInputStream zis = new ZipInputStream(new BufferedInputStream(checksum)); + try (ZipInputStream zis = new ZipInputStream( new BufferedInputStream( new CheckedInputStream( new FileInputStream(zipFile), new Adler32())))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { final String name = entry.getName(); @@ -710,25 +703,26 @@ static public void unzip(File zipFile, File dest) throws IOException { } } } + } static protected void unzipEntry(ZipInputStream zin, File f) throws IOException { - FileOutputStream out = new FileOutputStream(f); - byte[] b = new byte[512]; - int len; - while ((len = zin.read(b)) != -1) { - out.write(b, 0, len); - } - out.flush(); - out.close(); + try (FileOutputStream out = new FileOutputStream(f)) { + byte[] b = new byte[512]; + int len; + while ((len = zin.read(b)) != -1) { + out.write(b, 0, len); + } + out.flush(); + } } static public byte[] gzipEncode(byte[] what) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - GZIPOutputStream output = new GZIPOutputStream(baos); - PApplet.saveStream(output, new ByteArrayInputStream(what)); - output.close(); + try (GZIPOutputStream output = new GZIPOutputStream(baos);) { + PApplet.saveStream(output, new ByteArrayInputStream(what)); + } return baos.toByteArray(); } diff --git a/app/src/processing/app/tools/InstallCommander.java b/app/src/processing/app/tools/InstallCommander.java index 33eabc6f68..2978cc1a55 100644 --- a/app/src/processing/app/tools/InstallCommander.java +++ b/app/src/processing/app/tools/InstallCommander.java @@ -21,13 +21,6 @@ package processing.app.tools; -import java.io.File; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.PrintWriter; - -import javax.swing.JOptionPane; - import processing.app.Base; import processing.app.Language; import processing.app.Messages; @@ -36,6 +29,12 @@ import processing.core.PApplet; import processing.data.StringList; +import javax.swing.*; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.PrintWriter; + public class InstallCommander implements Tool { Base base; @@ -55,26 +54,31 @@ public void run() { try { Editor editor = base.getActiveEditor(); - final String primary = - "Install processing-java for all users?"; - final String secondary = - "This will install the processing-java program, which is capable " + - "of building and running Java Mode sketches from the command line. " + - "Click “Yes” to install it for all users (an administrator password " + - "is required), or “No” to place the program in your home directory. " + - "If you rename or move Processing.app, " + - "you'll need to reinstall the tool."; + final String messageHtml = """ + + + Install processing for all users? +

+ This utility will install the processing command line interface, + which is capable of building and running + sketches from the command line. +

+ Click “Yes” to install it for all users + (an administrator password is required). +

+ or “No” to place the program in your home directory. +

+ If you rename or move Processing.app, + you'll need to reinstall the tool. +

+ """.replaceAll("\n", " "); int result = - JOptionPane.showConfirmDialog(editor, - " " + - " " + - "" + primary + "" + - "

" + secondary + "

", - "Commander", + JOptionPane.showConfirmDialog(editor, messageHtml, + "Processing CLI", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); @@ -92,7 +96,7 @@ public void run() { var appBinary = (resourcesDir .split("\\.app")[0] + ".app/Contents/MacOS/Processing") .replaceAll(" ", "\\\\ "); - writer.print(appBinary + " cli $@"); + writer.print(appBinary + " $@"); } else { // Ant based distributable @@ -130,7 +134,7 @@ public void run() { // Moving to /usr/local/bin instead of /usr/bin for compatibility // with OS X 10.11 and its "System Integrity Protection" // https://github.com/processing/processing/issues/3497 - String targetPath = "/usr/local/bin/processing-java"; + String targetPath = "/usr/local/bin/processing"; // Remove the old version in case it exists // https://github.com/processing/processing/issues/3786 String oldPath = "/usr/bin/processing-java"; @@ -139,14 +143,14 @@ public void run() { " && /bin/mv " + sourcePath + " " + targetPath; String appleScript = "do shell script \"" + shellScript + "\" with administrator privileges"; - PApplet.exec(new String[] { "osascript", "-e", appleScript }); + PApplet.exec("osascript", "-e", appleScript); } else if (result == JOptionPane.NO_OPTION) { - File targetFile = new File(System.getProperty("user.home"), "processing-java"); + File targetFile = new File(System.getProperty("user.home"), "processing"); String targetPath = targetFile.getAbsolutePath(); if (targetFile.exists()) { Messages.showWarning("File Already Exists", - "The processing-java program already exists at:\n" + + "The processing program already exists at:\n" + targetPath + "\n" + "Please remove it and try again."); } else { diff --git a/app/src/processing/app/ui/Editor.java b/app/src/processing/app/ui/Editor.java index 3ef108a27d..52650ad659 100644 --- a/app/src/processing/app/ui/Editor.java +++ b/app/src/processing/app/ui/Editor.java @@ -148,6 +148,18 @@ public abstract class Editor extends JFrame implements RunnerListener { protected Editor(final Base base, String path, final EditorState state, final Mode mode) throws EditorException { super("Processing", state.getConfig()); + if (Platform.isLinux()) { + // If the frame is already displayable, dispose it to allow undecorated change + if (isDisplayable()) { + dispose(); + } + try { + setUndecorated(true); + } catch (IllegalComponentStateException e) { + System.err.println("Could not set undecorated: " + e.getMessage()); + } + getRootPane().setWindowDecorationStyle(JRootPane.FRAME); + } this.base = base; this.state = state; this.mode = mode; @@ -211,10 +223,7 @@ public void windowDeactivated(WindowEvent e) { spacer.setAlignmentX(Component.LEFT_ALIGNMENT); box.add(spacer); } - if (Platform.isLinux()) { - setUndecorated(true); - getRootPane().setWindowDecorationStyle(JRootPane.FRAME); - } + rebuildModePopup(); toolbar = createToolbar(); @@ -2946,4 +2955,13 @@ public void show(Component component, int x, int y) { super.show(component, x, y); } } + + /** + * Called when clicking on the version number in the footer. + * Return a string with diagnostic info from the sketch, + * or empty string (or null) if not implemented/available. + */ + public String getSketchDiagnostics() { + return ""; + } } diff --git a/app/src/processing/app/ui/EditorFooter.java b/app/src/processing/app/ui/EditorFooter.java index 94860a0abf..7efef4132e 100644 --- a/app/src/processing/app/ui/EditorFooter.java +++ b/app/src/processing/app/ui/EditorFooter.java @@ -109,7 +109,7 @@ public void mousePressed(MouseEvent e) { Base.DEBUG = !Base.DEBUG; editor.updateDevelopMenu(); } - copyDebugInformationToClipboard(); + copyFullDiagnosticsToClipboard(); } }); @@ -120,13 +120,23 @@ public void mousePressed(MouseEvent e) { updateTheme(); } - public static void copyDebugInformationToClipboard() { - var debugInformation = String.join("\n", + public static String getSystemDebugInformation() { + return String.join("\n", "Version: " + Base.getVersionName(), "Revision: " + Base.getRevision(), "OS: " + System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch"), "Java: " + System.getProperty("java.version") + " " + System.getProperty("java.vendor") ); + } + + public static void copyDebugInformationToClipboard() { + var stringSelection = new StringSelection(getSystemDebugInformation()); + var clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(stringSelection, null); + } + + public void copyFullDiagnosticsToClipboard() { + var debugInformation = getSystemDebugInformation() + "\n\n" + editor.getSketchDiagnostics(); var stringSelection = new StringSelection(debugInformation); var clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); diff --git a/app/src/processing/app/ui/PDEPreferences.kt b/app/src/processing/app/ui/PDEPreferences.kt index 020856b8d4..8d1e749c9f 100644 --- a/app/src/processing/app/ui/PDEPreferences.kt +++ b/app/src/processing/app/ui/PDEPreferences.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.window.application import com.mikepenz.markdown.compose.Markdown import com.mikepenz.markdown.m3.markdownColor import com.mikepenz.markdown.m3.markdownTypography +import processing.app.DEFAULTS_FILE_NAME import processing.app.LocalPreferences import processing.app.ReactiveProperties import processing.app.ui.PDEPreferences.Companion.preferences @@ -35,6 +36,7 @@ import processing.app.ui.theme.* import java.awt.Dimension import java.awt.event.WindowEvent import java.awt.event.WindowListener +import java.util.* import javax.swing.SwingUtilities import javax.swing.WindowConstants @@ -592,9 +594,16 @@ fun PDEPreferencePane.showPane(groups: PDEPreferenceGroups) { val prefs = LocalPreferences.current TextButton( onClick = { + val defaultsStream = + ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME) ?: return@TextButton + val defaults = Properties().apply { + defaultsStream.reader(Charsets.UTF_8).use { + load(it) + } + } groups.forEach { group -> group.forEach { pref -> - prefs.remove(pref.key) + prefs[pref.key] = defaults.getProperty(pref.key, "") } } } diff --git a/app/src/processing/app/ui/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt index 0370fc7533..0a13fa5346 100644 --- a/app/src/processing/app/ui/PDEWelcome.kt +++ b/app/src/processing/app/ui/PDEWelcome.kt @@ -571,7 +571,7 @@ fun showWelcomeScreen(base: Base? = null) { unique = WelcomeScreen::class, fullWindowContent = true ) { - PDEWelcomeWithSurvey(base) + PDEWelcome(base) } } @@ -620,12 +620,12 @@ fun main(){ application { PDEComposeWindow(titleKey = titleKey, size = size, fullWindowContent = true) { PDETheme(darkTheme = true) { - PDEWelcomeWithSurvey() + PDEWelcome() } } PDEComposeWindow(titleKey = titleKey, size = size, fullWindowContent = true) { PDETheme(darkTheme = false) { - PDEWelcomeWithSurvey() + PDEWelcome() } } } diff --git a/app/src/processing/app/ui/WelcomeSurvey.kt b/app/src/processing/app/ui/WelcomeSurvey.kt index c0ebb23acf..e68dc44655 100644 --- a/app/src/processing/app/ui/WelcomeSurvey.kt +++ b/app/src/processing/app/ui/WelcomeSurvey.kt @@ -23,17 +23,6 @@ import processing.app.ui.theme.LocalLocale import processing.app.ui.theme.PDETheme import javax.swing.JComponent - -fun addSurveyToWelcomeScreen(): JComponent { - return ComposePanel().apply { - setContent { - PDETheme { - SurveyInvitation() - } - } - } -} - @Composable fun SurveyInvitation() { val locale = LocalLocale.current diff --git a/app/src/processing/app/ui/theme/Locale.kt b/app/src/processing/app/ui/theme/Locale.kt index 90de50a712..4d16f2a5a5 100644 --- a/app/src/processing/app/ui/theme/Locale.kt +++ b/app/src/processing/app/ui/theme/Locale.kt @@ -3,6 +3,7 @@ package processing.app.ui.theme import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection +import processing.app.Base import processing.app.Messages import processing.app.watchFile import processing.utils.Settings @@ -90,7 +91,7 @@ var LastLocaleUpdate by mutableStateOf(0L) */ @Composable fun LocaleProvider(content: @Composable () -> Unit) { - val settingsFolder = Settings.getFolder() + val settingsFolder = Base.getSettingsOverride() ?: Settings.getFolder() val languageFile = File(settingsFolder, "language.txt") watchFile(languageFile) diff --git a/app/utils/build.gradle.kts b/app/utils/build.gradle.kts index 193188f956..1618e1706b 100644 --- a/app/utils/build.gradle.kts +++ b/app/utils/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("java") + alias(libs.plugins.mavenPublish) } repositories { @@ -11,6 +12,15 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter") } +publishing{ + repositories{ + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} + tasks.test { useJUnitPlatform() } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6c8c5262cb..371e34bc29 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.jetbrainsCompose) apply false + alias(libs.plugins.mavenPublish) apply false alias(libs.plugins.versions) } diff --git a/build/linux/processing-pde.xml b/build/linux/processing-pde.xml index 3b8a6837be..a869b1977d 100644 --- a/build/linux/processing-pde.xml +++ b/build/linux/processing-pde.xml @@ -2,41 +2,42 @@ Processing source code - شفرة مصدر Processing - Kryničny kod Processing - Изходен код на Processing - codi font en Processing - Processingkildekode - Processing-Quelltext - πηγαίος κώδικας Processing - Processing source code - Processing-fontkodo - código fuente en Processing - Processing iturburu-kodea - Processing-lähdekoodi - code source Processing - cód foinseach Processing - Processing-forráskód - Kode program Processing - Codice sorgente Processing - Processing ソースコード - Processing pradinis kodas - Processing pirmkods - Kod sumber Processing - Processing-kildekode - Processing-broncode - Processing-kjeldekode - Kod źródłowy Processing - código fonte Processing - Código fonte Processing - исходный код Processing - Kod burues Processing - Processing-källkod - Вихідний код на мові Processing - Mã nguồn Processing - Processing 源代码 - Processing 源代碼 + شفرة مصدر Processing + Kryničny kod Processing + Изходен код на Processing + codi font en Processing + Processingkildekode + Processing-Quelltext + πηγαίος κώδικας Processing + Processing source code + Processing-fontkodo + código fuente en Processing + Processing iturburu-kodea + Processing-lähdekoodi + code source Processing + cód foinseach Processing + Processing-forráskód + Kode program Processing + Codice sorgente Processing + Processing ソースコード + Processing pradinis kodas + Processing pirmkods + Kod sumber Processing + Processing-kildekode + Processing-broncode + Processing-kjeldekode + Kod źródłowy Processing + código fonte Processing + Código fonte Processing + исходный код Processing + Kod burues Processing + Processing-källkod + Вихідний код на мові Processing + Mã nguồn Processing + Processing 源代码 + Processing 源代碼 + diff --git a/build/shared/lib/icons/app-linux.svg b/build/shared/lib/icons/app-linux.svg new file mode 100644 index 0000000000..9b5ccd52b1 --- /dev/null +++ b/build/shared/lib/icons/app-linux.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/core/build.gradle.kts b/core/build.gradle.kts index e0c028f9ab..16593450ec 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -8,12 +8,14 @@ plugins { repositories { mavenCentral() + maven { url = uri("https://jogamp.org/deployment/maven") } } sourceSets{ main{ java{ srcDirs("src") + exclude("**/*.jnilib") } resources{ srcDirs("src") @@ -76,3 +78,6 @@ tasks.withType { tasks.compileJava{ options.encoding = "UTF-8" } +tasks.javadoc{ + options.encoding = "UTF-8" +} diff --git a/core/src/processing/core/PApplet.java b/core/src/processing/core/PApplet.java index 4fccd1a535..d9df211eb7 100644 --- a/core/src/processing/core/PApplet.java +++ b/core/src/processing/core/PApplet.java @@ -705,7 +705,7 @@ public class PApplet implements PConstants { protected boolean exitCalled; // ok to be static because it's not possible to mix enabled/disabled - static protected boolean disableAWT; + static protected boolean disableAWT = System.getProperty("processing.awt.disable", "false").equals("true");; // messages to send if attached as an external vm @@ -9940,19 +9940,21 @@ static public void runSketch(final String[] args, System.exit(1); } - boolean external = false; - int[] location = null; - int[] editorLocation = null; + boolean external = System.getProperty("processing.external", "false").equals("true");; + int[] location = System.getProperty("processing.location", null) != null ? + parseInt(split(System.getProperty("processing.location"), ',')) : null; + int[] editorLocation = System.getProperty("processing.editor.location", null) != null ? + parseInt(split(System.getProperty("processing.editor.location"), ',')) : null; String name = null; int windowColor = 0; int stopColor = 0xff808080; - boolean hideStop = false; + boolean hideStop = System.getProperty("processing.stop.hide", "false").equals("true"); int displayNum = -1; // use default - boolean present = false; - boolean fullScreen = false; - float uiScale = 0; + boolean present = System.getProperty("processing.present", "false").equals("true"); + boolean fullScreen = System.getProperty("processing.fullscreen", "false").equals("true"); + float uiScale = parseInt(System.getProperty("processing.uiScale", "0"), 0); String param, value; String folder = calcSketchPath(); diff --git a/core/src/processing/core/PGraphics.java b/core/src/processing/core/PGraphics.java index 0b9f0d2ed4..1ada6aa2ae 100644 --- a/core/src/processing/core/PGraphics.java +++ b/core/src/processing/core/PGraphics.java @@ -4768,7 +4768,7 @@ public void text(char[] chars, int start, int stop, float x, float y) { } // int start = 0; - int index = 0; + int index = start; while (index < stop) { //length) { if (chars[index] == '\n') { textLineAlignImpl(chars, start, index, x, y); diff --git a/core/src/processing/core/PMatrix.java b/core/src/processing/core/PMatrix.java index edb1d260eb..df15dc4a40 100644 --- a/core/src/processing/core/PMatrix.java +++ b/core/src/processing/core/PMatrix.java @@ -205,4 +205,8 @@ public void preApply(float n00, float n01, float n02, float n03, * @return the determinant of the matrix */ public float determinant(); + /** + * Print the matrix data to the console. + */ + public void print(); } diff --git a/core/src/processing/core/PMatrix2D.java b/core/src/processing/core/PMatrix2D.java index c30a3504e3..bc6538f918 100644 --- a/core/src/processing/core/PMatrix2D.java +++ b/core/src/processing/core/PMatrix2D.java @@ -466,26 +466,15 @@ public float determinant() { ////////////////////////////////////////////////////////////// + @Override public void print() { - int big = (int) abs(max(PApplet.max(abs(m00), abs(m01), abs(m02)), - PApplet.max(abs(m10), abs(m11), abs(m12)))); - - int digits = 1; - if (Float.isNaN(big) || Float.isInfinite(big)) { // avoid infinite loop - digits = 5; - } else { - while ((big /= 10) != 0) digits++; // cheap log() - } - - System.out.println(PApplet.nfs(m00, digits, 4) + " " + - PApplet.nfs(m01, digits, 4) + " " + - PApplet.nfs(m02, digits, 4)); - - System.out.println(PApplet.nfs(m10, digits, 4) + " " + - PApplet.nfs(m11, digits, 4) + " " + - PApplet.nfs(m12, digits, 4)); + System.out.print(toString()); + } - System.out.println(); + @Override + public String toString() { + return PApplet.nfs(m00, 1, 4) + " " + PApplet.nfs(m01, 1, 4) + " " + PApplet.nfs(m02, 1, 4) + "\n" + + PApplet.nfs(m10, 1, 4) + " " + PApplet.nfs(m11, 1, 4) + " " + PApplet.nfs(m12, 1, 4) + "\n"; } diff --git a/core/src/processing/core/PMatrix3D.java b/core/src/processing/core/PMatrix3D.java index 831d9ad635..082d7fa8ca 100644 --- a/core/src/processing/core/PMatrix3D.java +++ b/core/src/processing/core/PMatrix3D.java @@ -809,52 +809,18 @@ protected boolean invApply(float n00, float n01, float n02, float n03, ////////////////////////////////////////////////////////////// + @Override public void print() { - /* - System.out.println(m00 + " " + m01 + " " + m02 + " " + m03 + "\n" + - m10 + " " + m11 + " " + m12 + " " + m13 + "\n" + - m20 + " " + m21 + " " + m22 + " " + m23 + "\n" + - m30 + " " + m31 + " " + m32 + " " + m33 + "\n"); - */ - int big = (int) Math.abs(max(max(max(max(abs(m00), abs(m01)), - max(abs(m02), abs(m03))), - max(max(abs(m10), abs(m11)), - max(abs(m12), abs(m13)))), - max(max(max(abs(m20), abs(m21)), - max(abs(m22), abs(m23))), - max(max(abs(m30), abs(m31)), - max(abs(m32), abs(m33)))))); - - int digits = 1; - if (Float.isNaN(big) || Float.isInfinite(big)) { // avoid infinite loop - digits = 5; - } else { - while ((big /= 10) != 0) digits++; // cheap log() - } - - System.out.println(PApplet.nfs(m00, digits, 4) + " " + - PApplet.nfs(m01, digits, 4) + " " + - PApplet.nfs(m02, digits, 4) + " " + - PApplet.nfs(m03, digits, 4)); - - System.out.println(PApplet.nfs(m10, digits, 4) + " " + - PApplet.nfs(m11, digits, 4) + " " + - PApplet.nfs(m12, digits, 4) + " " + - PApplet.nfs(m13, digits, 4)); - - System.out.println(PApplet.nfs(m20, digits, 4) + " " + - PApplet.nfs(m21, digits, 4) + " " + - PApplet.nfs(m22, digits, 4) + " " + - PApplet.nfs(m23, digits, 4)); - - System.out.println(PApplet.nfs(m30, digits, 4) + " " + - PApplet.nfs(m31, digits, 4) + " " + - PApplet.nfs(m32, digits, 4) + " " + - PApplet.nfs(m33, digits, 4)); - - System.out.println(); + System.out.print(toString()); } + @Override + public String toString() { + return PApplet.nfs(m00, 1, 4) + " " + PApplet.nfs(m01, 1, 4) + " " + PApplet.nfs(m02, 1, 4) + " " + PApplet.nfs(m03, 1, 4) + "\n" + + PApplet.nfs(m10, 1, 4) + " " + PApplet.nfs(m11, 1, 4) + " " + PApplet.nfs(m12, 1, 4) + " " + PApplet.nfs(m13, 1, 4) + "\n" + + PApplet.nfs(m20, 1, 4) + " " + PApplet.nfs(m21, 1, 4) + " " + PApplet.nfs(m22, 1, 4) + " " + PApplet.nfs(m23, 1, 4) + "\n" + + PApplet.nfs(m30, 1, 4) + " " + PApplet.nfs(m31, 1, 4) + " " + PApplet.nfs(m32, 1, 4) + " " + PApplet.nfs(m33, 1, 4) + "\n"; + } ////////////////////////////////////////////////////////////// diff --git a/core/src/processing/core/PShapeSVG.java b/core/src/processing/core/PShapeSVG.java index f8aa3400fb..7e00b9a05b 100644 --- a/core/src/processing/core/PShapeSVG.java +++ b/core/src/processing/core/PShapeSVG.java @@ -961,14 +961,47 @@ else if (lexState == LexState.EXP_HEAD) { float rx = PApplet.parseFloat(pathTokens[i + 1]); float ry = PApplet.parseFloat(pathTokens[i + 2]); float angle = PApplet.parseFloat(pathTokens[i + 3]); - boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0; - boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; - float endX = PApplet.parseFloat(pathTokens[i + 6]); - float endY = PApplet.parseFloat(pathTokens[i + 7]); + // In compact arc notation, flags and coordinates may be concatenated. + // e.g. "013" is parsed as large-arc=0, sweep=1, x=3 + String token4 = pathTokens[i + 4]; + boolean fa; + boolean fs; + float endX; + float endY; + int tokenOffset = 0; + if (isCompactArcNotation(token4)) { + fa = token4.charAt(0) == '1'; + fs = token4.charAt(1) == '1'; + // Case: flags and x-coordinate are concatenated (e.g. "01100") + // token4 contains flags + x, so y is at i+5. + // We consume 2 fewer tokens than standard (8-2=6). + if (token4.length() > 2) { + endX = PApplet.parseFloat(token4.substring(2)); + endY = PApplet.parseFloat(pathTokens[i + 5]); + tokenOffset = -2; + } else { + // Case: flags are concatenated but separated from x (e.g. "01 100") + // token4 is flags, x is at i+5, y is at i+6. + // We consume 1 fewer token than standard (8-1=7). + endX = PApplet.parseFloat(pathTokens[i + 5]); + endY = PApplet.parseFloat(pathTokens[i + 6]); + tokenOffset = -1; + } + } else { + // Standard notation: flags and coordinates are separate tokens. + // The 'A' command takes 7 arguments: + // rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y + // Here, we've already parsed rx (i+1), ry (i+2), and angle (i+3). + // token4 (i+4) is the large-arc-flag. + fa = PApplet.parseFloat(token4) != 0; + fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; // sweep-flag + endX = PApplet.parseFloat(pathTokens[i + 6]); // x + endY = PApplet.parseFloat(pathTokens[i + 7]); // y + } parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY); cx = endX; cy = endY; - i += 8; + i += 8 + tokenOffset; prevCurve = true; } break; @@ -978,14 +1011,41 @@ else if (lexState == LexState.EXP_HEAD) { float rx = PApplet.parseFloat(pathTokens[i + 1]); float ry = PApplet.parseFloat(pathTokens[i + 2]); float angle = PApplet.parseFloat(pathTokens[i + 3]); - boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0; - boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; - float endX = cx + PApplet.parseFloat(pathTokens[i + 6]); - float endY = cy + PApplet.parseFloat(pathTokens[i + 7]); + String token4 = pathTokens[i + 4]; + boolean fa; + boolean fs; + float endX; + float endY; + int tokenOffset = 0; + if (isCompactArcNotation(token4)) { + fa = token4.charAt(0) == '1'; + fs = token4.charAt(1) == '1'; + // Case: flags and x-coordinate are concatenated + if (token4.length() > 2) { + endX = cx + PApplet.parseFloat(token4.substring(2)); + endY = cy + PApplet.parseFloat(pathTokens[i + 5]); + tokenOffset = -2; + } else { + // Case: flags are concatenated but separated from x + endX = cx + PApplet.parseFloat(pathTokens[i + 5]); + endY = cy + PApplet.parseFloat(pathTokens[i + 6]); + tokenOffset = -1; + } + } else { + // Standard notation: flags and coordinates are separate tokens. + // The 'a' command takes 7 arguments: + // rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y + // Here, we've already parsed rx (i+1), ry (i+2), and angle (i+3). + // token4 (i+4) is the large-arc-flag. + fa = PApplet.parseFloat(token4) != 0; + fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; // sweep-flag + endX = cx + PApplet.parseFloat(pathTokens[i + 6]); // x + endY = cy + PApplet.parseFloat(pathTokens[i + 7]); // y + } parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY); cx = endX; cy = endY; - i += 8; + i += 8 + tokenOffset; prevCurve = true; } break; @@ -1054,6 +1114,33 @@ private void parsePathMoveto(float px, float py) { } + /** + * Checks if a token represents compact arc notation where flags and coordinates + * are concatenated (e.g., "013" for large-arc=0, sweep=1, x=3). + * + * @param token the token to check + * @return true if the token is in compact arc notation format + */ + private boolean isCompactArcNotation(String token) { + if (token == null) { + return false; + } + return token.length() > 1 && + // First two characters must be '0' or '1' (flags) + (token.charAt(0) == '0' || token.charAt(0) == '1') && + (token.charAt(1) == '0' || token.charAt(1) == '1') && + // Either it's just the flags (length 2), + (token.length() == 2 || + // Or the flags are followed by the start of a number coordinate + // (digit, sign, or decimal point) + (token.length() > 2 && ( + Character.isDigit(token.charAt(2)) || + token.charAt(2) == '+' || + token.charAt(2) == '-' || + token.charAt(2) == '.'))); + } + + private void parsePathLineto(float px, float py) { parsePathCode(VERTEX); parsePathVertex(px, py); diff --git a/core/src/processing/opengl/PSurfaceJOGL.java b/core/src/processing/opengl/PSurfaceJOGL.java index 064f9a8dd0..27ebd41d01 100644 --- a/core/src/processing/opengl/PSurfaceJOGL.java +++ b/core/src/processing/opengl/PSurfaceJOGL.java @@ -24,61 +24,42 @@ package processing.opengl; -import java.awt.Component; -import java.awt.EventQueue; -import java.awt.FileDialog; -import java.awt.GraphicsDevice; -import java.awt.GraphicsEnvironment; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.image.BufferedImage; -import java.awt.image.DataBufferInt; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -import javax.swing.ImageIcon; - import com.jogamp.common.util.IOUtil; import com.jogamp.common.util.IOUtil.ClassResources; +import com.jogamp.nativewindow.MutableGraphicsConfiguration; import com.jogamp.nativewindow.NativeSurface; import com.jogamp.nativewindow.ScalableSurface; +import com.jogamp.nativewindow.WindowClosingProtocol; import com.jogamp.nativewindow.util.Dimension; import com.jogamp.nativewindow.util.PixelFormat; import com.jogamp.nativewindow.util.PixelRectangle; -import com.jogamp.opengl.GLAutoDrawable; -import com.jogamp.opengl.GLCapabilities; -import com.jogamp.opengl.GLEventListener; -import com.jogamp.opengl.GLException; -import com.jogamp.opengl.GLProfile; -import com.jogamp.opengl.GLDrawableFactory; -import com.jogamp.nativewindow.MutableGraphicsConfiguration; -import com.jogamp.nativewindow.WindowClosingProtocol; import com.jogamp.newt.Display; import com.jogamp.newt.Display.PointerIcon; import com.jogamp.newt.NewtFactory; import com.jogamp.newt.Screen; import com.jogamp.newt.awt.NewtCanvasAWT; import com.jogamp.newt.opengl.GLWindow; +import com.jogamp.opengl.*; import com.jogamp.opengl.util.FPSAnimator; - -import processing.core.PApplet; -import processing.core.PConstants; -import processing.core.PGraphics; -import processing.core.PImage; -import processing.core.PSurface; +import processing.awt.PImageAWT; +import processing.awt.ShimAWT; +import processing.core.*; import processing.event.KeyEvent; import processing.event.MouseEvent; -import processing.awt.PImageAWT; -// have this removed by 4.0 final -import processing.awt.ShimAWT; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; public class PSurfaceJOGL implements PSurface { @@ -489,7 +470,7 @@ protected void initIcons() { sketch.getClass().getClassLoader(), sketch.getClass()); } - NewtFactory.setWindowIcons(res); + if (PApplet.platform == PConstants.WINDOWS) NewtFactory.setWindowIcons(res); } diff --git a/core/test/processing/core/PImageTest.java b/core/test/processing/core/PImageTest.java new file mode 100644 index 0000000000..ea5116e85f --- /dev/null +++ b/core/test/processing/core/PImageTest.java @@ -0,0 +1,317 @@ +package processing.core; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +public class PImageTest { + + private PImage img; + private PApplet applet; + + @Before + public void setUp() { + applet = new PApplet(); + img = new PImage(10, 10, PConstants.ARGB); + for (int i = 0; i < img.pixels.length; i++) { + img.pixels[i] = 0xFF000000 | (i % 255) << 16 | ((i * 3) % 255) << 8 | ((i * 7) % 255); + } + img.updatePixels(); + } + + @Test + public void testConstructors() { + PImage img1 = new PImage(); + assertEquals(PConstants.ARGB, img1.format); + + PImage img2 = new PImage(20, 30); + assertEquals(20, img2.width); + assertEquals(30, img2.height); + assertEquals(PConstants.RGB, img2.format); + + PImage img3 = new PImage(20, 30, PConstants.ALPHA); + assertEquals(PConstants.ALPHA, img3.format); + + PImage img4 = new PImage(20, 30, PConstants.RGB, 2); + assertEquals(2, img4.pixelDensity); + assertEquals(40, img4.pixelWidth); + assertEquals(60, img4.pixelHeight); + + PImage zeroImg = new PImage(0, 0); + assertEquals(0, zeroImg.width); + assertEquals(0, zeroImg.height); + assertEquals(0, zeroImg.pixels.length); + } + + @Test + public void testPixelManipulation() { + img.loadPixels(); + img.pixels[0] = 0xFFFF0000; + img.updatePixels(); + assertEquals(0xFFFF0000, img.get(0, 0)); + + assertEquals(0xFFFF0000, img.get(0, 0)); + assertEquals(0, img.get(-1, -1)); + assertEquals(0, img.get(100, 100)); + + img.set(1, 1, 0xFF00FF00); + assertEquals(0xFF00FF00, img.get(1, 1)); + + img.set(-1, -1, 0xFFFFFFFF); + img.set(100, 100, 0xFFFFFFFF); + + PImage region = img.get(0, 0, 2, 2); + assertEquals(2, region.width); + assertEquals(2, region.height); + assertEquals(0xFFFF0000, region.get(0, 0)); + assertEquals(0xFF00FF00, region.get(1, 1)); + + PImage copy = img.get(); + assertEquals(img.width, copy.width); + assertEquals(img.height, copy.height); + assertEquals(0xFFFF0000, copy.get(0, 0)); + assertEquals(0xFF00FF00, copy.get(1, 1)); + + PImage negCopy = img.get(-5, -5, 20, 20); + assertEquals(20, negCopy.width); + assertEquals(20, negCopy.height); + } + + @Test + public void testCopyAndResize() { + PImage copy = img.copy(); + assertEquals(img.width, copy.width); + assertEquals(img.height, copy.height); + assertEquals(img.get(0, 0), copy.get(0, 0)); + + PImage resized = img.copy(); + resized.resize(20, 0); + assertEquals(20, resized.width); + assertTrue(resized.height > 0); + + PImage resized2 = img.copy(); + resized2.resize(20, 15); + assertEquals(20, resized2.width); + assertEquals(15, resized2.height); + + img.set(0, 0, 0xFFFF0000); + img.set(1, 0, 0xFF00FF00); + img.set(0, 1, 0xFF0000FF); + img.set(1, 1, 0xFFFFFF00); + + PImage dest = new PImage(4, 4, PConstants.ARGB); + dest.copy(img, 0, 0, 2, 2, 0, 0, 4, 4); + + int topLeft = dest.get(0, 0); + int topRight = dest.get(3, 0); + int bottomLeft = dest.get(0, 3); + int bottomRight = dest.get(3, 3); + + assertTrue((topLeft & 0x00FF0000) > 0); + assertTrue((topRight & 0x0000FF00) > 0); + assertTrue((bottomLeft & 0x000000FF) > 0); + assertTrue((bottomRight & 0x00FFFF00) > 0); + + PImage smallImg = new PImage(5, 5, PConstants.ARGB); + smallImg.copy(img, 0, 0, 10, 10, 0, 0, 5, 5); + img.copy(smallImg, 0, 0, 5, 5, 0, 0, 10, 10); + } + + @Test + public void testMask() { + PImage mask = new PImage(10, 10, PConstants.ALPHA); + for (int i = 0; i < mask.pixels.length; i++) { + mask.pixels[i] = (i * 255) / mask.pixels.length; + } + mask.updatePixels(); + + PImage original = img.copy(); + img.mask(mask); + + assertTrue((img.get(0, 0) >>> 24) < 10); + + assertTrue((img.get(9, 9) >>> 24) > 240); + + img = original.copy(); + img.mask(mask.pixels); + + assertTrue((img.get(0, 0) >>> 24) < 10); + + assertTrue((img.get(9, 9) >>> 24) > 240); + + PImage smallMask = new PImage(5, 5); + try { + img.mask(smallMask.pixels); + fail("Should throw IllegalArgumentException for wrong size mask"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testFilter() { + for (int i = 0; i < img.pixels.length; i++) { + img.pixels[i] = 0xFF808080; + } + img.updatePixels(); + + PImage thresholdImg = img.copy(); + thresholdImg.filter(PConstants.THRESHOLD, 0.7f); + int thresholdColor = thresholdImg.get(0, 0); + assertTrue((thresholdColor & 0x00FFFFFF) < 0x00808080); + + thresholdImg = img.copy(); + thresholdImg.filter(PConstants.THRESHOLD, 0.3f); + thresholdColor = thresholdImg.get(0, 0); + assertTrue((thresholdColor & 0x00FFFFFF) > 0x00808080); + + PImage grayImg = img.copy(); + grayImg.filter(PConstants.GRAY); + int grayColor = grayImg.get(0, 0); + int r = (grayColor >> 16) & 0xFF; + int g = (grayColor >> 8) & 0xFF; + int b = grayColor & 0xFF; + assertEquals(r, g, 5); + assertEquals(g, b, 5); + + PImage invertImg = img.copy(); + invertImg.filter(PConstants.INVERT); + int originalColor = img.get(0, 0) & 0x00FFFFFF; + int invertedColor = invertImg.get(0, 0) & 0x00FFFFFF; + assertTrue(originalColor + invertedColor > 0x00FFFFFF - 10 && + originalColor + invertedColor < 0x00FFFFFF + 10); + + PImage posterizeImg = img.copy(); + posterizeImg.filter(PConstants.POSTERIZE, 2); + + PImage blurImg = img.copy(); + blurImg.filter(PConstants.BLUR, 1.0f); + + img.pixels[0] = 0x80808080; + img.updatePixels(); + PImage opaqueImg = img.copy(); + opaqueImg.filter(PConstants.OPAQUE); + assertTrue((opaqueImg.get(0, 0) >>> 24) > (img.get(0, 0) >>> 24)); + + PImage img2 = new PImage(10, 10, PConstants.RGB); + for (int y = 0; y < img2.height; y++) { + for (int x = 0; x < img2.width; x++) { + img2.pixels[y * img2.width + x] = (x == 5 || y == 5) ? 0xFFFFFFFF : 0xFF000000; + } + } + img2.updatePixels(); + + PImage erodeImg = img2.copy(); + erodeImg.filter(PConstants.ERODE); + + PImage dilateImg = img2.copy(); + dilateImg.filter(PConstants.DILATE); + + int blackPixelsInOriginal = 0; + int blackPixelsInDilated = 0; + for (int i = 0; i < img2.pixels.length; i++) { + if ((img2.pixels[i] & 0x00FFFFFF) == 0) blackPixelsInOriginal++; + if ((dilateImg.pixels[i] & 0x00FFFFFF) == 0) blackPixelsInDilated++; + } + assertTrue(blackPixelsInDilated < blackPixelsInOriginal); + } + + @Test + public void testAllBlendModesExactMatchStaticHelper() { + final int W = 10, H = 10; + final int red = 0x80FF0000; + final int blue = 0x400000FF; + + PImage img1 = new PImage(W, H, PConstants.ARGB); + PImage img2 = new PImage(W, H, PConstants.ARGB); + Arrays.fill(img1.pixels, red); + Arrays.fill(img2.pixels, blue); + img1.updatePixels(); + img2.updatePixels(); + + int[] modes = { + PConstants.BLEND, PConstants.ADD, PConstants.SUBTRACT, PConstants.LIGHTEST, + PConstants.DARKEST, PConstants.DIFFERENCE, PConstants.EXCLUSION, + PConstants.MULTIPLY, PConstants.SCREEN, PConstants.REPLACE + }; + + for (int mode : modes) { + PImage out = img1.copy(); + out.blend(img2, 0,0,W,H, 0,0,W,H, mode); + out.loadPixels(); + + int[] expected = new int[W*H]; + for (int i = 0; i < expected.length; i++) { + expected[i] = (mode == PConstants.REPLACE) + ? img2.pixels[i] + : PImage.blendColor(img1.pixels[i], img2.pixels[i], mode); + } + + for (int i = 0; i < expected.length; i++) { + assertEquals( + String.format("Mode %d failed at pixel %d: got 0x%08X, expected 0x%08X", + mode, i, out.pixels[i], expected[i]), + expected[i], out.pixels[i] + ); + } + } + } + + + @Test + public void testSaveAndLoad_pngRoundTrip() throws IOException { + PImage out = new PImage(10, 10, PConstants.ARGB); + for (int y = 0; y < out.height; y++) { + for (int x = 0; x < out.width; x++) { + out.pixels[y*out.width + x] = + ((x + y) % 2 == 0) + ? 0xFFFFFFFF + : 0xFF000000; + } + } + out.updatePixels(); + out.parent = applet; + + File f = File.createTempFile("test", ".png"); + f.deleteOnExit(); + assertTrue(out.save(f.getAbsolutePath())); + + PImage in = applet.loadImage(f.getAbsolutePath()); + assertNotNull(in); + assertEquals(out.width, in.width); + assertEquals(out.height, in.height); + + in.loadPixels(); + for (int i = 0; i < out.pixels.length; i++) { + assertEquals( + String.format( + "Pixel %d mismatch: saved=0x%08X loaded=0x%08X", + i, out.pixels[i], in.pixels[i] + ), + out.pixels[i], + in.pixels[i] + ); + } + } + + + @Test + public void testCheckAlpha() { + PImage opaqueImg = new PImage(5, 5, PConstants.RGB); + for (int i = 0; i < opaqueImg.pixels.length; i++) { + opaqueImg.pixels[i] = 0xFFFFFFFF; + } + opaqueImg.checkAlpha(); + assertEquals(PConstants.RGB, opaqueImg.format); + + PImage transImg = new PImage(5, 5, PConstants.RGB); + for (int i = 0; i < transImg.pixels.length; i++) { + transImg.pixels[i] = 0x80FFFFFF; + } + transImg.checkAlpha(); + assertEquals(PConstants.ARGB, transImg.format); + } + +} \ No newline at end of file diff --git a/core/test/processing/core/PMatrix3DTest.java b/core/test/processing/core/PMatrix3DTest.java new file mode 100644 index 0000000000..7cb03a63af --- /dev/null +++ b/core/test/processing/core/PMatrix3DTest.java @@ -0,0 +1,226 @@ +package processing.core; + +import static org.junit.Assert.*; +import org.junit.Test; + +public class PMatrix3DTest { + + @Test + public void testConstructorsResetAndGet() { + PMatrix3D m = new PMatrix3D(); + float[] vals = m.get(null); + assertEquals(1, vals[0], 1e-6); + assertEquals(1, vals[5], 1e-6); + assertEquals(1, vals[10], 1e-6); + assertEquals(1, vals[15], 1e-6); + + m.m00 = 2; + m.reset(); + vals = m.get(null); + assertEquals(1, vals[0], 1e-6); + } + + @Test + public void testSetAndGetMethods() { + PMatrix3D m = new PMatrix3D(); + float[] source = { + 2, 3, 4, 5, + 6, 7, 8, 9, + 10,11,12,13, + 14,15,16,17 + }; + m.set(source); + float[] target = m.get(null); + for (int i = 0; i < 16; i++) { + assertEquals(source[i], target[i], 1e-6); + } + + m.set(1, 2, 3, 4, 5, 6); + target = m.get(null); + assertEquals(1, target[0], 1e-6); + assertEquals(2, target[1], 1e-6); + assertEquals(0, target[2], 1e-6); + assertEquals(3, target[3], 1e-6); + + PMatrix2D m2d = new PMatrix2D(1, 2, 3, 4, 5, 6); + m.set(m2d); + target = m.get(null); + assertEquals(1, target[0], 1e-6); + assertEquals(2, target[1], 1e-6); + assertEquals(0, target[2], 1e-6); + assertEquals(3, target[3], 1e-6); + } + + @Test + public void testTranslate() { + PMatrix3D m = new PMatrix3D(); + m.translate(0, 0, 0); + float[] original = new PMatrix3D().get(null); + float[] result = m.get(null); + for (int i = 0; i < 16; i++) { + assertEquals(original[i], result[i], 1e-6); + } + + m.reset(); + m.translate(10, -5, 3); + result = m.get(null); + assertEquals(10, result[3], 1e-6); + assertEquals(-5, result[7], 1e-6); + assertEquals(3, result[11], 1e-6); + assertEquals(1, result[15], 1e-6); + } + + @Test + public void testRotateAndShear() { + PMatrix3D m = new PMatrix3D(); + m.reset(); + m.rotate(0); + float[] original = new PMatrix3D().get(null); + float[] result = m.get(null); + for (int i = 0; i < 16; i++) { + assertEquals(original[i], result[i], 1e-6); + } + + m.reset(); + m.rotateZ((float) Math.PI / 2); + result = m.get(null); + assertEquals(0, result[0], 1e-6); + assertEquals(-1, result[1], 1e-6); + assertEquals(1, result[4], 1e-6); + assertEquals(0, result[5], 1e-6); + + m.reset(); + m.shearX((float) Math.PI / 4); + result = m.get(null); + float expectedT = (float) Math.tan(Math.PI / 4); + assertEquals(expectedT, result[1], 1e-6); + + m.reset(); + m.shearY((float) Math.PI / 4); + result = m.get(null); + assertEquals(expectedT, result[4], 1e-6); + } + + @Test + public void testScale() { + PMatrix3D m = new PMatrix3D(); + m.reset(); + m.scale(1); + float[] original = new PMatrix3D().get(null); + float[] result = m.get(null); + for (int i = 0; i < 16; i++) { + assertEquals(original[i], result[i], 1e-6); + } + + m.reset(); + m.scale(2, 3, 4); + result = m.get(null); + assertEquals(2, result[0], 1e-6); + assertEquals(3, result[5], 1e-6); + assertEquals(4, result[10], 1e-6); + + m.reset(); + m.scale(0, 1, 1); + result = m.get(null); + assertEquals(0, result[0], 1e-6); + } + + @Test + public void testApplyAndPreApply() { + PMatrix3D m = new PMatrix3D(); + m.reset(); + PMatrix3D n = new PMatrix3D( + 2, 3, 4, 5, + 6, 7, 8, 9, + 10,11,12,13, + 14,15,16,17 + ); + m.apply(n); + float[] result = m.get(null); + float[] expected = n.get(null); + for (int i = 0; i < 16; i++) { + assertEquals(expected[i], result[i], 1e-6); + } + + m.reset(); + m.preApply(n); + result = m.get(null); + for (int i = 0; i < 16; i++) { + assertEquals(expected[i], result[i], 1e-6); + } + } + + @Test + public void testMultMethods() { + PMatrix3D m = new PMatrix3D(); + m.reset(); + PVector p = new PVector(1, 2, 3); + PVector resultP = m.mult(p, null); + assertEquals(1, resultP.x, 1e-6); + assertEquals(2, resultP.y, 1e-6); + assertEquals(3, resultP.z, 1e-6); + + float[] vec3 = {1, 2, 3}; + float[] out3 = m.mult(vec3, new float[3]); + assertEquals(1, out3[0], 1e-6); + assertEquals(2, out3[1], 1e-6); + assertEquals(3, out3[2], 1e-6); + + float[] vec4 = {1, 2, 3, 4}; + float[] out4 = m.mult(vec4, new float[4]); + for (int i = 0; i < 4; i++) { + assertEquals(vec4[i], out4[i], 1e-6); + } + + try { + m.mult(vec3, vec3); + fail("Expected RuntimeException for identical source and target arrays."); + } catch (RuntimeException e) { + // Exception expected. + } + + m.reset(); + m.translate(10, 20, 30); + float x = m.multX(1, 2, 3); + float y = m.multY(1, 2, 3); + float z = m.multZ(1, 2, 3); + float w = m.multW(1, 2, 3); + assertEquals(1 + 10, x, 1e-6); + assertEquals(2 + 20, y, 1e-6); + assertEquals(3 + 30, z, 1e-6); + assertEquals(1, w, 1e-6); + } + + @Test + public void testTransposeAndDeterminant() { + PMatrix3D m = new PMatrix3D( + 2, 3, 4, 5, + 6, 7, 8, 9, + 10,11,12,13, + 14,15,16,17 + ); + float det = m.determinant(); + m.transpose(); + float det2 = m.determinant(); + assertEquals(det, det2, 1e-6); + } + + @Test + public void testInvert() { + PMatrix3D m = new PMatrix3D(); + m.reset(); + boolean inverted = m.invert(); + assertTrue(inverted); + float[] result = m.get(null); + PMatrix3D identity = new PMatrix3D(); + float[] idArr = identity.get(null); + for (int i = 0; i < 16; i++) { + assertEquals(idArr[i], result[i], 1e-6); + } + + m.reset(); + m.scale(0, 1, 1); + inverted = m.invert(); + assertFalse(inverted); + } +} diff --git a/core/test/processing/core/PShapeSVGPathTest.java b/core/test/processing/core/PShapeSVGPathTest.java new file mode 100644 index 0000000000..298fb085de --- /dev/null +++ b/core/test/processing/core/PShapeSVGPathTest.java @@ -0,0 +1,110 @@ +package processing.core; + +import org.junit.Assert; +import org.junit.Test; +import processing.data.XML; + +public class PShapeSVGPathTest { + + @Test + public void testCompactPathNotation() { + String svgContent = "" + + "" + + ""; + + try { + XML xml = XML.parse(svgContent); + PShapeSVG shape = new PShapeSVG(xml); + Assert.assertNotNull(shape); + Assert.assertTrue(shape.getChildCount() > 0); + + PShape path = shape.getChild(0); + Assert.assertNotNull(path); + Assert.assertTrue(path.getVertexCount() > 5); + } catch (Exception e) { + Assert.fail("Encountered exception " + e); + } + } + + @Test + public void testWorkingPathNotation() { + // Test the working SVG (with explicit decimal points) + String svgContent = "" + + "" + + ""; + + try { + XML xml = XML.parse(svgContent); + PShapeSVG shape = new PShapeSVG(xml); + Assert.assertNotNull(shape); + } catch (Exception e) { + Assert.fail("Encountered exception " + e); + } + } + + @Test + public void testCompactArcNotationVariations() { + String svgContent1 = "" + + ""; + + try { + XML xml = XML.parse(svgContent1); + PShapeSVG shape = new PShapeSVG(xml); + PShape path = shape.getChild(0); + int vertexCount = path.getVertexCount(); + PVector lastVertex = path.getVertex(vertexCount - 1); + Assert.assertEquals(3.0f, lastVertex.x, 0.0001f); + Assert.assertEquals(50.0f, lastVertex.y, 0.0001f); + } catch (Exception e) { + Assert.fail("Encountered exception " + e); + } + + String svgContent2 = "" + + ""; + + try { + XML xml = XML.parse(svgContent2); + PShapeSVG shape = new PShapeSVG(xml); + PShape path = shape.getChild(0); + int vertexCount = path.getVertexCount(); + PVector lastVertex = path.getVertex(vertexCount - 1); + Assert.assertEquals(10.0f, lastVertex.x, 0.0001f); + Assert.assertEquals(50.0f, lastVertex.y, 0.0001f); + } catch (Exception e) { + Assert.fail("Encountered exception " + e); + } + + String svgContent3 = "" + + ""; + + try { + XML xml = XML.parse(svgContent3); + PShapeSVG shape = new PShapeSVG(xml); + PShape path = shape.getChild(0); + int vertexCount = path.getVertexCount(); + PVector lastVertex = path.getVertex(vertexCount - 1); + Assert.assertEquals(10.0f, lastVertex.x, 0.0001f); + Assert.assertEquals(50.0f, lastVertex.y, 0.0001f); + } catch (Exception e) { + Assert.fail("Encountered exception " + e); + } + } + + @Test + public void testCompactArcWithNegativeCoordinates() { + String svgContent = "" + + ""; + + try { + XML xml = XML.parse(svgContent); + PShapeSVG shape = new PShapeSVG(xml); + PShape path = shape.getChild(0); + int vertexCount = path.getVertexCount(); + PVector lastVertex = path.getVertex(vertexCount - 1); + Assert.assertEquals(40.0f, lastVertex.x, 0.0001f); + Assert.assertEquals(70.0f, lastVertex.y, 0.0001f); + } catch (Exception e) { + Assert.fail("Encountered exception " + e); + } + } +} diff --git a/core/test/processing/data/IntListTest.java b/core/test/processing/data/IntListTest.java new file mode 100644 index 0000000000..3c422d84e4 --- /dev/null +++ b/core/test/processing/data/IntListTest.java @@ -0,0 +1,492 @@ +package processing.data; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * IntList.java has two fields: + * 1. count - the number of elements currently stored in the list with an initial value of 0. + * 2. int[] data - An array to store elements with an initial capacity of 10. + */ +public class IntListTest { + @Test + public void testDefaultConstructor() { + IntList testedList = new IntList(); + assertEquals(0, testedList.size()); + assertEquals(10, testedList.data.length); + } + + @Test + public void testConstructorWithLength() { + IntList testedList = new IntList(20); + assertEquals(0, testedList.size()); + assertEquals(20, testedList.data.length); + } + + @Test + public void testConstructorWithArray() { + int[] source = {1, 2}; + IntList testedList = new IntList(source); + assertEquals(2, testedList.size()); + assertEquals(2, testedList.data.length); + + assertEquals(1, testedList.get(0)); + assertEquals(2, testedList.get(1)); + } + + @Test + public void testConstructorWithIterableObject() { + List source = new ArrayList<>(Arrays.asList(1, "2", null, 4.5, -1)); + IntList testedList = new IntList(source); + assertEquals(5, testedList.size()); + + int[] expected = {1, 2, 0, 4, -1}; + assertArrayEquals(expected, testedList.values()); + } + @Test + public void testConstructorWithObject() { + String eleStr = "Hello"; + int eleInt = 10; + float eleFloat = 1.2f; + Object eleObj = new Object(); + + IntList testedList = new IntList(eleStr, eleInt, eleFloat, eleObj); + + int[] expected = {0, 10, 1, 0}; + assertArrayEquals(expected, testedList.values()); + } + @Test + public void testFromRangeWithStopIndex() { + IntList originalList = new IntList(new int[]{5,10,15,20,25}); + IntList result = originalList.fromRange(2); + assertArrayEquals(new int[]{0,1}, result.values()); + } + @Test + public void testFromRangeWithStartAndStopIndex() { + IntList originalList = new IntList(new int[]{5,10,15,20,25}); + IntList result = originalList.fromRange(1,3); + assertArrayEquals(new int[]{1,2}, result.values()); + } + @Test + public void testClear() { + IntList testedList = new IntList(new int[]{1, 2, 3}); + testedList.clear(); + assertEquals(0, testedList.size()); + } + @Test + public void testResize() { + IntList testedList = new IntList(new int[]{1, 2, 3}); + testedList.resize(5); + assertEquals(5, testedList.size()); + assertEquals(5, testedList.data.length); + } + @Test + public void testSet() { + IntList testedList = new IntList(); + testedList.set(0, 20); + assertEquals(1, testedList.size()); + assertEquals(20, testedList.get(0)); + + testedList.set(100, 2000); + assertEquals(101, testedList.size()); + assertEquals(2000, testedList.get(100)); + } + @Test + public void testPush() { + IntList testedList = new IntList(); + testedList.push(100); + assertEquals(1, testedList.size()); + assertEquals(100, testedList.get(0)); + } + @Test + public void testAppendWithInt() { + IntList testedList = new IntList(); + testedList.append(100); + assertEquals(1, testedList.size()); + assertEquals(100, testedList.get(0)); + } + @Test + public void testAppendWithIntArray() { + IntList testedList = new IntList(); + int[] source = {10, 20, 30}; + testedList.append(source); + assertArrayEquals(source, testedList.values()); + } + @Test + public void testAppendWithIntList() { + IntList testedList = new IntList(); + IntList source = new IntList(new int[]{10, 20, 30}); + testedList.append(source); + assertArrayEquals(source.values(), testedList.values()); + } + @Test + public void testAppendUnique() { + IntList testedList = new IntList(new int[]{10, 20, 30}); + testedList.appendUnique(100); + assertArrayEquals(new int[]{10, 20, 30, 100}, testedList.values()); + } + + @Test + public void testPop() { + IntList testedList = new IntList(new int[]{10, 20}); + + assertEquals(20,testedList.pop()); + assertEquals(1,testedList.size()); + assertEquals(10,testedList.pop()); + assertEquals(0,testedList.size()); + } + + @Test + public void testPopOnEmptyIntListThrowsException() { + IntList testedList = new IntList(); + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + testedList.pop(); + }); + + assertEquals("Can't call pop() on an empty list", exception.getMessage()); + } + + @Test + public void testRemoveWithIndexGreaterThanSize() { + IntList testedList = new IntList(); + assertThrows(ArrayIndexOutOfBoundsException.class, + () -> testedList.remove(3)); + } + + @Test + public void testRemoveWithIndexLessThanZeroThrowsException() { + IntList testedList = new IntList(); + assertThrows(ArrayIndexOutOfBoundsException.class, + () -> testedList.remove(-1)); + } + + @Test + public void testRemoveWithIndex() { + IntList testedList = new IntList(new int[]{1, 2, 3}); + assertEquals(2,testedList.remove(1)); + assertEquals(2,testedList.size()); + assertArrayEquals(new int[]{1, 3},testedList.values()); + } + @Test + public void testRemoveValue() { + IntList testedList = new IntList(new int[]{10, 20, 20}); + assertEquals(1,testedList.removeValue(20)); + assertEquals(-1,testedList.removeValue(100)); + } + @Test + public void testRemoveValues() { + IntList testedList = new IntList(new int[]{10, 20, 20}); + assertEquals(2,testedList.removeValues(20)); + } + + @Test + public void testInsertWithInvalidIndexThrowsException() { + IntList testedList = new IntList(); + + ArrayIndexOutOfBoundsException exception = assertThrows(ArrayIndexOutOfBoundsException.class, + () -> testedList.insert(1, 20)); + + IllegalArgumentException negativeIndex = assertThrows(IllegalArgumentException.class, + () -> testedList.insert(-1, 20)); + assertEquals("insert() index cannot be negative: it was -1", negativeIndex.getMessage()); + } + + @Test + public void testInsertWithValidIndex() { + IntList testedList = new IntList(); + testedList.insert(0, 10); + assertEquals(10, testedList.get(0)); + } + + @Test + public void testInsertWithArray() { + IntList testedList = new IntList(new int[]{5, 5, 5}); + int[] source = {100, 200}; + testedList.insert(1, source); + int[] expectedList = new int[]{5, 100, 200, 5, 5}; + assertArrayEquals(expectedList, testedList.values()); + } + + @Test + public void testInsertWithIntList() { + IntList testedList = new IntList(new int[]{5, 5, 5}); + IntList source = new IntList(new int[]{100, 200}); + testedList.insert(1, source); + int[] expectedList = new int[]{5, 100, 200, 5, 5}; + assertArrayEquals(expectedList, testedList.values()); + } + + @Test + public void testIndex() { + IntList testedList = new IntList(new int[]{5, 5, 5}); + assertEquals(0,testedList.index(5)); + assertEquals(-1,testedList.index(10)); + } + + @Test + public void testHasValue() { + IntList testedList = new IntList(new int[]{1, 2, 3}); + assertTrue(testedList.hasValue(3)); + assertFalse(testedList.hasValue(100)); + } + + @Test + public void testIncrementWithIndex() { + IntList testedList = new IntList(new int[] {20}); + testedList.increment(0); + assertEquals(21,testedList.get(0)); + } + @Test + public void testIncrementWithIndexGreaterThanSize() { + IntList testedList = new IntList(new int[] {20}); + testedList.increment(5); + assertEquals(1,testedList.get(5)); + } + @Test + public void testAddWithValidIndex() { + IntList testedList = new IntList(new int[] {20}); + testedList.add(0, 20); + assertEquals(40,testedList.get(0)); + } + + @Test + public void testAddWithInvalidIndexThrowsException() { + IntList testedList = new IntList(new int[] {20}); + ArrayIndexOutOfBoundsException exception = assertThrows(ArrayIndexOutOfBoundsException.class, + ()->testedList.add(5, 20)); + + assertEquals("The list size is 1. You cannot add() to element 5.",exception.getMessage()); + } + @Test + public void testSubWithValidIndex() { + IntList testedList = new IntList(new int[] {20}); + testedList.sub(0, 20); + assertEquals(0,testedList.get(0)); + } + + @Test + public void testSubWithInvalidIndexThrowsException() { + IntList testedList = new IntList(new int[] {20}); + ArrayIndexOutOfBoundsException exception = assertThrows(ArrayIndexOutOfBoundsException.class, + ()->testedList.sub(5, 20)); + + assertEquals("The list size is 1. You cannot sub() to element 5.",exception.getMessage()); + } + @Test + public void testMultWithValidIndex() { + IntList testedList = new IntList(new int[] {20}); + testedList.mult(0, 20); + assertEquals(400,testedList.get(0)); + } + + @Test + public void testMultWithInvalidIndexThrowsException() { + IntList testedList = new IntList(new int[] {20}); + ArrayIndexOutOfBoundsException exception = assertThrows(ArrayIndexOutOfBoundsException.class, + ()->testedList.mult(5, 20)); + + assertEquals("The list size is 1. You cannot mult() to element 5.",exception.getMessage()); + } + @Test + public void testDivWithValidIndex() { + IntList testedList = new IntList(new int[] {20}); + testedList.div(0, 20); + assertEquals(1,testedList.get(0)); + } + + @Test + public void testDivWithInvalidIndexThrowsException() { + IntList testedList = new IntList(new int[] {20}); + ArrayIndexOutOfBoundsException exception = assertThrows(ArrayIndexOutOfBoundsException.class, + ()->testedList.div(5, 20)); + + assertEquals("The list size is 1. You cannot div() to element 5.",exception.getMessage()); + } + @Test + public void testMinWithValidIndex() { + IntList testedList = new IntList(new int[] {20, 10, -5}); + assertEquals(-5, testedList.min()); + } + + @Test + public void testMinWithInvalidIndexThrowsException() { + IntList testedList = new IntList(); + RuntimeException exception = assertThrows(RuntimeException.class, + ()->testedList.min()); + + assertEquals("Cannot use min() on an empty IntList.",exception.getMessage()); + } + @Test + public void testMinIndexWithValidIndex() { + IntList testedList = new IntList(new int[] {20, 10, -5}); + assertEquals(2, testedList.minIndex()); + } + + @Test + public void testMinIndexWithInvalidIndexThrowsException() { + IntList testedList = new IntList(); + RuntimeException exception = assertThrows(RuntimeException.class, + ()->testedList.minIndex()); + + assertEquals("Cannot use minIndex() on an empty IntList.",exception.getMessage()); + } + + @Test + public void testMaxWithValidIndex() { + IntList testedList = new IntList(new int[] {20, 10, -5}); + assertEquals(20, testedList.max()); + } + + @Test + public void testMaxWithInvalidIndexThrowsException() { + IntList testedList = new IntList(); + RuntimeException exception = assertThrows(RuntimeException.class, + ()->testedList.max()); + + assertEquals("Cannot use max() on an empty IntList.",exception.getMessage()); + } + + @Test + public void testMaxIndexWithValidIndex() { + IntList testedList = new IntList(new int[] {20, 10, -5}); + assertEquals(0, testedList.maxIndex()); + } + + @Test + public void testMaxIndexWithInvalidIndexThrowsException() { + IntList testedList = new IntList(); + RuntimeException exception = assertThrows(RuntimeException.class, + ()->testedList.maxIndex()); + + assertEquals("Cannot use maxIndex() on an empty IntList.",exception.getMessage()); + } + @Test + public void testSumLong() { + IntList testedList = new IntList(new int[] {20, 10, -5}); + assertEquals(25, testedList.sumLong()); + } + @Test + public void testSumWithValidIntegerValue() { + IntList testedList = new IntList(new int[] {20, 10, -5}); + assertEquals(25, testedList.sum()); + } + @Test + public void testSumGreaterThanMaxIntegerValueThrowsException() { + int value = Integer.MAX_VALUE; + IntList testedList = new IntList(new int[] {value, 1}); + RuntimeException exception = assertThrows(RuntimeException.class, + ()->testedList.sum()); + + assertEquals("sum() exceeds 2147483647, use sumLong()",exception.getMessage()); + } + @Test + public void testSumLessThanMinIntegerValueThrowsException() { + int value = Integer.MIN_VALUE; + IntList testedList = new IntList(new int[] {value, -1}); + RuntimeException exception = assertThrows(RuntimeException.class, + ()->testedList.sum()); + + assertEquals("sum() less than -2147483648, use sumLong()",exception.getMessage()); + } + @Test + public void testSort() { + IntList testedList = new IntList(new int[] {20, 10, -5}); + testedList.sort(); + assertArrayEquals(new int[]{-5,10,20}, testedList.values()); + } + @Test + public void testSortReverse() { + IntList testedList = new IntList(new int[] {20, 10, 100}); + testedList.sortReverse(); + assertArrayEquals(new int[]{100,20,10}, testedList.values()); + } + @Test + public void testReverse() { + IntList testedList = new IntList(new int[] {20, 10, 100}); + testedList.reverse(); + assertArrayEquals(new int[]{100,10,20}, testedList.values()); + } + @Test + public void testChoice() { + IntList testedList = new IntList(new int[] {20, 10, 100}); + int num = testedList.choice(); + assertTrue(testedList.hasValue(num)); + } + @Test + public void testChoiceOnEmptyIntListThrowsException() { + IntList testedList = new IntList(); + ArrayIndexOutOfBoundsException exception = assertThrows(ArrayIndexOutOfBoundsException.class, + ()->testedList.choice()); + assertEquals("No entries in this IntList", exception.getMessage()); + } + @Test + public void testRemoveChoice() { + IntList testedList = new IntList(new int[] {20, 10, 100}); + int num = testedList.removeChoice(); + assertEquals(2, testedList.size()); + } + + @Test + public void testRemoveChoiceOnEmptyIntListThrowsException() { + IntList testedList = new IntList(); + ArrayIndexOutOfBoundsException exception = assertThrows(ArrayIndexOutOfBoundsException.class, + ()->testedList.removeChoice()); + assertEquals("No entries in this IntList", exception.getMessage()); + } + @Test + public void testCopy() { + IntList originalList = new IntList(new int[] {20, 10, 100}); + IntList copyList = originalList.copy(); + assertArrayEquals(originalList.values(), copyList.values()); + assertEquals(originalList.size(), copyList.size()); + } + @Test + public void testToArray() { + IntList originalList = new IntList(new int[] {20, 10, 100}); + int[] result = originalList.toArray(); + assertArrayEquals(new int[] {20, 10, 100}, result); + } + @Test + public void testToArrayWithDestinationArray() { + IntList originalList = new IntList(new int[] {20, 10, 100}); + int[] oversize = new int[100]; + int[] result = originalList.toArray(oversize); + assertArrayEquals(new int[] {20, 10, 100}, result); + assertEquals(3, result.length); + } + @Test + public void testToArrayWithDestinationArrayNull() { + IntList originalList = new IntList(new int[] {20, 10, 100}); + int[] initialIsNull = null; + int[] result = originalList.toArray(initialIsNull); + assertArrayEquals(new int[] {20, 10, 100}, result); + assertEquals(3, result.length); + } + @Test + public void testGetPercent() { + IntList originalList = new IntList(new int[] {5, 10, 5}); + FloatList result = originalList.getPercent(); + assertArrayEquals(new float[] {0.25f, 0.5f, 0.25f}, result.values(), 1e-6f); + } + @Test + public void testGetSubset() { + IntList originalList = new IntList(new int[] {5, 10, 20}); + IntList result = originalList.getSubset(1); + assertArrayEquals(new int[]{10, 20}, result.values()); + } + @Test + public void testJoin() { + IntList originalList = new IntList(new int[] {5, 10}); + assertEquals("5&10", originalList.join("&")); + } + @Test + public void testJoinWithEmptyList() { + IntList originalList = new IntList(); + assertEquals("", originalList.join("&")); + } +} diff --git a/gradle/plugins/library/build.gradle.kts b/gradle/plugins/library/build.gradle.kts new file mode 100644 index 0000000000..f70338b4ec --- /dev/null +++ b/gradle/plugins/library/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("com.gradle.plugin-publish") version "2.0.0" + kotlin("jvm") version "2.2.20" +} + +gradlePlugin { + website = "https://processing.org/" + vcsUrl = "https://github.com/processing/processing4" + plugins { + create("processing.library") { + id = project.properties.getOrElse("publishingGroup", { "org.processing" }).toString() + ".library" + displayName = "Processing Library Plugin" + description = "Gradle plugin for building Processing libraries" + tags = listOf("processing", "library", "dsl") + implementationClass = "ProcessingLibraryPlugin" + } + } +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} +kotlin { + jvmToolchain(17) +} \ No newline at end of file diff --git a/gradle/plugins/library/src/main/kotlin/BundleLibraryFilesTask.kt b/gradle/plugins/library/src/main/kotlin/BundleLibraryFilesTask.kt new file mode 100644 index 0000000000..1d5ab57eed --- /dev/null +++ b/gradle/plugins/library/src/main/kotlin/BundleLibraryFilesTask.kt @@ -0,0 +1,77 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.bundling.Jar +import org.gradle.api.tasks.javadoc.Javadoc + +abstract class BundleLibraryFilesTask : DefaultTask() { + @Input + var configuration: ProcessingLibraryConfiguration? = null + + @OutputDirectory + val outputDir = project.objects.directoryProperty() + + init { + outputDir.convention(project.layout.buildDirectory.dir("library")) + } + + @TaskAction + fun bundle() { + val configuration = configuration + ?: throw GradleException("Processing library configuration must be provided.") + val libraryName = configuration.name ?: project.name + + val buildDir = project.layout.buildDirectory.dir("library/$libraryName").get().asFile + buildDir.mkdirs() + + val libDir = buildDir.resolve("library") + libDir.mkdirs() + + // Copy the jar file + val jarFile = project.tasks.named("jar", Jar::class.java).get().archiveFile.get().asFile + jarFile.copyTo(libDir.resolve("$libraryName.jar"), overwrite = true) + + // Copy all runtime dependencies + val runtimeClasspath = project.configurations.getByName("runtimeClasspath") + runtimeClasspath.resolvedConfiguration.resolvedArtifacts.forEach { artifact -> + val depFile = artifact.file + depFile.copyTo(libDir.resolve(depFile.name), overwrite = true) + } + + // Copy Examples folder + val examplesDir = project.projectDir.resolve("examples") + if (!examplesDir.exists() || !examplesDir.isDirectory) { + throw GradleException("Examples folder not found in project directory.") + } + examplesDir.copyRecursively(buildDir.resolve("examples"), overwrite = true) + + // Copy javadoc to reference folder + val docsDir = project.tasks.named("javadoc", Javadoc::class.java).get().destinationDir + docsDir?.copyRecursively(buildDir.resolve("reference"), overwrite = true) + + // Create library.properties file + val propertiesFile = buildDir.resolve("library.properties") + propertiesFile.bufferedWriter().use { writer -> + val properties = mapOf( + "name" to libraryName, + "version" to (configuration.version ?: "1.0.0"), + "prettyVersion" to (configuration.prettyVersion ?: configuration.version ?: "1.0.0"), + "authors" to (configuration.authors.entries.joinToString(", ") { "[${it.key}](${it.value})" }), + "url" to configuration.url, + "category" to configuration.categories.joinToString(", "), + "sentence" to configuration.sentence, + "paragraph" to configuration.paragraph, + "minRevision" to configuration.minRevision, + "maxRevision" to configuration.maxRevision + ) + properties + .filter { it.value != null && it.value.toString().isNotEmpty() } + .forEach { (key, value) -> + writer.write("$key=$value\n") + } + } + propertiesFile.copyTo(buildDir.resolve("../$libraryName.txt"), overwrite = true) + } +} \ No newline at end of file diff --git a/gradle/plugins/library/src/main/kotlin/ProcessingLibraryExtension.kt b/gradle/plugins/library/src/main/kotlin/ProcessingLibraryExtension.kt new file mode 100644 index 0000000000..f03b4cb1d1 --- /dev/null +++ b/gradle/plugins/library/src/main/kotlin/ProcessingLibraryExtension.kt @@ -0,0 +1,64 @@ +import org.gradle.api.Action +import org.gradle.api.model.ObjectFactory +import java.io.Serializable +import javax.inject.Inject + +open class ProcessingLibraryExtension @Inject constructor(objects: ObjectFactory) { + var version: String? = null + val library = objects.newInstance(ProcessingLibraryConfiguration::class.java) + fun library(action: Action) { + action.execute(library) + } +} + +open class ProcessingLibraryConfiguration @Inject constructor() : Serializable { + /** + * Name of the library. If not set, the project name will be used. + */ + var name: String? = null + + /** + * Version number of the library. + */ + var version: Int? = null + + /** + * Pretty version string of the library. + */ + var prettyVersion: String? = null + + /** + * Map of author URLs to author names. + */ + var authors: Map = emptyMap() + + /** + * URL of the library where more information can be found. + */ + var url: String? = null + + /** + * List of categories the library belongs to. + */ + var categories: List = emptyList() + + /** + * A one-line sentence describing the library. + */ + var sentence: String? = null + + /** + * A longer paragraph describing the library. + */ + var paragraph: String? = null + + /** + * Minimum Processing revision required. + */ + var minRevision: Int? = null + + /** + * Maximum Processing revision supported. + */ + var maxRevision: Int? = null +} \ No newline at end of file diff --git a/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt b/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt new file mode 100644 index 0000000000..4514f581fd --- /dev/null +++ b/gradle/plugins/library/src/main/kotlin/ProcessingLibraryPlugin.kt @@ -0,0 +1,125 @@ +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.bundling.Jar +import org.gradle.api.tasks.bundling.Zip +import org.gradle.api.tasks.javadoc.Javadoc +import org.gradle.jvm.toolchain.JavaLanguageVersion +import java.util.prefs.Preferences + +class ProcessingLibraryPlugin : Plugin { + + override fun apply(target: Project) { + val extension = target.extensions.create("processing", ProcessingLibraryExtension::class.java) + target.plugins.apply(JavaPlugin::class.java) + + target.repositories.mavenCentral() + target.repositories.maven { it.setUrl("https://jogamp.org/deployment/maven/") } + + // Grab processing core if available, otherwise use the published version + val hasCore = try { + val core = target.project(":core") + target.dependencies.add("compileOnly", core) + true + } catch (_: Exception) { + false + } + + target.afterEvaluate { + if (!hasCore) { + if (extension.version == null) { + throw GradleException("Processing library version must be specified, please set processing.version in your build.gradle.kts") + } + val processingVersion = extension.version + target.dependencies.add("compileOnly", "org.processing:core:$processingVersion") + } + } + target.extensions.configure(JavaPluginExtension::class.java) { extension -> + extension.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) + } + + target.plugins.withType(JavaPlugin::class.java) { + val jarTask = target.tasks.named("jar", Jar::class.java) + val javaDocTask = target.tasks.named("javadoc", Javadoc::class.java) + + val bundleTask = target.tasks.register("bundleLibrary", BundleLibraryFilesTask::class.java) { task -> + task.configuration = extension.library + task.group = "processing" + task.description = "Creates the Processing library folder with jar, library.properties, and examples." + task.dependsOn(jarTask, javaDocTask) + } + + val zipTask = target.tasks.register("zipLibrary", Zip::class.java) { task -> + task.apply { + val libraryName = extension.library.name ?: target.name + val sourceDir = bundleTask.get().outputDir.get().asFile + + group = "processing" + description = "Creates a zip & pdex archive of the Processing library folder." + dependsOn(bundleTask) + include("${libraryName}/**") + + archiveFileName.set("$libraryName.zip") + from(sourceDir) + destinationDirectory.set(sourceDir) + doLast { + val zip = task.outputs.files.files.first() + zip.copyTo(sourceDir.resolve("$libraryName.pdex"), overwrite = true) + } + } + } + + target.tasks.register("installLibrary") { task -> + task.apply { + group = "processing" + dependsOn(zipTask) + doLast { + val preferences = Preferences.userRoot().node("org/processing/app") + + val semverRe = Regex("""^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?""") + fun semverKey(v: String): Triple { + val m = semverRe.find(v) + val maj = m?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0L + val min = m?.groupValues?.getOrNull(2)?.toLongOrNull() ?: 0L + val pat = m?.groupValues?.getOrNull(3)?.toLongOrNull() ?: 0L + val pre = m?.groupValues?.getOrNull(4) + val packed = (maj shl 40) or (min shl 20) or pat + return Triple(packed, pre == null, pre ?: "") + } + + val installLocations = preferences.get("installLocations", "") + .split(",") + .filter { it.isNotEmpty() } + .mapNotNull { + val parts = it.split("^") + if (parts.size < 2) null else parts[1] to parts[0] // version to path + } + .sortedWith(Comparator { a, b -> + val ka = semverKey(a.first) + val kb = semverKey(b.first) + when { + ka.first != kb.first -> kb.first.compareTo(ka.first) + ka.second != kb.second -> kb.second.compareTo(ka.second) + else -> kb.third.compareTo(ka.third) + } + }) + + val installPath = installLocations.firstOrNull()?.second + ?: throw GradleException("Could not find Processing install location in preferences.") + + val libraryName = extension.library.name ?: target.name + val sourceDir = bundleTask.get().outputDir.get().asFile.resolve("$libraryName.pdex") + + ProcessBuilder() + .command(installPath, sourceDir.absolutePath) + .inheritIO() + .start() + } + } + } + + } + } +} \ No newline at end of file diff --git a/gradle/plugins/settings.gradle.kts b/gradle/plugins/settings.gradle.kts new file mode 100644 index 0000000000..ab39f6aca7 --- /dev/null +++ b/gradle/plugins/settings.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} + +include("library") \ No newline at end of file diff --git a/java/build.gradle.kts b/java/build.gradle.kts index 47fa76e46b..7c26a200f4 100644 --- a/java/build.gradle.kts +++ b/java/build.gradle.kts @@ -68,7 +68,7 @@ tasks.register("copyCore"){ into(coreProject.layout.projectDirectory.dir("library")) } -val legacyLibraries = arrayOf("io","net") +val legacyLibraries = emptyArray() legacyLibraries.forEach { library -> tasks.register("library-$library-extraResources"){ val build = project(":java:libraries:$library").tasks.named("build") @@ -81,13 +81,14 @@ legacyLibraries.forEach { library -> include("library/**/*") include("examples/**/*") into( javaMode("/libraries/$library")) + dirPermissions { unix("rwx------") }; } bundle.configure { dependsOn("library-$library-extraResources") } } -val libraries = arrayOf("dxf", "pdf", "serial", "svg") +val libraries = arrayOf("dxf", "io", "net", "pdf", "serial", "svg") libraries.forEach { library -> val name = "create-$library-library" diff --git a/java/gradle/README.md b/java/gradle/README.md new file mode 100644 index 0000000000..b827972769 --- /dev/null +++ b/java/gradle/README.md @@ -0,0 +1,113 @@ +# Processing Gradle Plugin + +This folder contains the source for the Processing Gradle plugin. +The plugin will transform any Processing sketch into a Gradle project for easy compilation and advanced features. + +## Motivation + +Processing was designed to be easy to start with, and the PDE (Processing Development Environment) handles most things +for you: you can write code, import libraries, run your sketch, or even export it as an executable. This works very well +for learning and for small to medium sketches, but it isn’t ideal for larger projects. + +With the Processing Gradle Plugin, we want to make it possible to build more ambitious projects on top of Processing. +This is intended for users who are comfortable moving beyond the PDE, such as artists and developers working on larger +sketches, long running installations, multi sketch projects, or teams who want version control, automated builds, and +integration with standard Java tools and editors. It is optional and does not replace the PDE, but complements it for +more advanced workflows. + +## What is Gradle + +Gradle is a build tool commonly used in the Java ecosystem. It is responsible for tasks like compiling code, managing +dependencies, and running applications. You do not need to learn Gradle to use Processing in the P + +## Usage + +Add the following files to any Processing sketch alongside the `.pde` files + +`build.gradle.kts` +```kotlin +plugins { + id("org.processing.java") version "4.5.3" +} +``` + +The version number determines which version of Processing will be used. + +`settings.gradle.kts` +create the file but leave blank + +This will turn the Processing sketch into a Gradle project, usable with any editor that supports Gradle. +Including the `gradle` command if installed. If you want to use your own editor, or no editor at all, use the +gradle command if installed. Find installation instructions +here: https://docs.gradle.org/current/userguide/installation.html + +The plugin will add the `sketch` command to the Gradle tasks lists, so run the sketch with `gradle sketch`, this will +build and launch your sketch. + +The sketch can also be bundled into a standalone app by using the `gradle export` command. +Or run in fullscreen with `gradle present` + +To include libraries into your sketch add `processing.sketchbook=/path/to/sketchbook` to a `gradle.properties` file in +the same folder. + +To use any kind of dependency add as a normal gradle dependency, the plugin has already automatically added the Maven +Central repository. + +`build.gradle.kts` +```kotlin +plugins { + id("org.processing.java") version "4.5.3" +} + +dependencies { + implementation("com.lowagie:itext:2.1.7") +} +``` + +To use an older version of Processing just change the plugin version: + +`build.gradle.kts` +```kotlin +plugins { + id("org.processing.java") version "4.5.0" +} +``` + +Other gradle plugins are also supported + +`build.gradle.kts` +```kotlin +plugins { + id("org.processing.java") version "4.5.3" + id("com.gradleup.shadow") version "" +} +``` + +If you want to combine multiple sketches into a single project + +`sketch-a/build.gradle.kts` +```kotlin +plugins { + id("org.processing.java") version "4.5.3" +} +``` + +`sketch-b/build.gradle.kts` + +```kotlin +plugins { + id("org.processing.java") version "4.5.3" +} +``` + +`build.gradle.kts` + +```kotlin +plugins { + id("org.processing.java") version "4.5.3" apply false +} +``` + +`settings.gradle.kts` - create the file but leave blank + +Then run all sketches at once with `gradle sketch` \ No newline at end of file diff --git a/java/gradle/build.gradle.kts b/java/gradle/build.gradle.kts new file mode 100644 index 0000000000..8680c0a851 --- /dev/null +++ b/java/gradle/build.gradle.kts @@ -0,0 +1,64 @@ +plugins{ + `java-gradle-plugin` + alias(libs.plugins.gradlePublish) + + kotlin("jvm") version libs.versions.kotlin +} + +repositories { + mavenCentral() + maven("https://jogamp.org/deployment/maven") +} + +dependencies{ + implementation(project(":java:preprocessor")) + + implementation(libs.composeGradlePlugin) + implementation(libs.kotlinGradlePlugin) + implementation(libs.kotlinComposePlugin) + + testImplementation(project(":core")) + testImplementation(libs.junit) +} + +gradlePlugin{ + website = "https://processing.org/" + vcsUrl = "https://github.com/processing/processing4" + plugins{ + create("processing.java"){ + id = "$group.java" + displayName = "Processing Plugin" + description = "Gradle plugin for building Processing sketches" + tags = listOf("processing", "sketch", "dsl") + implementationClass = "org.processing.java.gradle.ProcessingPlugin" + } + } +} + +publishing{ + repositories{ + mavenLocal() + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} +// Grab the group before running tests, since the group is used in the test configuration and may be modified by the publishing configuration +val testGroup = group.toString() +tasks.withType().configureEach { + systemProperty("project.group", testGroup) +} + +tasks.register("writeVersion") { + // make the version available to the plugin at runtime by writing it to a properties file in the resources directory + doLast { + val file = layout.buildDirectory.file("resources/main/version.properties").get().asFile + file.parentFile.mkdirs() + file.writeText("version=${project.version}") + } +} + +tasks.named("processResources") { + dependsOn("writeVersion") +} \ No newline at end of file diff --git a/java/gradle/example/brightness.pde b/java/gradle/example/brightness.pde new file mode 100644 index 0000000000..dad7885af3 --- /dev/null +++ b/java/gradle/example/brightness.pde @@ -0,0 +1,28 @@ +/** + * Brightness + * by Rusty Robison. + * + * Brightness is the relative lightness or darkness of a color. + * Move the cursor vertically over each bar to alter its brightness. + */ + +int barWidth = 20; +int lastBar = -1; + + +void setup() { + size(640, 360, P2D); + colorMode(HSB, width, 100, height); + noStroke(); + background(0); +} + +void draw() { + int whichBar = mouseX / barWidth; + if (whichBar != lastBar) { + int barX = whichBar * barWidth; + fill(barX, 100, mouseY); + rect(barX, 0, barWidth, height); + lastBar = whichBar; + } +} diff --git a/java/gradle/example/build.gradle.kts b/java/gradle/example/build.gradle.kts new file mode 100644 index 0000000000..b476d51bba --- /dev/null +++ b/java/gradle/example/build.gradle.kts @@ -0,0 +1,3 @@ +plugins{ + id("org.processing.java") +} \ No newline at end of file diff --git a/java/gradle/example/settings.gradle.kts b/java/gradle/example/settings.gradle.kts new file mode 100644 index 0000000000..ee9c97e155 --- /dev/null +++ b/java/gradle/example/settings.gradle.kts @@ -0,0 +1,5 @@ +rootProject.name = "processing-gradle-plugin-demo" + +pluginManagement { + includeBuild("../../../") +} \ No newline at end of file diff --git a/java/gradle/src/main/kotlin/DependenciesTask.kt b/java/gradle/src/main/kotlin/DependenciesTask.kt new file mode 100644 index 0000000000..8e2cb9bca3 --- /dev/null +++ b/java/gradle/src/main/kotlin/DependenciesTask.kt @@ -0,0 +1,79 @@ +package org.processing.java.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.ObjectInputStream + +/* +* The DependenciesTask resolves the dependencies for the sketch based on the libraries used + */ +abstract class DependenciesTask: DefaultTask() { + @InputFile + val librariesMetaData: RegularFileProperty = project.objects.fileProperty() + + @InputFile + val sketchMetaData: RegularFileProperty = project.objects.fileProperty() + + init{ + librariesMetaData.convention(project.layout.buildDirectory.file("processing/libraries")) + sketchMetaData.convention(project.layout.buildDirectory.file("processing/sketch")) + } + + @TaskAction + fun execute() { + val sketchMetaFile = sketchMetaData.get().asFile + val librariesMetaFile = librariesMetaData.get().asFile + + val libraries = librariesMetaFile.inputStream().use { input -> + ObjectInputStream(input).readObject() as ArrayList + } + + val sketch = sketchMetaFile.inputStream().use { input -> + ObjectInputStream(input).readObject() as PDETask.SketchMeta + } + + val dependencies = mutableSetOf() + + // Loop over the import statements in the sketch and import the relevant jars from the libraries + sketch.importStatements.forEach import@{ statement -> + libraries.forEach { library -> + library.jars.forEach { jar -> + jar.classes.forEach { className -> + if (className.startsWith(statement)) { + dependencies.addAll(library.jars.map { it.path } ) + return@import + } + } + } + } + } + project.dependencies.add("implementation", project.files(dependencies) ) + + // TODO: Mutating the dependencies of configuration ':implementation' after it has been resolved or consumed. This + + // TODO: Add only if user is compiling for P2D or P3D + // Add JOGL and Gluegen dependencies + project.dependencies.add("runtimeOnly", "org.jogamp.jogl:jogl-all-main:2.5.0") + project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0") + + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + + val variant = when { + os.contains("mac") -> "macosx-universal" + os.contains("win") && arch.contains("64") -> "windows-amd64" + os.contains("linux") && arch.contains("aarch64") -> "linux-aarch64" + os.contains("linux") && arch.contains("arm") -> "linux-arm" + os.contains("linux") && arch.contains("amd64") -> "linux-amd64" + else -> throw GradleException("Unsupported OS/architecture: $os / $arch") + } + + project.dependencies.add("runtimeOnly", "org.jogamp.gluegen:gluegen-rt:2.5.0:natives-$variant") + project.dependencies.add("runtimeOnly", "org.jogamp.jogl:nativewindow:2.5.0:natives-$variant") + project.dependencies.add("runtimeOnly", "org.jogamp.jogl:newt:2.5.0:natives-$variant") + } +} \ No newline at end of file diff --git a/java/gradle/src/main/kotlin/LibrariesTask.kt b/java/gradle/src/main/kotlin/LibrariesTask.kt new file mode 100644 index 0000000000..2ccca5cde7 --- /dev/null +++ b/java/gradle/src/main/kotlin/LibrariesTask.kt @@ -0,0 +1,81 @@ +package org.processing.java.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.ObjectOutputStream +import java.util.jar.JarFile + +/* +The libraries task scans the sketchbook libraries folder for all the libraries +This task stores the resulting information in a file that can be used later to resolve dependencies + */ +abstract class LibrariesTask : DefaultTask() { + + @InputFiles + val libraryDirectories: ConfigurableFileCollection = project.files() + + @OutputFile + val librariesMetaData: RegularFileProperty = project.objects.fileProperty() + + init{ + librariesMetaData.convention { project.gradle.gradleUserHomeDir.resolve("common/processing/libraries") } + } + + data class Jar( + val path: File, + val classes: List + ) : java.io.Serializable + + data class Library( + val jars: List + ) : java.io.Serializable + + @TaskAction + fun execute() { + val output = libraryDirectories.flatMap { librariesDirectory -> + if (!librariesDirectory.exists()) { + logger.error("Libraries directory (${librariesDirectory.path}) does not exist. Libraries will not be imported.") + return@flatMap emptyList() + } + val libraries = librariesDirectory + .listFiles { file -> file.isDirectory } + ?.map { folder -> + // Find all the jars in the sketchbook + val jars = folder.resolve("library") + .listFiles{ file -> file.extension == "jar" } + ?.map{ file -> + + // Inside each jar, look for the defined classes + val jar = JarFile(file) + val classes = jar.entries().asSequence() + .filter { entry -> entry.name.endsWith(".class") } + .map { entry -> entry.name } + .map { it.substringBeforeLast('/').replace('/', '.') } + .distinct() + .toList() + + // Return a reference to the jar and its classes + return@map Jar( + path = file, + classes = classes + ) + }?: emptyList() + + // Save the parsed jars and which folder + return@map Library( + jars = jars + ) + }?: emptyList() + + return@flatMap libraries + } + val meta = ObjectOutputStream(librariesMetaData.get().asFile.outputStream()) + meta.writeObject(output) + meta.close() + } +} \ No newline at end of file diff --git a/java/gradle/src/main/kotlin/PDETask.kt b/java/gradle/src/main/kotlin/PDETask.kt new file mode 100644 index 0000000000..76ac195e53 --- /dev/null +++ b/java/gradle/src/main/kotlin/PDETask.kt @@ -0,0 +1,83 @@ +package org.processing.java.gradle + +import org.gradle.api.file.* +import org.gradle.api.tasks.* +import org.gradle.internal.file.Deleter +import org.gradle.work.InputChanges +import processing.mode.java.preproc.PdePreprocessor +import java.io.File +import java.io.ObjectOutputStream +import java.io.Serializable +import java.util.concurrent.Callable +import java.util.jar.JarFile +import javax.inject.Inject + + +// TODO: Generate sourcemaps +/* +* The PDETask is the main task that processes the .pde files and generates the Java source code through the PdePreprocessor. + */ +abstract class PDETask : SourceTask() { + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:IgnoreEmptyDirectories + @get:SkipWhenEmpty + open val stableSources: FileCollection = project.files(Callable { this.source }) + + @OutputDirectory + val outputDirectory: DirectoryProperty = project.objects.directoryProperty() + + @get:Input + var sketchName: String = "processing" + + @OutputFile + val sketchMetaData: RegularFileProperty = project.objects.fileProperty() + + init{ + outputDirectory.convention(project.layout.buildDirectory.dir("generated/pde")) + sketchMetaData.convention(project.layout.buildDirectory.file("processing/sketch")) + } + + data class SketchMeta( + val sketchName: String, + val sketchRenderer: String?, + val importStatements: List + ) : Serializable + + @TaskAction + fun execute() { + // Using stableSources since we can only run the pre-processor on the full set of sources + val combined = stableSources + .files + .groupBy { it.name } + .map { entry -> + entry.value.firstOrNull { it.parentFile?.name == "unsaved" } + ?: entry.value.first() + } + .joinToString("\n"){ + it.readText() + } + val javaFile = File(outputDirectory.get().asFile, "$sketchName.java").bufferedWriter() + + val meta = PdePreprocessor + .builderFor(sketchName) + .setTabSize(4) + .build() + .write(javaFile, combined) + + // TODO: Save the edits to meta files + + javaFile.flush() + javaFile.close() + + val sketchMeta = SketchMeta( + sketchName = sketchName, + sketchRenderer = meta.sketchRenderer, + importStatements = meta.importStatements.map { importStatement -> importStatement.packageName } + ) + + val metaFile = ObjectOutputStream(sketchMetaData.get().asFile.outputStream()) + metaFile.writeObject(sketchMeta) + metaFile.close() + } +} \ No newline at end of file diff --git a/java/gradle/src/main/kotlin/ProcessingPlugin.kt b/java/gradle/src/main/kotlin/ProcessingPlugin.kt new file mode 100644 index 0000000000..375b17549a --- /dev/null +++ b/java/gradle/src/main/kotlin/ProcessingPlugin.kt @@ -0,0 +1,219 @@ +package org.processing.java.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.SourceDirectorySet +import org.gradle.api.internal.file.DefaultSourceDirectorySet +import org.gradle.api.internal.tasks.TaskDependencyFactory +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.JavaExec +import org.jetbrains.compose.ComposeExtension +import org.jetbrains.compose.desktop.DesktopExtension +import java.io.File +import java.net.Socket +import javax.inject.Inject + +class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFactory) : Plugin { + override fun apply(project: Project) { + val sketchName = project.layout.projectDirectory.asFile.name.replace(Regex("[^a-zA-Z0-9_]"), "_") + + val isProcessing = project.findProperty("processing.version") != null + val processingVersion = project.findProperty("processing.version") as String? + ?: javaClass.classLoader.getResourceAsStream("version.properties")?.use { stream -> + java.util.Properties().apply { load(stream) }.getProperty("version") + } ?: "4.3.4" + val processingGroup = project.findProperty("processing.group") as String? ?: "org.processing" + val workingDir = project.findProperty("processing.workingDir") as String? + val debugPort = project.findProperty("processing.debugPort") as String? + val logPort = project.findProperty("processing.logPort") as String? + val errPort = project.findProperty("processing.errPort") as String? + + // TODO: Setup sketchbook when using as a standalone plugin, use the Java Preferences + val sketchbook = project.findProperty("processing.sketchbook") as String? + val settings = project.findProperty("processing.settings") as String? + val root = project.findProperty("processing.root") as String? + + // Apply the Java plugin to the Project, equivalent of + // plugins { + // java + // } + project.plugins.apply(JavaPlugin::class.java) + + if(isProcessing){ + // Set the build directory to a temp file so it doesn't clutter up the sketch folder + // Only if the build directory doesn't exist, otherwise proceed as normal + if(!project.layout.buildDirectory.asFile.get().exists()) { + project.layout.buildDirectory.set(File(project.findProperty("processing.workingDir") as String)) + } + // Disable the wrapper in the sketch to keep it cleaner + project.tasks.findByName("wrapper")?.enabled = false + } + + // Add kotlin support, equivalent of + // plugins { + // kotlin("jvm") version "1.8.0" + // kotlin("plugin.compose") version "1.8.0" + // } + project.plugins.apply("org.jetbrains.kotlin.jvm") + // Add jetpack compose support + project.plugins.apply("org.jetbrains.kotlin.plugin.compose") + // Add the compose plugin to wrap the sketch in an executable + project.plugins.apply("org.jetbrains.compose") + + // Add the Processing core library (within Processing from the internal maven repo and outside from the internet), equivalent of + // dependencies { + // implementation("org.processing:core:4.3.4") + // } + project.dependencies.add("implementation", "$processingGroup:core:${processingVersion}") + + // Add the jars in the code folder, equivalent of + // dependencies { + // implementation(fileTree("src") { include("**/code/*.jar") }) + // } + project.dependencies.add("implementation", project.fileTree("src").apply { include("**/code/*.jar") }) + + // Add the repositories necessary for building the sketch, equivalent of + // repositories { + // maven("https://jogamp.org/deployment/maven") + // mavenCentral() + // mavenLocal() + // } + project.repositories.add(project.repositories.maven { it.setUrl("https://jogamp.org/deployment/maven") }) + project.repositories.add(project.repositories.mavenCentral()) + project.repositories.add(project.repositories.mavenLocal()) + + // Configure the compose Plugin, equivalent of + // compose { + // application { + // mainClass.set(sketchName) + // nativeDistributions { + // includeAllModules() + // } + // } + // } + project.extensions.configure(ComposeExtension::class.java) { extension -> + extension.extensions.getByType(DesktopExtension::class.java).application { application -> + // Set the class to be executed initially + application.mainClass = sketchName + application.nativeDistributions.includeAllModules = true + if(debugPort != null) { + application.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") + } + } + } + + // TODO: Add support for customizing distributables + // TODO: Setup sensible defaults for the distributables + + // Add convenience tasks for running, presenting, and exporting the sketch outside of Processing + if(!isProcessing) { + project.tasks.create("sketch").apply { + group = "processing" + description = "Runs the Processing sketch" + dependsOn("run") + } + project.tasks.create("present").apply { + group = "processing" + description = "Presents the Processing sketch" + doFirst { + project.tasks.withType(JavaExec::class.java).configureEach { task -> + task.systemProperty("processing.fullscreen", "true") + } + } + finalizedBy("run") + } + project.tasks.create("export").apply { + group = "processing" + description = "Creates a distributable version of the Processing sketch" + + dependsOn("createDistributable") + + } + } + + project.afterEvaluate { + // Copy the result of create distributable to the project directory + project.tasks.named("createDistributable") { task -> + task.doLast { + project.copy { + it.from(project.tasks.named("createDistributable").get().outputs.files) + it.into(project.layout.projectDirectory) + } + } + } + } + + // Move the processing variables into javaexec tasks so they can be used in the sketch as well + project.tasks.withType(JavaExec::class.java).configureEach { task -> + project.properties + .filterKeys { it.startsWith("processing") } + .forEach { (key, value) -> task.systemProperty(key, value) } + + // Connect the stdio to the PDE if ports are specified + if(logPort != null) task.standardOutput = Socket("localhost", logPort.toInt()).outputStream + if(errPort != null) task.errorOutput = Socket("localhost", errPort.toInt()).outputStream + + } + + // For every Java Source Set (main, test, etc) add a PDE source set that includes .pde files + // and a task to process them before compilation + project.extensions.getByType(JavaPluginExtension::class.java).sourceSets.first().let{ sourceSet -> + val pdeSourceSet = objectFactory.newInstance( + DefaultPDESourceDirectorySet::class.java, + objectFactory.sourceDirectorySet("${sourceSet.name}.pde", "${sourceSet.name} Processing Source") + ) + + // Configure the PDE source set to include all .pde files in the sketch folder except those in the build directory + pdeSourceSet.apply { + srcDir("./") + srcDir("$workingDir/unsaved") + + filter.include("**/*.pde") + filter.exclude("${project.layout.buildDirectory.asFile.get().name}/**") + } + sourceSet.allSource.source(pdeSourceSet) + + // Add top level java source files + sourceSet.java.srcDir(project.layout.projectDirectory).apply { + include("/*.java") + } + + // Scan the libraries before compiling the sketches + val librariesTaskName = sourceSet.getTaskName("scanLibraries", "PDE") + val librariesScan = project.tasks.register(librariesTaskName, LibrariesTask::class.java) { task -> + task.description = "Scans the libraries in the sketchbook" + task.libraryDirectories.from(sketchbook?.let { File(it, "libraries") }, root?.let { File(it).resolve("modes/java/libraries") }) + } + + // Create a task to process the .pde files before compiling the java sources + val pdeTaskName = sourceSet.getTaskName("preprocess", "PDE") + val pdeTask = project.tasks.register(pdeTaskName, PDETask::class.java) { task -> + task.description = "Processes the ${sourceSet.name} PDE" + task.source = pdeSourceSet + task.sketchName = sketchName + + // Set the output of the pre-processor as the input for the java compiler + sourceSet.java.srcDir(task.outputDirectory) + } + + val depsTaskName = sourceSet.getTaskName("addLegacyDependencies", "PDE") + project.tasks.register(depsTaskName, DependenciesTask::class.java){ task -> + // Link the output of the libraries task to the dependencies task + task.librariesMetaData.set(librariesScan.get().librariesMetaData) + task.dependsOn(pdeTask, librariesScan) + } + + // Make sure that the PDE tasks runs before the java compilation task + project.tasks.named(sourceSet.compileJavaTaskName) { task -> + task.dependsOn(pdeTaskName, depsTaskName) + } + } + } + abstract class DefaultPDESourceDirectorySet @Inject constructor( + sourceDirectorySet: SourceDirectorySet, + taskDependencyFactory: TaskDependencyFactory + ) : DefaultSourceDirectorySet(sourceDirectorySet, taskDependencyFactory), SourceDirectorySet +} + diff --git a/java/gradle/src/test/kotlin/ProcessingPluginTest.kt b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt new file mode 100644 index 0000000000..c67725e993 --- /dev/null +++ b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt @@ -0,0 +1,303 @@ +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.lang.management.ManagementFactory +import java.net.URLClassLoader + +class ProcessingPluginTest{ + // TODO: Test on multiple platforms since there are meaningful differences between the platforms + data class TemporaryProcessingSketchResult( + val buildResult: BuildResult, + val sketchFolder: File, + val classLoader: ClassLoader + ) + + fun createTemporaryProcessingSketch(vararg arguments: String, configure: (sketchFolder: File) -> Unit): TemporaryProcessingSketchResult{ + val directory = TemporaryFolder() + directory.create() + val sketchFolder = directory.newFolder("sketch") + directory.newFile("sketch/build.gradle.kts").writeText(""" + plugins { + id("${System.getProperty("project.group")}.java") + } + """.trimIndent()) + directory.newFile("sketch/settings.gradle.kts") + configure(sketchFolder) + + val buildResult = GradleRunner.create() + .withProjectDir(sketchFolder) + .withArguments(*arguments) + .withPluginClasspath() + .withDebug(true) + .build() + + val classDir = sketchFolder.resolve("build/classes/java/main") + val classLoader = URLClassLoader(arrayOf(classDir.toURI().toURL()), this::class.java.classLoader) + + return TemporaryProcessingSketchResult( + buildResult, + sketchFolder, + classLoader + ) + } + + data class TemporaryProcessingLibraryResult( + val buildResult: BuildResult, + val libraryFolder: File + ) + + fun createTemporaryProcessingLibrary(name: String): TemporaryProcessingLibraryResult{ + val directory = TemporaryFolder() + directory.create() + val libraryFolder = directory.newFolder("libraries",name) + directory.newFile("libraries/$name/build.gradle.kts").writeText(""" + plugins { + java + } + tasks.jar{ + destinationDirectory.set(file("library")) + } + """.trimIndent()) + val srcDirectory = directory.newFolder("libraries", name,"src", "main", "java") + directory.newFile("libraries/$name/src/main/java/Example.java").writeText(""" + package testing.example; + + public class Example { + public void exampleMethod() { + System.out.println("Hello from Example library"); + } + } + """.trimIndent()) + directory.newFile("libraries/$name/settings.gradle.kts") + directory.newFile("libraries/$name/library.properties").writeText(""" + name=$name + author=Test Author + version=1.0.0 + sentence=An example library + paragraph=This is a longer description of the example library. + category=Examples + url=http://example.com + """.trimIndent()) + + if(isDebuggerAttached()){ + openFolderInFinder(libraryFolder) + } + + val buildResult = GradleRunner.create() + .withProjectDir(libraryFolder) + .withArguments("jar") + .withPluginClasspath() + .withDebug(true) + .build() + + + return TemporaryProcessingLibraryResult( + buildResult, + libraryFolder + ) + } + + @Test + fun testSinglePDE(){ + val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder -> + sketchFolder.resolve("sketch.pde").writeText(""" + void setup(){ + size(100, 100); + } + + void draw(){ + println("Hello World"); + } + """.trimIndent()) + } + + val sketchClass = classLoader.loadClass("sketch") + + assert(sketchClass != null) { + "Class sketch not found" + } + + assert(sketchClass?.methods?.find { method -> method.name == "setup" } != null) { + "Method setup not found in class sketch" + } + + assert(sketchClass?.methods?.find { method -> method.name == "draw" } != null) { + "Method draw not found in class sketch" + } + } + + @Test + fun testMultiplePDE(){ + val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder -> + sketchFolder.resolve("sketch.pde").writeText(""" + void setup(){ + size(100, 100); + } + + void draw(){ + otherFunction(); + } + """.trimIndent()) + sketchFolder.resolve("sketch2.pde").writeText(""" + void otherFunction(){ + println("Hi"); + } + """.trimIndent()) + } + + val sketchClass = classLoader.loadClass("sketch") + + assert(sketchClass != null) { + "Class sketch not found" + } + + assert(sketchClass?.methods?.find { method -> method.name == "otherFunction" } != null) { + "Method otherFunction not found in class sketch" + } + + } + + @Test + fun testJavaSourceFile(){ + val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder -> + sketchFolder.resolve("sketch.pde").writeText(""" + void setup(){ + size(100, 100); + } + + void draw(){ + println("Hello World"); + } + """.trimIndent()) + sketchFolder.resolve("extra.java").writeText(""" + class SketchJava { + public void javaMethod() { + System.out.println("Hello from Java"); + } + } + """.trimIndent()) + } + val sketchJavaClass = classLoader.loadClass("SketchJava") + + assert(sketchJavaClass != null) { + "Class SketchJava not found" + } + + assert(sketchJavaClass?.methods?.find { method -> method.name == "javaMethod" } != null) { + "Method javaMethod not found in class SketchJava" + } + } + + @Test + fun testWithUnsavedSource(){ + val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build"){ sketchFolder -> + sketchFolder.resolve("sketch.pde").writeText(""" + void setup(){ + size(100, 100); + } + + void draw(){ + println("Hello World"); + } + """.trimIndent()) + sketchFolder.resolve("../unsaved").mkdirs() + sketchFolder.resolve("../unsaved/sketch.pde").writeText(""" + void setup(){ + size(100, 100); + } + + void draw(){ + println("Hello World"); + } + + void newMethod(){ + println("This is an unsaved method"); + } + """.trimIndent()) + sketchFolder.resolve("gradle.properties").writeText(""") + processing.workingDir = ${sketchFolder.parentFile.absolutePath} + """.trimIndent()) + } + val sketchClass = classLoader.loadClass("sketch") + + assert(sketchClass != null) { + "Class sketch not found" + } + + assert(sketchClass?.methods?.find { method -> method.name == "newMethod" } != null) { + "Method otherFunction not found in class sketch" + } + } + + @Test + fun testImportingLibrary(){ + val libraryResult = createTemporaryProcessingLibrary("ExampleLibrary") + val (buildResult, sketchFolder, classLoader) = createTemporaryProcessingSketch("build") { sketchFolder -> + sketchFolder.resolve("sketch.pde").writeText(""" + import testing.example.*; + + Example example; + + void setup(){ + size(100, 100); + example = new Example(); + example.exampleMethod(); + } + + void draw(){ + println("Hello World"); + } + """.trimIndent()) + sketchFolder.resolve("gradle.properties").writeText(""") + processing.sketchbook = ${libraryResult.libraryFolder.parentFile.parentFile.absolutePath} + """.trimIndent()) + } + + val sketchClass = classLoader.loadClass("sketch") + + assert(sketchClass != null) { + "Class sketch not found" + } + + assert(sketchClass?.methods?.find { method -> method.name == "setup" } != null) { + "Method setup not found in class sketch" + } + + assert(sketchClass?.methods?.find { method -> method.name == "draw" } != null) { + "Method draw not found in class sketch" + } + } + + @Test + fun testUseInternalLibraries(){ + + } + + @Test + fun testUseCodeJar(){ + // TODO: test if adding jars to the code folder works + } + + fun isDebuggerAttached(): Boolean { + val runtimeMxBean = ManagementFactory.getRuntimeMXBean() + val inputArguments = runtimeMxBean.inputArguments + return inputArguments.any { + it.contains("-agentlib:jdwp") + } + } + fun openFolderInFinder(folder: File) { + if (!folder.exists() || !folder.isDirectory) { + println("Invalid directory: ${folder.absolutePath}") + return + } + + val process = ProcessBuilder("open", folder.absolutePath) + .inheritIO() + .start() + process.waitFor() + } +} + + diff --git a/java/libraries/dxf/build.gradle.kts b/java/libraries/dxf/build.gradle.kts index 93c114f41d..0fb74dfd3c 100644 --- a/java/libraries/dxf/build.gradle.kts +++ b/java/libraries/dxf/build.gradle.kts @@ -1,5 +1,24 @@ plugins{ - java + id("org.processing.library") + alias(libs.plugins.mavenPublish) +} + +processing { + library { + version = 1 + prettyVersion = "1.0.0" + + authors = mapOf( + "The Processing Foundation" to "https://processing.org" + ) + url = "https://processing.org/" + categories = listOf("file", "exporter", "dxf") + + sentence = "DXF export library for Processing" + paragraph = + "This library allows you to export your Processing drawings as DXF files, which can be opened in CAD applications." + + } } sourceSets { @@ -9,27 +28,55 @@ sourceSets { } } } -repositories{ - mavenCentral() - maven("https://jogamp.org/deployment/maven/") -} -dependencies{ - compileOnly(project(":core")) - implementation("com.lowagie:itext:2.1.7") +mavenPublishing { + publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) + signAllPublications() + coordinates("$group.core", name, version.toString()) + + pom { + name.set("Processing DXF") + description.set("Processing DFX") + url.set("https://processing.org") + licenses { + license { + name.set("LGPL") + url.set("https://www.gnu.org/licenses/lgpl-2.1.html") + } + } + developers { + developer { + id.set("steftervelde") + name.set("Stef Tervelde") + } + developer { + id.set("benfry") + name.set("Ben Fry") + } + } + scm { + url.set("https://github.com/processing/processing4") + connection.set("scm:git:git://github.com/processing/processing4.git") + developerConnection.set("scm:git:ssh://git@github.com/processing/processing4.git") + } + } } -tasks.register("createLibrary"){ + +/** + * @deprecated Legacy task, use 'bundleLibrary' task provided by 'org.processing.library' plugin + */ +tasks.register("createLibrary") { dependsOn("jar") into(layout.buildDirectory.dir("library")) - from(layout.projectDirectory){ - include ("library.properties") + from(layout.projectDirectory) { + include("library.properties") include("examples/**") } - from(configurations.runtimeClasspath){ + from(configurations.runtimeClasspath) { into("library") } diff --git a/java/libraries/io/build.gradle.kts b/java/libraries/io/build.gradle.kts index a176f03df7..f6af4b2b13 100644 --- a/java/libraries/io/build.gradle.kts +++ b/java/libraries/io/build.gradle.kts @@ -1 +1,92 @@ -ant.importBuild("build.xml") \ No newline at end of file +import com.vanniktech.maven.publish.SonatypeHost + +plugins { + java + alias(libs.plugins.mavenPublish) +} + +sourceSets { + main { + java { + srcDirs("src") + } + } +} + +repositories { + mavenCentral() +} + +dependencies { + compileOnly(project(":core")) +} + +tasks.register("createLibrary") { + dependsOn("jar") + into(layout.buildDirectory.dir("library")) + + from(layout.projectDirectory) { + include("library.properties") + include("examples/**") + } + + from(configurations.runtimeClasspath) { + into("library") + } + + from(tasks.jar) { + into("library") + rename { "io.jar" } + } + + from(layout.projectDirectory.dir("library")) { + include("linux-arm64/**") + include("linux-armv6hf/**") + include("linux32/**") + include("linux64/**") + into("library") + } +} + +publishing { + repositories { + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} + +mavenPublishing { + coordinates("$group.core", name, version.toString()) + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) + + signAllPublications() + + pom { + name.set("Processing IO") + description.set("Processing IO") + url.set("https://processing.org") + licenses { + license { + name.set("LGPL") + url.set("https://www.gnu.org/licenses/lgpl-2.1.html") + } + } + developers { + developer { + id.set("steftervelde") + name.set("Stef Tervelde") + } + developer { + id.set("benfry") + name.set("Ben Fry") + } + } + scm { + url.set("https://github.com/processing/processing4") + connection.set("scm:git:git://github.com/processing/processing4.git") + developerConnection.set("scm:git:ssh://git@github.com/processing/processing4.git") + } + } +} \ No newline at end of file diff --git a/java/libraries/net/build.gradle.kts b/java/libraries/net/build.gradle.kts index a176f03df7..23289e4855 100644 --- a/java/libraries/net/build.gradle.kts +++ b/java/libraries/net/build.gradle.kts @@ -1 +1,84 @@ -ant.importBuild("build.xml") \ No newline at end of file +import com.vanniktech.maven.publish.SonatypeHost + +plugins { + java + alias(libs.plugins.mavenPublish) +} + +sourceSets { + main { + java { + srcDirs("src") + } + } +} + +repositories { + mavenCentral() +} + +dependencies { + compileOnly(project(":core")) +} + +tasks.register("createLibrary") { + dependsOn("jar") + into(layout.buildDirectory.dir("library")) + + from(layout.projectDirectory) { + include("library.properties") + include("examples/**") + } + + from(configurations.runtimeClasspath) { + into("library") + } + + from(tasks.jar) { + into("library") + rename { "net.jar" } + } +} + +publishing { + repositories { + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} + +mavenPublishing { + coordinates("$group.core", name, version.toString()) + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) + + signAllPublications() + + pom { + name.set("Processing Net") + description.set("Processing Net") + url.set("https://processing.org") + licenses { + license { + name.set("LGPL") + url.set("https://www.gnu.org/licenses/lgpl-2.1.html") + } + } + developers { + developer { + id.set("steftervelde") + name.set("Stef Tervelde") + } + developer { + id.set("benfry") + name.set("Ben Fry") + } + } + scm { + url.set("https://github.com/processing/processing4") + connection.set("scm:git:git://github.com/processing/processing4.git") + developerConnection.set("scm:git:ssh://git@github.com/processing/processing4.git") + } + } +} \ No newline at end of file diff --git a/java/libraries/pdf/build.gradle.kts b/java/libraries/pdf/build.gradle.kts index 634fd265e9..f0e0485a17 100644 --- a/java/libraries/pdf/build.gradle.kts +++ b/java/libraries/pdf/build.gradle.kts @@ -1,5 +1,8 @@ +import com.vanniktech.maven.publish.SonatypeHost + plugins{ java + alias(libs.plugins.mavenPublish) } sourceSets { @@ -37,4 +40,47 @@ tasks.register("createLibrary"){ into("library") rename { "pdf.jar" } } -} \ No newline at end of file +} + +publishing{ + repositories{ + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} + +mavenPublishing{ + coordinates("$group.core", name, version.toString()) + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) + + signAllPublications() + + pom{ + name.set("Processing PDF") + description.set("Processing PDF") + url.set("https://processing.org") + licenses { + license { + name.set("LGPL") + url.set("https://www.gnu.org/licenses/lgpl-2.1.html") + } + } + developers { + developer { + id.set("steftervelde") + name.set("Stef Tervelde") + } + developer { + id.set("benfry") + name.set("Ben Fry") + } + } + scm{ + url.set("https://github.com/processing/processing4") + connection.set("scm:git:git://github.com/processing/processing4.git") + developerConnection.set("scm:git:ssh://git@github.com/processing/processing4.git") + } + } +} diff --git a/java/libraries/serial/build.gradle.kts b/java/libraries/serial/build.gradle.kts index f7f1ff4136..652b1b398e 100644 --- a/java/libraries/serial/build.gradle.kts +++ b/java/libraries/serial/build.gradle.kts @@ -1,5 +1,8 @@ +import com.vanniktech.maven.publish.SonatypeHost + plugins { java + alias(libs.plugins.mavenPublish) } sourceSets { @@ -23,15 +26,61 @@ dependencies { tasks.register("createLibrary") { dependsOn("jar") into(layout.buildDirectory.dir("library")) + from(layout.projectDirectory) { include("library.properties") include("examples/**") } + from(configurations.runtimeClasspath) { into("library") } + from(tasks.jar) { into("library") rename { "serial.jar" } } -} \ No newline at end of file +} + +publishing { + repositories { + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} + +mavenPublishing { + coordinates("$group.core", name, version.toString()) + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) + + signAllPublications() + + pom { + name.set("Processing Serial") + description.set("Processing Serial") + url.set("https://processing.org") + licenses { + license { + name.set("LGPL") + url.set("https://www.gnu.org/licenses/lgpl-2.1.html") + } + } + developers { + developer { + id.set("steftervelde") + name.set("Stef Tervelde") + } + developer { + id.set("benfry") + name.set("Ben Fry") + } + } + scm { + url.set("https://github.com/processing/processing4") + connection.set("scm:git:git://github.com/processing/processing4.git") + developerConnection.set("scm:git:ssh://git@github.com/processing/processing4.git") + } + } +} diff --git a/java/libraries/svg/build.gradle.kts b/java/libraries/svg/build.gradle.kts index ddc4397842..6189265ba1 100644 --- a/java/libraries/svg/build.gradle.kts +++ b/java/libraries/svg/build.gradle.kts @@ -1,5 +1,8 @@ +import com.vanniktech.maven.publish.SonatypeHost + plugins { java + alias(libs.plugins.mavenPublish) } sourceSets { @@ -9,6 +12,7 @@ sourceSets { } } } + repositories { mavenCentral() } @@ -38,3 +42,45 @@ tasks.register("createLibrary") { } } +publishing { + repositories { + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} + +mavenPublishing { + coordinates("$group.core", name, version.toString()) + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) + + signAllPublications() + + pom { + name.set("Processing SVG") + description.set("Processing SVG") + url.set("https://processing.org") + licenses { + license { + name.set("LGPL") + url.set("https://www.gnu.org/licenses/lgpl-2.1.html") + } + } + developers { + developer { + id.set("steftervelde") + name.set("Stef Tervelde") + } + developer { + id.set("benfry") + name.set("Ben Fry") + } + } + scm { + url.set("https://github.com/processing/processing4") + connection.set("scm:git:git://github.com/processing/processing4.git") + developerConnection.set("scm:git:ssh://git@github.com/processing/processing4.git") + } + } +} diff --git a/java/src/processing/mode/java/JavaBuild.java b/java/src/processing/mode/java/JavaBuild.java index b09f7d8dc0..b696ab0e20 100644 --- a/java/src/processing/mode/java/JavaBuild.java +++ b/java/src/processing/mode/java/JavaBuild.java @@ -997,7 +997,7 @@ protected boolean exportApplication(File destFolder, XML clazzPath = config.addChild("classPath"); clazzPath.addChild("mainClass").setContent(sketch.getMainName()); for (String jarName : jarList) { - clazzPath.addChild("cp").setContent("lib/" + jarName); + clazzPath.addChild("cp").setContent("%EXEDIR%/lib/" + jarName); } XML jre = config.addChild("jre"); if (embedJava) { diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 8d9b2252c8..561e38ba54 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -2234,4 +2234,14 @@ static private int howManyFloats(List> handles) { } return count; } + + @Override + public String getSketchDiagnostics() { + if (debugger.isStarted()) { + return debugger.getDiagnostics(); + } else if (runtime != null) { + return Debugger.getDiagnostics(runtime); + } + return super.getSketchDiagnostics(); + } } diff --git a/java/src/processing/mode/java/debug/Debugger.java b/java/src/processing/mode/java/debug/Debugger.java index 0136793200..e9d42895be 100644 --- a/java/src/processing/mode/java/debug/Debugger.java +++ b/java/src/processing/mode/java/debug/Debugger.java @@ -31,6 +31,7 @@ import java.awt.event.KeyEvent; import java.io.*; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -458,6 +459,124 @@ public synchronized void stepOut() { } + + /** + * Get diagnostics from the sketch, whether paused or running. + * If running, it will temporarily suspend the VM. + */ + public String getDiagnostics() { + return getDiagnostics(runtime); + } + + + /** + * Static helper to fetch diagnostics from a Runner, even if not debugging. + * Uses field reads instead of method invocations to avoid thread state issues. + */ + public static String getDiagnostics(Runner targetRuntime) { + if (targetRuntime == null) return ""; + VirtualMachine targetVM = targetRuntime.vm(); + if (targetVM == null) return ""; + + targetVM.suspend(); + try { + // Find the PApplet subclass + List pAppletClasses = targetVM.classesByName("processing.core.PApplet"); + if (pAppletClasses.isEmpty()) { + return "processing.core.PApplet not found in VM"; + } + ClassType pAppletBase = (ClassType) pAppletClasses.get(0); + + ClassType sketchClass = null; + for (ReferenceType type : targetVM.allClasses()) { + if (type instanceof ClassType) { + ClassType ct = (ClassType) type; + ClassType superclass = ct.superclass(); + while (superclass != null) { + if (superclass.equals(pAppletBase)) { + sketchClass = ct; + break; + } + superclass = superclass.superclass(); + } + if (sketchClass != null) break; + } + } + + if (sketchClass == null) { + return "Could not find sketch class extending PApplet"; + } + + // Find instance + List instances = sketchClass.instances(1); + if (instances.isEmpty()) { + return "No instance of " + sketchClass.name() + " found"; + } + ObjectReference appletInstance = instances.get(0); + + // Build diagnostics by reading fields directly (no thread required) + StringBuilder diag = new StringBuilder(); + diag.append("Sketch Diagnostics:\n"); + diag.append(" Class: ").append(sketchClass.name()).append("\n"); + + // Read PApplet fields + appendField(diag, appletInstance, pAppletBase, "width"); + appendField(diag, appletInstance, pAppletBase, "height"); + appendField(diag, appletInstance, pAppletBase, "pixelDensity"); + appendField(diag, appletInstance, pAppletBase, "frameCount"); + appendField(diag, appletInstance, pAppletBase, "frameRate"); + appendField(diag, appletInstance, pAppletBase, "focused"); + + // Try to get renderer class name from 'g' field (PGraphics) + try { + Field gField = pAppletBase.fieldByName("g"); + if (gField != null) { + Value gValue = appletInstance.getValue(gField); + if (gValue instanceof ObjectReference) { + ObjectReference graphics = (ObjectReference) gValue; + diag.append(" renderer: ").append(graphics.referenceType().name()).append("\n"); + } + } + } catch (Exception e) { + diag.append(" renderer: (unavailable)\n"); + } + + return diag.toString(); + + } catch (Exception e) { + return "Error gathering diagnostics: " + e.toString(); + } finally { + targetVM.resume(); + } + } + + /** + * Helper to append a field value to the diagnostics string. + */ + private static void appendField(StringBuilder sb, ObjectReference obj, ClassType type, String fieldName) { + try { + Field field = type.fieldByName(fieldName); + if (field != null) { + Value value = obj.getValue(field); + sb.append(" ").append(fieldName).append(": "); + if (value == null) { + sb.append("null"); + } else if (value instanceof com.sun.jdi.PrimitiveValue) { + sb.append(value.toString()); + } else if (value instanceof StringReference) { + sb.append(((StringReference) value).value()); + } else { + sb.append(value.toString()); + } + sb.append("\n"); + } + } catch (Exception e) { + sb.append(" ").append(fieldName).append(": (error: ").append(e.getMessage()).append(")\n"); + } + } + + + // /** Print the current stack trace. */ // public synchronized void printStackTrace() { // if (isStarted()) { diff --git a/settings.gradle.kts b/settings.gradle.kts index 7eacb06877..813b0a296c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,9 @@ rootProject.name = "processing" + +pluginManagement { + includeBuild("gradle/plugins") +} + include( "core", "core:examples", @@ -6,6 +11,7 @@ include( "app:utils", "java", "java:preprocessor", + "java:gradle", "java:libraries:dxf", "java:libraries:io", "java:libraries:net",