diff --git a/.all-contributorsrc b/.all-contributorsrc index d1d18fb98..5ba97fcde 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1642,6 +1642,15 @@ "contributions": [ "doc" ] + }, + { + "login": "catilac", + "name": "Moon", + "avatar_url": "https://avatars.githubusercontent.com/u/15107?v=4", + "profile": "https://softmoon.world", + "contributions": [ + "code" + ] } ], "repoType": "github", diff --git a/.github/workflows/release-gradle.yml b/.github/workflows/release-gradle.yml index 16e8984e3..8ec45cad0 100644 --- a/.github/workflows/release-gradle.yml +++ b/.github/workflows/release-gradle.yml @@ -153,6 +153,7 @@ jobs: 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' diff --git a/README.md b/README.md index c27391e6f..18920086f 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,7 @@ _Note: due to GitHub's limitations, this repository's [Contributors](https://git Andrew
Andrew

💻 Ngoc Doan
Ngoc Doan

💻 Manoel Ribeiro
Manoel Ribeiro

📖 + Moon
Moon

💻 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5323a1a82..c93092d59 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -124,6 +124,7 @@ dependencies { testImplementation(libs.junitJupiterParams) implementation(libs.clikt) + implementation(libs.kotlinxSerializationJson) } tasks.test { @@ -228,61 +229,44 @@ tasks.register("packageCustomMsi"){ tasks.register("generateSnapConfiguration"){ - val name = findProperty("snapname") ?: rootProject.name + 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") } - - onlyIf { OperatingSystem.current().isLinux } - val distributable = tasks.named("createDistributable").get() - dependsOn(distributable) - + val confinement = findProperty("snapconfinement") as String? ?: "strict" val dir = distributable.destinationDir.get() - val content = """ - name: $name - version: $version - base: core22 - summary: A creative coding editor - description: | - Processing is a flexible software sketchbook and a programming language designed for learning how to code. - confinement: strict - - apps: - processing: - command: opt/processing/bin/Processing - desktop: opt/processing/lib/processing-Processing.desktop - environment: - LD_LIBRARY_PATH: ${'$'}SNAP/opt/processing/lib/runtime/lib:${'$'}LD_LIBRARY_PATH - LIBGL_DRIVERS_PATH: ${'$'}SNAP/usr/lib/${'$'}SNAPCRAFT_ARCH_TRIPLET/dri - plugs: - - desktop - - desktop-legacy - - wayland - - x11 - - network - - opengl - - home - - removable-media - - audio-playback - - audio-record - - pulseaudio - - gpio - - parts: - processing: - plugin: dump - source: deb/processing_$version-1_$arch.deb - source-type: deb - stage-packages: - - openjdk-17-jre - override-prime: | - snapcraftctl prime - rm -vf usr/lib/jvm/java-17-openjdk-*/lib/security/cacerts - chmod -R +x opt/processing/lib/app/resources/jdk - """.trimIndent() - dir.file("../snapcraft.yaml").asFile.writeText(content) + val base = layout.projectDirectory.file("linux/snapcraft.base.yml") + + doFirst { + + 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 + } + dir.file("../snapcraft.yaml").asFile.writeText(content) + } } tasks.register("packageSnap"){ @@ -424,7 +408,6 @@ tasks.register("renameWindres") { } tasks.register("includeProcessingResources"){ dependsOn( - "includeJdk", "includeCore", "includeJavaMode", "includeSharedAssets", @@ -433,6 +416,7 @@ tasks.register("includeProcessingResources"){ "includeJavaModeResources", "renameWindres" ) + mustRunAfter("includeJdk") finalizedBy("signResources") } @@ -539,6 +523,7 @@ afterEvaluate { dependsOn("includeProcessingResources") } tasks.named("createDistributable").configure { + dependsOn("includeJdk") finalizedBy("setExecutablePermissions") } } diff --git a/app/linux/snapcraft.base.yml b/app/linux/snapcraft.base.yml new file mode 100644 index 000000000..4847f0a7c --- /dev/null +++ b/app/linux/snapcraft.base.yml @@ -0,0 +1,42 @@ +name: $name +version: $version +base: core22 +summary: A creative coding editor +description: | + Processing is a flexible software sketchbook and a programming language designed for learning how to code. +confinement: $confinement + +apps: + processing: + command: opt/processing/bin/Processing + desktop: opt/processing/lib/processing-Processing.desktop + environment: + LD_LIBRARY_PATH: $SNAP/opt/processing/lib/runtime/lib:$LD_LIBRARY_PATH + LIBGL_DRIVERS_PATH: $SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/dri + # PLUGS START + plugs: + - desktop + - desktop-legacy + - wayland + - x11 + - network + - opengl + - home + - removable-media + - audio-playback + - audio-record + - pulseaudio + - gpio + # PLUGS END + +parts: + processing: + plugin: dump + source: deb/processing_$version-1_$arch.deb + source-type: deb + stage-packages: + - openjdk-17-jre + override-prime: | + snapcraftctl prime + rm -vf usr/lib/jvm/java-17-openjdk-*/lib/security/cacerts + chmod -R +x opt/processing/lib/app/resources/jdk \ No newline at end of file diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index ce78b4b6c..06e6458fc 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -166,18 +166,6 @@ public class Base { static private void createAndShowGUI(String[] args) { // these times are fairly negligible relative to Base. // long t1 = System.currentTimeMillis(); - var preferences = java.util.prefs.Preferences.userRoot().node("org/processing/app"); - var installLocations = new ArrayList<>(List.of(preferences.get("installLocations", "").split(","))); - var installLocation = System.getProperty("user.dir") + "^" + Base.getVersionName(); - - // Check if the installLocation is already in the list - if (!installLocations.contains(installLocation)) { - // Add the installLocation to the list - installLocations.add(installLocation); - - // Save the updated list back to preferences - preferences.put("installLocations", String.join(",", installLocations)); - } // TODO: Cleanup old locations if no longer installed // TODO: Cleanup old locations if current version is installed in the same location diff --git a/app/src/processing/app/Platform.java b/app/src/processing/app/Platform.java index b911d7e0a..2c2ade5e1 100644 --- a/app/src/processing/app/Platform.java +++ b/app/src/processing/app/Platform.java @@ -105,6 +105,9 @@ public class Platform { "An unknown error occurred while trying to load\n" + "platform-specific code for your machine.", e); } + + // Fix the issue where `java.home` points to the JRE instead of the JDK. processing/processing4#1163 + System.setProperty("java.home", getJavaHome().getAbsolutePath()); } @@ -389,6 +392,7 @@ public class Platform { } static public File getJavaHome() { + // Get the build in JDK location from the Jetpack Compose resources var resourcesDir = System.getProperty("compose.application.resources.dir"); if(resourcesDir != null) { var jdkFolder = new File(resourcesDir,"jdk"); @@ -397,10 +401,13 @@ public class Platform { } } + // If the JDK is set in the environment, use that. var home = System.getProperty("java.home"); if(home != null){ return new File(home); } + + // Otherwise try to use the Ant embedded JDK. if (Platform.isMacOS()) { //return "Contents/PlugIns/jdk1.7.0_40.jdk/Contents/Home/jre/bin/java"; File[] plugins = getContentFile("../PlugIns").listFiles((dir, name) -> dir.isDirectory() && diff --git a/app/src/processing/app/Processing.kt b/app/src/processing/app/Processing.kt index 4ca96d58e..a94f852df 100644 --- a/app/src/processing/app/Processing.kt +++ b/app/src/processing/app/Processing.kt @@ -10,7 +10,12 @@ import com.github.ajalt.clikt.parameters.arguments.multiple import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.help import com.github.ajalt.clikt.parameters.options.option +import processing.app.api.Contributions +import processing.app.api.Sketchbook import processing.app.ui.Start +import java.io.File +import java.util.prefs.Preferences +import kotlin.concurrent.thread class Processing: SuspendingCliktCommand("processing"){ val version by option("-v","--version") @@ -29,6 +34,11 @@ class Processing: SuspendingCliktCommand("processing"){ return } + thread { + // Update the install locations in preferences + updateInstallLocations() + } + val subcommand = currentContext.invokedSubcommand if (subcommand == null) { Start.main(sketches.toTypedArray()) @@ -40,7 +50,9 @@ suspend fun main(args: Array){ Processing() .subcommands( LSP(), - LegacyCLI(args) + LegacyCLI(args), + Contributions(), + Sketchbook() ) .main(args) } @@ -49,6 +61,9 @@ class LSP: SuspendingCliktCommand("lsp"){ override fun help(context: Context) = "Start the Processing Language Server" override suspend fun run(){ try { + // run in headless mode + System.setProperty("java.awt.headless", "true") + // Indirect invocation since app does not depend on java mode Class.forName("processing.mode.java.lsp.PdeLanguageServer") .getMethod("main", Array::class.java) @@ -68,10 +83,9 @@ class LegacyCLI(val args: Array): SuspendingCliktCommand("cli") { override suspend fun run() { try { - if (arguments.contains("--build")) { - System.setProperty("java.awt.headless", "true") - } + System.setProperty("java.awt.headless", "true") + // Indirect invocation since app does not depend on java mode Class.forName("processing.mode.java.Commander") .getMethod("main", Array::class.java) .invoke(null, arguments.toTypedArray()) @@ -80,3 +94,49 @@ class LegacyCLI(val args: Array): SuspendingCliktCommand("cli") { } } } + +fun updateInstallLocations(){ + val preferences = Preferences.userRoot().node("org/processing/app") + val installLocations = preferences.get("installLocations", "") + .split(",") + .dropLastWhile { it.isEmpty() } + .filter { install -> + try{ + val (path, version) = install.split("^") + val file = File(path) + if(!file.exists() || file.isDirectory){ + return@filter false + } + // call the path to check if it is a valid install location + val process = ProcessBuilder(path, "--version") + .redirectErrorStream(true) + .start() + val exitCode = process.waitFor() + if(exitCode != 0){ + return@filter false + } + val output = process.inputStream.bufferedReader().readText() + return@filter output.contains(version) + } catch (e: Exception){ + false + } + } + .toMutableList() + val command = ProcessHandle.current().info().command() + if(command.isEmpty) { + return + } + val installLocation = "${command.get()}^${Base.getVersionName()}" + + + // Check if the installLocation is already in the list + if (installLocations.contains(installLocation)) { + return + } + + // Add the installLocation to the list + installLocations.add(installLocation) + + // Save the updated list back to preferences + preferences.put("installLocations", java.lang.String.join(",", installLocations)) +} diff --git a/app/src/processing/app/UpdateCheck.java b/app/src/processing/app/UpdateCheck.java index 1bfa29688..e18daee3e 100644 --- a/app/src/processing/app/UpdateCheck.java +++ b/app/src/processing/app/UpdateCheck.java @@ -35,6 +35,7 @@ import processing.app.ui.WelcomeToBeta; import processing.core.PApplet; + /** * Threaded class to check for updates in the background. *

@@ -112,6 +113,7 @@ public class UpdateCheck { System.getProperty("os.arch")); int latest = readInt(LATEST_URL + "?" + info); + int revision = Base.getRevision(); String lastString = Preferences.get("update.last"); long now = System.currentTimeMillis(); @@ -125,18 +127,19 @@ public class UpdateCheck { Preferences.set("update.last", String.valueOf(now)); if (base.activeEditor != null) { -// boolean offerToUpdateContributions = true; - if (latest > Base.getRevision()) { + if (latest > revision) { System.out.println("You are running Processing revision 0" + - Base.getRevision() + ", the latest build is 0" + + revision + ", the latest build is 0" + latest + "."); // Assume the person is busy downloading the latest version // offerToUpdateContributions = !promptToVisitDownloadPage(); promptToVisitDownloadPage(); } - if(latest < Base.getRevision()){ - WelcomeToBeta.showWelcomeToBeta(); + + int lastBetaWelcomeSeen = Preferences.getInteger("update.beta_welcome"); + if(latest < revision && revision != lastBetaWelcomeSeen ) { + WelcomeToBeta.showWelcomeToBeta(); } /* diff --git a/app/src/processing/app/api/Contributions.kt b/app/src/processing/app/api/Contributions.kt new file mode 100644 index 000000000..25e693404 --- /dev/null +++ b/app/src/processing/app/api/Contributions.kt @@ -0,0 +1,144 @@ +package processing.app.api + +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.core.subcommands +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import processing.app.Platform +import processing.app.api.Sketch.Companion.getSketches +import java.io.File + +class Contributions: SuspendingCliktCommand(){ + override fun help(context: Context) = "Manage Processing contributions" + override suspend fun run() { + System.setProperty("java.awt.headless", "true") + } + init { + subcommands(Examples()) + } + + class Examples: SuspendingCliktCommand("examples") { + override fun help(context: Context) = "Manage Processing examples" + override suspend fun run() { + } + init { + subcommands(ExamplesList()) + } + } + + class ExamplesList: SuspendingCliktCommand("list") { + + + val serializer = Json { + prettyPrint = true + } + + override fun help(context: Context) = "List all examples" + override suspend fun run() { + Platform.init() + // TODO: Decouple modes listing from `Base` class, defaulting to Java mode for now + // TODO: Allow the user to change the sketchbook location + // TODO: Currently blocked since `Base.getSketchbookFolder()` is not available in headless mode + val sketchbookFolder = Platform.getDefaultSketchbookFolder() + val resourcesDir = System.getProperty("compose.application.resources.dir") + + val javaMode = "$resourcesDir/modes/java" + + val javaModeExamples = File("$javaMode/examples") + .listFiles() + ?.map { getSketches(it)} + ?: emptyList() + + val javaModeLibrariesExamples = File("$javaMode/libraries") + .listFiles{ it.isDirectory } + ?.map { library -> + val properties = library.resolve("library.properties") + val name = findNameInProperties(properties) ?: library.name + + val libraryExamples = getSketches(library.resolve("examples")) + Sketch.Companion.Folder( + type = "folder", + name = name, + path = library.absolutePath, + mode = "java", + children = libraryExamples?.children ?: emptyList(), + sketches = libraryExamples?.sketches ?: emptyList() + ) + } ?: emptyList() + val javaModeLibraries = Sketch.Companion.Folder( + type = "folder", + name = "Libraries", + path = "$javaMode/libraries", + mode = "java", + children = javaModeLibrariesExamples, + sketches = emptyList() + ) + + val contributedLibraries = sketchbookFolder.resolve("libraries") + .listFiles{ it.isDirectory } + ?.map { library -> + val properties = library.resolve("library.properties") + val name = findNameInProperties(properties) ?: library.name + // Get library name from library.properties if it exists + val libraryExamples = getSketches(library.resolve("examples")) + Sketch.Companion.Folder( + type = "folder", + name = name, + path = library.absolutePath, + mode = "java", + children = libraryExamples?.children ?: emptyList(), + sketches = libraryExamples?.sketches ?: emptyList() + ) + } ?: emptyList() + + val contributedLibrariesFolder = Sketch.Companion.Folder( + type = "folder", + name = "Contributed Libraries", + path = sketchbookFolder.resolve("libraries").absolutePath, + mode = "java", + children = contributedLibraries, + sketches = emptyList() + ) + + val contributedExamples = sketchbookFolder.resolve("examples") + .listFiles{ it.isDirectory } + ?.map { + val properties = it.resolve("examples.properties") + val name = findNameInProperties(properties) ?: it.name + + val sketches = getSketches(it.resolve("examples")) + Sketch.Companion.Folder( + type = "folder", + name, + path = it.absolutePath, + mode = "java", + children = sketches?.children ?: emptyList(), + sketches = sketches?.sketches ?: emptyList(), + ) + } + ?: emptyList() + val contributedExamplesFolder = Sketch.Companion.Folder( + type = "folder", + name = "Contributed Examples", + path = sketchbookFolder.resolve("examples").absolutePath, + mode = "java", + children = contributedExamples, + sketches = emptyList() + ) + + val json = serializer.encodeToString(javaModeExamples + javaModeLibraries + contributedLibrariesFolder + contributedExamplesFolder) + println(json) + } + + private fun findNameInProperties(properties: File): String? { + if (!properties.exists()) return null + + return properties.readLines().firstNotNullOfOrNull { line -> + line.split("=", limit = 2) + .takeIf { it.size == 2 && it[0].trim() == "name" } + ?.let { it[1].trim() } + } + } + } +} \ No newline at end of file diff --git a/app/src/processing/app/api/Sketch.kt b/app/src/processing/app/api/Sketch.kt new file mode 100644 index 000000000..0b57f369d --- /dev/null +++ b/app/src/processing/app/api/Sketch.kt @@ -0,0 +1,50 @@ +package processing.app.api + +import kotlinx.serialization.Serializable +import java.io.File + +class Sketch { + companion object{ + @Serializable + data class Sketch( + val type: String = "sketch", + val name: String, + val path: String, + val mode: String = "java", + ) + + @Serializable + data class Folder( + val type: String = "folder", + val name: String, + val path: String, + val mode: String = "java", + val children: List = emptyList(), + val sketches: List = emptyList() + ) + + fun getSketches(file: File, filter: (File) -> Boolean = { true }): Folder? { + val name = file.name + val (sketchesFolders, childrenFolders) = file.listFiles()?.filter (File::isDirectory)?.partition { isSketchFolder(it) } ?: return Folder( + name = name, + path = file.absolutePath, + sketches = emptyList(), + children = emptyList() + ) + val children = childrenFolders.filter(filter).mapNotNull { getSketches(it) } + val sketches = sketchesFolders.map { Sketch(name = it.name, path = it.absolutePath) } + if(sketches.isEmpty() && children.isEmpty()) { + return null + } + return Folder( + name = name, + path = file.absolutePath, + children = children, + sketches = sketches + ) + } + fun isSketchFolder(file: File): Boolean { + return file.isDirectory && file.listFiles().any { it.isFile && it.name.endsWith(".pde") } + } + } +} diff --git a/app/src/processing/app/api/Sketchbook.kt b/app/src/processing/app/api/Sketchbook.kt new file mode 100644 index 000000000..d3fdb411b --- /dev/null +++ b/app/src/processing/app/api/Sketchbook.kt @@ -0,0 +1,50 @@ +package processing.app.api + +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.core.subcommands +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import processing.app.Platform +import processing.app.Preferences +import processing.app.api.Sketch.Companion.getSketches +import java.io.File + +class Sketchbook: SuspendingCliktCommand() { + + + override fun help(context: Context) = "Manage the sketchbook" + override suspend fun run() { + System.setProperty("java.awt.headless", "true") + } + init { + subcommands(SketchbookList()) + } + + + class SketchbookList: SuspendingCliktCommand("list") { + val serializer = Json { + prettyPrint = true + } + + override fun help(context: Context) = "List all sketches" + override suspend fun run() { + Platform.init() + // TODO: Allow the user to change the sketchbook location + // TODO: Currently blocked since `Base.getSketchbookFolder()` is not available in headless mode + val sketchbookFolder = Platform.getDefaultSketchbookFolder() + + val sketches = getSketches(sketchbookFolder) { + !listOf( + "android", + "modes", + "tools", + "examples", + "libraries" + ).contains(it.name) + } + val json = serializer.encodeToString(listOf(sketches)) + println(json) + } + } +} \ No newline at end of file diff --git a/app/src/processing/app/ui/WelcomeToBeta.kt b/app/src/processing/app/ui/WelcomeToBeta.kt index d7492fa6a..7757e820f 100644 --- a/app/src/processing/app/ui/WelcomeToBeta.kt +++ b/app/src/processing/app/ui/WelcomeToBeta.kt @@ -35,6 +35,7 @@ import com.mikepenz.markdown.m2.markdownColor import com.mikepenz.markdown.m2.markdownTypography import com.mikepenz.markdown.model.MarkdownColors import com.mikepenz.markdown.model.MarkdownTypography +import processing.app.Preferences import processing.app.Base.getRevision import processing.app.Base.getVersionName import processing.app.ui.theme.LocalLocale @@ -61,7 +62,10 @@ class WelcomeToBeta { val mac = SystemInfo.isMacFullWindowContentSupported SwingUtilities.invokeLater { JFrame(windowTitle).apply { - val close = { dispose() } + val close = { + Preferences.set("update.beta_welcome", getRevision().toString()) + dispose() + } rootPane.putClientProperty("apple.awt.transparentTitleBar", mac) rootPane.putClientProperty("apple.awt.fullWindowContent", mac) defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE diff --git a/build/shared/lib/defaults.txt b/build/shared/lib/defaults.txt index 6e3e00f0d..1cfc190ca 100644 --- a/build/shared/lib/defaults.txt +++ b/build/shared/lib/defaults.txt @@ -76,6 +76,10 @@ theme.gradient.method = rgb # on how many people are using Processing) update.check = true +# default value for beta_welcome +# -1 means no beta has been run +update.beta_welcome = -1 + # on windows, automatically associate .pde files with processing.exe platform.auto_file_type_associations = true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 70f93aaff..dfacae1ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,7 @@ jsoup = { module = "org.jsoup:jsoup", version = "1.17.2" } markdown = { module = "com.mikepenz:multiplatform-markdown-renderer-m2", version = "0.31.0" } markdownJVM = { module = "com.mikepenz:multiplatform-markdown-renderer-jvm", version = "0.31.0" } clikt = { module = "com.github.ajalt.clikt:clikt", version = "5.0.2" } +kotlinxSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.3" } [plugins] jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }