diff --git a/.gitignore b/.gitignore index 67057f9425..4532993a0d 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/ @@ -123,4 +136,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/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000000..2db2e88c86 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/ant/processing/app/gradle/GradleService.java b/app/ant/processing/app/gradle/GradleService.java new file mode 100644 index 0000000000..98f7395133 --- /dev/null +++ b/app/ant/processing/app/gradle/GradleService.java @@ -0,0 +1,22 @@ +package processing.app.gradle; + +import processing.app.Mode; +import processing.app.Sketch; +import processing.app.ui.Editor; + +import java.io.PrintStream; + +public class GradleService { + public GradleService(Mode mode, Editor editor) { } + + public void setEnabled(boolean enabled) {} + public boolean getEnabled() { return false; } + public void prepare(){} + public void run() {} + public void export(){} + public void stop() {} + public void startService() {} + public void setSketch(Sketch sketch) {} + public void setErr(PrintStream err) {} + public void setOut(PrintStream out) {} +} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0d3fcbd12d..25d52e6f9b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,8 @@ import org.gradle.internal.jvm.Jvm +import org.gradle.kotlin.dsl.support.zipTo import org.gradle.internal.os.OperatingSystem import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +import org.jetbrains.compose.ExperimentalComposeLibrary import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask import org.jetbrains.compose.internal.de.undercouch.gradle.tasks.download.Download @@ -49,14 +51,18 @@ compose.desktop { application { mainClass = "processing.app.ProcessingKt" - jvmArgs(*listOf( - Pair("processing.version", rootProject.version), - Pair("processing.revision", findProperty("revision") ?: Int.MAX_VALUE), - Pair("processing.contributions.source", "https://contributions.processing.org/contribs"), - Pair("processing.download.page", "https://processing.org/download/"), - Pair("processing.download.latest", "https://processing.org/download/latest.txt"), - Pair("processing.tutorials", "https://processing.org/tutorials/"), - ).map { "-D${it.first}=${it.second}" }.toTypedArray()) + + val variables = mapOf( + "processing.group" to (rootProject.group.takeIf { it != "" } ?: "processing"), + "processing.version" to rootProject.version, + "processing.revision" to (findProperty("revision") ?: Int.MAX_VALUE), + "processing.contributions.source" to "https://contributions.processing.org/contribs", + "processing.download.page" to "https://processing.org/download/", + "processing.download.latest" to "https://processing.org/download/latest.txt", + "processing.tutorials" to "https://processing.org/tutorials/" + ) + + jvmArgs(*variables.entries.map { "-D${it.key}=${it.value}" }.toTypedArray()) nativeDistributions{ modules("jdk.jdi", "java.compiler", "jdk.accessibility", "java.management.rmi", "java.scripting", "jdk.httpserver") @@ -111,6 +117,7 @@ dependencies { implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) + implementation(compose.materialIconsExtended) implementation(compose.desktop.currentOs) @@ -118,14 +125,14 @@ dependencies { implementation(libs.kaml) implementation(libs.markdown) implementation(libs.markdownJVM) + implementation(gradleApi()) + implementation(libs.clikt) + implementation(libs.kotlinxSerializationJson) testImplementation(kotlin("test")) testImplementation(libs.mockitoKotlin) testImplementation(libs.junitJupiter) testImplementation(libs.junitJupiterParams) - - implementation(libs.clikt) - implementation(libs.kotlinxSerializationJson) } tasks.test { @@ -390,23 +397,6 @@ tasks.register("includeJavaModeResources") { from(java.layout.buildDirectory.dir("resources-bundled")) into(composeResources("../")) } -// TODO: Move to java mode -tasks.register("renameWindres") { - dependsOn("includeSharedAssets","includeJavaModeResources") - val dir = composeResources("modes/java/application/launch4j/bin/") - val os = DefaultNativePlatform.getCurrentOperatingSystem() - val platform = when { - os.isWindows -> "windows" - os.isMacOsX -> "macos" - else -> "linux" - } - from(dir) { - include("*-$platform*") - rename("(.*)-$platform(.*)", "$1$2") - } - duplicatesStrategy = DuplicatesStrategy.INCLUDE - into(dir) -} tasks.register("includeProcessingResources"){ dependsOn( "includeCore", @@ -414,8 +404,7 @@ tasks.register("includeProcessingResources"){ "includeSharedAssets", "includeProcessingExamples", "includeProcessingWebsiteExamples", - "includeJavaModeResources", - "renameWindres" + "includeJavaModeResources" ) mustRunAfter("includeJdk") finalizedBy("signResources") @@ -495,9 +484,9 @@ tasks.register("signResources"){ } file(composeResources("Info.plist")).delete() } +} -} tasks.register("setExecutablePermissions") { description = "Sets executable permissions on binaries in Processing.app resources" group = "compose desktop" @@ -522,6 +511,8 @@ tasks.register("setExecutablePermissions") { afterEvaluate { tasks.named("prepareAppResources").configure { dependsOn("includeProcessingResources") + // Make sure all libraries are bundled in the maven repository distributed with the app + dependsOn(listOf("core","java:preprocessor", "java:gradle", "java:gradle:hotreload").map { project(":$it").tasks.named("publishAllPublicationsToAppRepository") }) } tasks.named("createDistributable").configure { dependsOn("includeJdk") diff --git a/app/src/main/resources/defaults.txt b/app/src/main/resources/defaults.txt index 6e3e00f0d6..431988bb74 100644 --- a/app/src/main/resources/defaults.txt +++ b/app/src/main/resources/defaults.txt @@ -186,6 +186,9 @@ console.temp.days = 7 console.scrollback.lines = 500 console.scrollback.chars = 40000 +# run java sketches with Gradle aka the Modern Build System +run.use_gradle = false + # Any additional Java options when running. # If you change this and can't run things, it's your own durn fault. run.options = diff --git a/app/src/processing/app/Language.java b/app/src/processing/app/Language.java index d55c8b710c..ad67ffe8fb 100644 --- a/app/src/processing/app/Language.java +++ b/app/src/processing/app/Language.java @@ -183,7 +183,6 @@ static public Language init() { return instance; } - static private String get(String key) { LanguageBundle bundle = init().bundle; diff --git a/app/src/processing/app/Preferences.java b/app/src/processing/app/Preferences.java index 640c77eade..076506296f 100644 --- a/app/src/processing/app/Preferences.java +++ b/app/src/processing/app/Preferences.java @@ -393,6 +393,8 @@ static public String getSketchbookPath() { static protected void setSketchbookPath(String path) { + // Unify path seperator for all platforms + path = path.replace(File.separatorChar, '/'); set("sketchbook.path.four", path); //$NON-NLS-1$ } } diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt index c5645c9bbc..87f36fe358 100644 --- a/app/src/processing/app/Preferences.kt +++ b/app/src/processing/app/Preferences.kt @@ -34,6 +34,7 @@ fun loadPreferences(): Properties{ } } +// TODO: Move this to a more appropriate place @Composable fun watchFile(file: File): Any? { val scope = rememberCoroutineScope() diff --git a/app/src/processing/app/Processing.kt b/app/src/processing/app/Processing.kt index a94f852df8..7f7e82a09d 100644 --- a/app/src/processing/app/Processing.kt +++ b/app/src/processing/app/Processing.kt @@ -10,6 +10,7 @@ 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.gradle.api.Sketch import processing.app.api.Contributions import processing.app.api.Sketchbook import processing.app.ui.Start @@ -51,6 +52,7 @@ suspend fun main(args: Array){ .subcommands( LSP(), LegacyCLI(args), + Sketch(), Contributions(), Sketchbook() ) diff --git a/app/src/processing/app/Sketch.java b/app/src/processing/app/Sketch.java index 8bb50352b0..52c0de5b20 100644 --- a/app/src/processing/app/Sketch.java +++ b/app/src/processing/app/Sketch.java @@ -50,6 +50,8 @@ * Stores information about files in the current sketch. */ public class Sketch { + public static final String PROPERTIES_NAME = "sketch.properties"; + private final Editor editor; private final Mode mode; @@ -1305,7 +1307,7 @@ static protected Settings loadProperties(File folder) throws IOException { } return null; */ - return new Settings(new File(folder, "sketch.properties")); + return new Settings(new File(folder, PROPERTIES_NAME)); } diff --git a/app/src/processing/app/gradle/Debugger.kt b/app/src/processing/app/gradle/Debugger.kt new file mode 100644 index 0000000000..9c93bd824a --- /dev/null +++ b/app/src/processing/app/gradle/Debugger.kt @@ -0,0 +1,44 @@ +package processing.app.gradle + +import com.sun.jdi.Bootstrap +import com.sun.jdi.VirtualMachine +import com.sun.jdi.connect.AttachingConnector +import kotlinx.coroutines.delay +import processing.app.Messages +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource + +class Debugger { + companion object { + suspend fun connect(port: Int?): VirtualMachine? { + try { + Messages.log("Attaching to VM $port") + val connector = Bootstrap.virtualMachineManager().allConnectors() + .firstOrNull { it.name() == "com.sun.jdi.SocketAttach" } + as AttachingConnector? + ?: throw IllegalStateException("No socket attach connector found") + val args = connector.defaultArguments() + args["port"]?.setValue(port?.toString() ?: "5005") + + // Try to attach the debugger, retrying if it fails + // TODO: Stop retrying after the job has been cancelled / failed + val start = TimeSource.Monotonic.markNow() + while (start.elapsedNow() < 10.seconds) { + try { + val sketch = connector.attach(args) + sketch.resume() + Messages.log("Attached to VM: ${sketch.name()}") + return sketch + } catch (e: Exception) { + Messages.log("Error while attaching to VM: ${e.message}... Retrying") + } + delay(250) + } + } catch (e: Exception) { + Messages.log("Error while attaching to VM: ${e.message}") + return null + } + return null + } + } +} \ No newline at end of file diff --git a/app/src/processing/app/gradle/Exceptions.kt b/app/src/processing/app/gradle/Exceptions.kt new file mode 100644 index 0000000000..c913ef4f8b --- /dev/null +++ b/app/src/processing/app/gradle/Exceptions.kt @@ -0,0 +1,99 @@ +package processing.app.gradle + +import com.sun.jdi.Location +import com.sun.jdi.StackFrame +import com.sun.jdi.VirtualMachine +import com.sun.jdi.event.ExceptionEvent +import com.sun.jdi.request.EventRequest +import kotlinx.coroutines.delay +import processing.app.Messages +import processing.app.SketchException +import processing.app.ui.Editor + +// TODO: Consider adding a panel to the footer +class Exceptions (val vm: VirtualMachine, val editor: Editor?) { + suspend fun listen() { + try { + val manager = vm.eventRequestManager() + + val request = manager.createExceptionRequest(null, false, true) + request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD) + request.enable() + + val queue = vm.eventQueue() + while (true) { + val eventSet = queue.remove() + for (event in eventSet) { + if (event is ExceptionEvent) { + printExceptionDetails(event) + event.thread().resume() + } + } + eventSet.resume() + delay(10) + } + } catch (e: Exception) { + Messages.log("Error while listening for exceptions: ${e.message}") + } + } + + fun printExceptionDetails(event: ExceptionEvent) { + val exception = event.exception() + val thread = event.thread() + val location = event.location().mapToPdeFile() + val stackFrames = thread.frames() + + val (processingFrames, userFrames) = stackFrames + .map{ + val location = it.location().mapToPdeFile() + val method = location.method() + it to "${method.declaringType().name()}.${method.name()}() @ ${location.sourcePath()}:${location.lineNumber()}" + } + .partition { + it.first.location().declaringType().name().startsWith("processing.") + } + + /* + We have 6 lines by default within the editor to display more information about the exception. + */ + // TODO: Improve the display and clarity of the exception details + + val message = """ + In Processing code: + #processingFrames + + In your code: + #userFrames + + """ + .trimIndent() + .replace("#processingFrames", processingFrames.joinToString("\n ") { it.second }) + .replace("#userFrames", userFrames.joinToString("\n ") { it.second }) + + val error = """ + Exception: ${exception.referenceType().name()} @ ${location.sourcePath()}:${location.lineNumber()} + """.trimIndent() + + println(message) + System.err.println(error) + + editor?.statusError(exception.referenceType().name()) + } + + fun Location.mapToPdeFile(): Location { + if(editor == null) return this + + // Check if the source is a .java file + val sketch = editor.sketch + sketch.code.forEach { code -> + if(code.extension != "java") return@forEach + if(sourceName() != code.fileName) return@forEach + return@mapToPdeFile this + } + + // TODO: Map to .pde file again, @see JavaBuild.placeException + // TODO: This functionality should be provided by the mode + + return this + } +} \ No newline at end of file diff --git a/app/src/processing/app/gradle/GradleJob.kt b/app/src/processing/app/gradle/GradleJob.kt new file mode 100644 index 0000000000..8c23fff69e --- /dev/null +++ b/app/src/processing/app/gradle/GradleJob.kt @@ -0,0 +1,444 @@ +package processing.app.gradle + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import com.sun.jdi.VirtualMachine +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.gradle.tooling.BuildCancelledException +import org.gradle.tooling.BuildLauncher +import org.gradle.tooling.GradleConnector +import org.gradle.tooling.events.ProgressListener +import org.gradle.tooling.events.problems.ProblemEvent +import org.gradle.tooling.events.problems.Severity +import org.gradle.tooling.events.problems.internal.DefaultSingleProblemEvent +import org.gradle.tooling.events.task.TaskFinishEvent +import org.gradle.tooling.events.task.TaskStartEvent +import org.gradle.tooling.events.task.TaskSuccessResult +import processing.app.Base.DEBUG +import processing.app.Base.getSketchbookFolder +import processing.app.Base.getVersionName +import processing.app.Language.text +import processing.app.Messages +import processing.app.Platform +import processing.app.Platform.getContentFile +import processing.app.Platform.getSettingsFolder +import processing.app.Settings +import processing.app.Sketch +import processing.app.gradle.Log.Companion.startLogServer +import processing.app.ui.Editor +import processing.app.ui.EditorStatus +import java.nio.file.Path +import kotlin.io.path.deleteIfExists +import kotlin.io.path.writeText +import kotlin.text.split + +/* +* The gradle job runs the gradle tasks and manages the gradle connection + */ +class GradleJob( + vararg val tasks: String, + val workingDir: Path, + val sketch: Sketch, + val editor: Editor? = null, +){ + enum class State{ + NONE, + BUILDING, + RUNNING, + ERROR, + DONE + } + + val debugPort = (30_000..60_000).random() + val logPort = debugPort + 1 + val errPort = logPort + 1 + + val state = mutableStateOf(State.NONE) + val vm = mutableStateOf(null) + val problems = mutableStateListOf() + val jobs = mutableStateListOf() + + private val scope = CoroutineScope(Dispatchers.IO) + private val cancel = GradleConnector.newCancellationTokenSource() + + + /* + Set up the gradle build launcher with the necessary configuration + This includes setting the working directory, the tasks to run, + and the arguments to pass to gradle. + Create the necessary build files if they do not exist. + */ + private fun BuildLauncher.setupGradle(extraArguments: List = listOf()) { + + val copy = sketch.isReadOnly || sketch.isUntitled + + val sketchFolder = if(copy) workingDir.resolve("sketch").toFile() else sketch.folder + + if(copy){ + // If the sketch is read-only, we copy it to the working directory + // This allows us to run the sketch without modifying the original files + sketch.folder.copyRecursively(sketchFolder, overwrite = true) + } + // Save the unsaved code into the working directory for gradle to compile + val unsaved = sketch.code + .map { code -> + val file = workingDir.resolve("unsaved/${code.fileName}") + file.parent.toFile().mkdirs() + // If tab is marked modified save it to the working directory + // Otherwise delete the file so we don't compile with old code + if(code.isModified){ + file.writeText(code.documentText) + }else{ + file.deleteIfExists() + } + return@map code.fileName + } + // Collect the variables to pass to gradle + val variables = mapOf( + "group" to System.getProperty("processing.group", "org.processing"), + "version" to getVersionName(), + "sketchFolder" to sketchFolder, + "sketchbook" to getSketchbookFolder(), + "workingDir" to workingDir.toAbsolutePath().toString(), + "settings" to getSettingsFolder().absolutePath.toString(), + "unsaved" to unsaved.joinToString(","), + "debugPort" to debugPort.toString(), + "logPort" to logPort.toString(), + "errPort" to errPort.toString(), + "fullscreen" to System.getProperty("processing.fullscreen", "false").equals("true"), + "display" to 1, // TODO: Implement + "external" to true, + "location" to null, // TODO: Implement + "editor.location" to editor?.location?.let { "${it.x},${it.y}" }, + //"awt.disable" to false, + //"window.color" to "0xFF000000", // TODO: Implement + //"stop.color" to "0xFF000000", // TODO: Implement + "stop.hide" to false, // TODO: Implement + ) + val repository = getContentFile("repository").absolutePath.replace("""\""", """\\""") + // Create the init.gradle.kts file in the working directory + // This allows us to run the gradle plugin that has been bundled with the editor + // TODO: Add the plugin repositories if they are defined + // TODO: Feedback when the JDK is being downloaded + val initGradle = workingDir.resolve("init.gradle.kts").apply { + val content = """ + beforeSettings{ + pluginManagement { + plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" + } + repositories { + maven("$repository") + gradlePluginPortal() + } + } + } + allprojects{ + repositories { + maven("$repository") + mavenCentral() + } + } + """.trimIndent() + + writeText(content) + } + // Create the build.gradle.kts file in the sketch folder + val buildGradle = sketchFolder.resolve("build.gradle.kts") + val generate = buildGradle.let { + if(!it.exists()) return@let true + + val contents = it.readText() + if(!contents.contains("@processing-auto-generated")) return@let false + + val version = contents.substringAfter("version=").substringBefore("\n") + if(version != getVersionName()) return@let true + + val modeTitle = contents.substringAfter("mode=").substringBefore(" ") + if(sketch.mode.title != modeTitle) return@let true + + return@let DEBUG + } + if (generate) { + Messages.log("build.gradle.kts outdated or not found in ${sketch.folder}, creating one") + val header = """ + // @processing-auto-generated mode=${sketch.mode.title} version=${getVersionName()} + // + """.trimIndent() + + val instructions = text("gradle.instructions") + .split("\n") + .joinToString("\n") { "// $it" } + + val enabledPlugins = mutableListOf(GradlePlugin( + "Processing Java", + "The Processing Java mode for Gradle", + null, + "org.processing.java", + getVersionName() + )) + val propertiesFile = sketchFolder.resolve(Sketch.PROPERTIES_NAME) + if(propertiesFile.exists()){ + val sketchSettings = Settings(propertiesFile) + + // Grab the installed plugins + val plugins = GradlePlugin.plugins + + // Grab the enabled plugins + val pluginSetting = (sketchSettings.get(GradlePlugin.PROPERTIES_KEY) ?: "") + .split(",") + .map { it.trim() } + .filter{ it.isNotEmpty() } + + // Link plugins in the settings to their installed counterparts + enabledPlugins.addAll( + pluginSetting + .mapNotNull { id -> + plugins.find { plugin -> plugin.id == id + } + } + ) + } + + val pluginList = enabledPlugins + .joinToString("\n ") { "id(\"${it.id}\") version \"${it.version}\"" } + + val configuration = """ + plugins{ + #plugins + } + """.trimIndent().replace("#plugins", pluginList) + val content = "${header}\n${instructions}\n\n${configuration}" + buildGradle.writeText(content) + } + // Create and empty settings.gradle.kts file in the sketch folder + val settingsGradle = sketchFolder.resolve("settings.gradle.kts") + if (!settingsGradle.exists()) { + settingsGradle.createNewFile() + } + // Collect the arguments to pass to gradle + val arguments = mutableListOf("--init-script", initGradle.toAbsolutePath().toString()) + // Hide Gradle output from the console if not in debug mode + if(!DEBUG) arguments += "--quiet" + if(copy) arguments += listOf("--project-dir", sketchFolder.absolutePath) + + arguments += variables.entries + .filter { it.value != null } + .map { "-Pprocessing.${it.key}=${it.value}" } + + arguments += extraArguments + + withArguments(*arguments.toTypedArray()) + + forTasks(*tasks) + + // TODO: Instead of shipping Processing with a build-in JDK we should download the JDK through Gradle + setJavaHome(Platform.getJavaHome()) + withCancellationToken(cancel.token()) + } + + /* + Start the gradle job and run the tasks + */ + fun start() { + launchJob { + handleExceptions { + state.value = State.BUILDING + + // Connect Gradle, configure the build and run it + GradleConnector.newConnector() + .forProjectDirectory(sketch.folder) + .apply { + editor?.statusMessage("Connecting to Gradle", EditorStatus.NOTICE) + // TODO: Remove when switched to classic confinement within Snap + if (System.getenv("SNAP_USER_COMMON") != null) { + useGradleUserHomeDir(getSettingsFolder().resolve("gradle")) + } + } + .connect() + .apply { + editor?.statusMessage("Building sketch", EditorStatus.NOTICE) + } + .newBuild() + .apply { + if (DEBUG) { + setStandardOutput(System.out) + setStandardError(System.err) + } + + setupGradle() + + addStateListener() + addLogserver() + addDebugging() + + } + .run() + } + } + } + + + /* + Cancel the gradle job and all the jobs that were launched in this scope + */ + fun cancel(){ + cancel.cancel() + jobs.forEach(Job::cancel) + } + + /* + Add a job to the scope and add it to the list of jobs so we can cancel it later + */ + private fun launchJob(block: suspend CoroutineScope.() -> Unit){ + val job = scope.launch { block() } + jobs.add(job) + } + + + /* + Handle exceptions that occur during the build process and inform the user about them + */ + private fun handleExceptions(action: () -> Unit){ + try{ + action() + }catch (e: Exception){ + val causesList = mutableListOf() + var cause: Throwable? = e + + while (cause != null && cause.cause != cause) { + causesList.add(cause) + cause = cause.cause + } + + val errors = causesList.joinToString("\n") { it.message ?: "Unknown error" } + + val skip = listOf(BuildCancelledException::class) + + if (skip.any { it.isInstance(e) }) { + Messages.log("Gradle job error: $errors") + return + } + + if(state.value == State.RUNNING){ + Messages.log("Gradle job error: $errors") + return + } + + // An error occurred during the build process + + System.err.println(errors) + editor?.statusError(causesList.last().message) + }finally { + state.value = State.DONE + vm.value = null + } + } + + // TODO: Move to separate file? + /* + Add a progress listener to the build launcher + to track the progress of the build and update the editor status accordingly + */ + private fun BuildLauncher.addStateListener(){ + addProgressListener(ProgressListener { event -> + if(event is TaskStartEvent) { + editor?.statusMessage("Running task: ${event.descriptor.name}", EditorStatus.NOTICE) + when(event.descriptor.name) { + ":run" -> { + state.value = State.RUNNING + Messages.log("Start run") + editor?.toolbar?.activateRun() + } + } + + } + if(event is TaskFinishEvent) { + if(event.result is TaskSuccessResult){ + editor?.statusMessage("Finished task ${event.descriptor.name}", EditorStatus.NOTICE) + } + + when(event.descriptor.name){ + ":run"->{ + state.value = State.DONE + editor?.toolbar?.deactivateRun() + editor?.toolbar?.deactivateStop() + } + } + } + if(event is DefaultSingleProblemEvent) { + + + problems.add(event) + + val skip = listOf( + "mutating-the-dependencies-of-configuration-implementation-after-it-has-been-resolved-or-consumed-this-behavior-has-been-deprecated", + "mutating-the-dependencies-of-configuration-runtimeonly-after-it-has-been-resolved-or-consumed-this-behavior-has-been-deprecated" + ) + if(skip.any { event.definition.id.name.contains(it) }) { + Messages.log(event.toString()) + return@ProgressListener + } + + if(event.definition.severity == Severity.ADVICE) { + Messages.log(event.toString()) + return@ProgressListener + } + // TODO: Show the error on the location if it is available + // TODO: This functionality should be provided by the mode + /* + We have 6 lines to display the error in the editor. + */ + + val error = event.definition.id.displayName + editor?.statusError(error) + System.err.println("Problem: $error") + state.value = State.ERROR + + val message = """ + Context: ${event.contextualLabel.contextualLabel} + Solutions: ${event.solutions.joinToString("\n\t") { it.solution }} + """ + .trimIndent() + + println(message) + } + }) + } + + /* + Start log servers for the standard output and error streams + This allows us to capture the output of Processing and display it in the editor + Whilst keeping the gradle output separate + */ + fun BuildLauncher.addLogserver(){ + launchJob { + startLogServer(logPort, System.out) + } + launchJob{ + startLogServer(errPort, System.err) + } + } + + /* + Connected a debugger to the gradle run task + This allows us to debug the sketch while it is running + */ + fun BuildLauncher.addDebugging() { + addProgressListener(ProgressListener { event -> + if (event !is TaskStartEvent) return@ProgressListener + if (event.descriptor.name != ":run") return@ProgressListener + + launchJob { + val debugger = Debugger.connect(debugPort) ?: return@launchJob + vm.value = debugger + val exceptions = Exceptions(debugger, editor) + exceptions.listen() + } + + }) + } +} \ No newline at end of file diff --git a/app/src/processing/app/gradle/GradlePlugin.kt b/app/src/processing/app/gradle/GradlePlugin.kt new file mode 100644 index 0000000000..798dd97936 --- /dev/null +++ b/app/src/processing/app/gradle/GradlePlugin.kt @@ -0,0 +1,20 @@ +package processing.app.gradle + +import androidx.compose.runtime.mutableStateListOf +import processing.app.Base +import java.nio.file.Path + +data class GradlePlugin( + val name: String, + val description: String, + val repository: Path?, + val id: String, + val version: String){ + companion object{ + const val PROPERTIES_KEY = "sketch.plugins" + val plugins = mutableStateListOf( + GradlePlugin("Hot Reload", "Automatically apply changes in your sketch upon saving", null, "org.processing.java.hotreload", Base.getVersionName()), + GradlePlugin("Android","Run your sketch on an Android device", null, "org.processing.android", Base.getVersionName()), + ) + } +} \ No newline at end of file diff --git a/app/src/processing/app/gradle/GradleService.kt b/app/src/processing/app/gradle/GradleService.kt new file mode 100644 index 0000000000..34d0498a27 --- /dev/null +++ b/app/src/processing/app/gradle/GradleService.kt @@ -0,0 +1,121 @@ +package processing.app.gradle + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.neverEqualPolicy +import androidx.compose.ui.awt.ComposePanel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import processing.app.Language.text +import processing.app.Mode +import processing.app.Preferences +import processing.app.Sketch +import processing.app.ui.Editor +import processing.app.ui.Theme +import kotlin.io.path.createTempDirectory + +// TODO: Highlight errors in the editor in the right place + +// TODO: ---- FUTURE ---- +// TODO: Improve progress tracking and show it in the UI +// TODO: PoC new debugger/tweak mode +// TODO: Track build speed (for analytics?) +// TODO: Bundle Gradle with the app + +/* +* The gradle service runs the gradle tasks and manages the gradle connection +* It will create the necessary build files for gradle to run +* Then it will kick off a new GradleJob to run the tasks +* GradleJob manages the gradle build and connects the debugger +*/ +class GradleService( + val mode: Mode, + val editor: Editor?, +) { + val active = mutableStateOf(Preferences.getBoolean("run.use_gradle")) + var sketch = mutableStateOf(null, neverEqualPolicy()) + val jobs = mutableStateListOf() + val workingDir = createTempDirectory() + + fun run(){ + startJob("run") + } + + fun export(){ + startJob("runDistributable") + } + + fun stop(){ + stopJobs() + } + + private fun startJob(vararg tasks: String) { + if(!active.value) return + editor?.let { println(text("gradle.using_gradle")) } + + val job = GradleJob( + tasks = tasks, + workingDir = workingDir, + sketch = sketch.value ?: throw IllegalStateException("Sketch is not set"), + editor = editor + ) + jobs.add(job) + job.start() + } + + private fun stopJobs(){ + jobs.forEach(GradleJob::cancel) + } + + private val scope = CoroutineScope(Dispatchers.IO) + + /* + Watch the sketch folder for changes and start a build job when the sketch is modified + This need to be done properly to use hooks in the future but right now this is the simplest way to do it + */ + init{ + scope.launch { + var path = "" + var modified = false + var sketched: Sketch? = null + while(true){ + sketch.value?.let { sketch -> + if(sketch.folder.absolutePath != path){ + path = sketch.folder.absolutePath + if(sketched == sketch){ + // The same sketch has its folder changed, trigger updates downstream from the service + this@GradleService.sketch.value = sketch + }else { + sketched = sketch + } + startJob("build") + } + if(sketch.isModified != modified){ + modified = sketch.isModified + if(!modified){ + // If the sketch is no longer modified, start the build job, aka build on save + startJob("build") + } + } + } + + + delay(100) + } + } + } + + // Hooks for java to interact with the Gradle service since mutableStateOf is not accessible in java + fun setSketch(sketch: Sketch){ + this.sketch.value = sketch + } + fun getEnabled(): Boolean { + return active.value + } + fun setEnabled(active: Boolean) { + if(!active) stopJobs() + this.active.value = active + } +} \ No newline at end of file diff --git a/app/src/processing/app/gradle/GradleSettings.kt b/app/src/processing/app/gradle/GradleSettings.kt new file mode 100644 index 0000000000..8eebd7699d --- /dev/null +++ b/app/src/processing/app/gradle/GradleSettings.kt @@ -0,0 +1,195 @@ +package processing.app.gradle + +import androidx.compose.foundation.VerticalScrollbar +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Checkbox +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposePanel +import androidx.compose.ui.awt.SwingPanel +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import com.formdev.flatlaf.util.SwingUtils +import com.github.ajalt.mordant.rendering.TextStyle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import processing.app.Language.text +import processing.app.Settings +import processing.app.Sketch +import processing.app.ui.Editor +import processing.app.ui.EditorFooter +import processing.app.ui.Theme +import processing.app.ui.theme.ProcessingTheme +import processing.app.watchFile +import java.awt.Dimension +import java.util.UUID +import javax.swing.JCheckBox +import javax.swing.JPanel +import javax.swing.SwingUtilities + +class GradleSettings{ + companion object{ + private val scope = CoroutineScope(Dispatchers.IO) + + @JvmStatic + fun addGradleSettings(footer: EditorFooter, service: GradleService){ + val panel = ComposePanel() + panel.setContent { + Panel(service) + } + scope.launch { + // Only add the panel to the footer when Gradle is active + // Can be removed later when Gradle becomes the default build system + snapshotFlow { service.active.value } + .collect { active -> + SwingUtilities.invokeLater { + if(active){ + footer.addPanel(panel, text("gradle.settings"), "/lib/footer/settings") + }else{ + footer.removePanel(panel) + } + } + } + } + } + + @Composable + fun Panel(service: GradleService){ + val properties = service.sketch.value?.folder?.resolve(Sketch.PROPERTIES_NAME) ?: return + // TODO: Rewatch again is the sketch is saved in a different location + + val changed = watchFile(properties) + + val settings = remember(changed) {Settings(properties) } + + LaunchedEffect(changed){ + /* + If the sketch.id is not set, generate a new UUID and save it. + We will use this key to save preferences that do not influence the sketch itself, + so they are not code, but do influence how the sketch shows up in the editor. + This is useful for things like favoring a sketch + These are items that should not be shared between users/computers + // TODO: Reset id on save-as? + */ + if(settings.get("sketch.id") == null){ + // TODO: Should this watch the file or should it update a bunch on running the sketch? + settings.set("sketch.id", UUID.randomUUID().toString()) + settings.save() + } + } + val stateVertical = rememberScrollState(0) + + ProcessingTheme { + Box { + Row( + modifier = Modifier + .background(Color(Theme.getColor("editor.line.highlight.color").rgb)) + .padding(start = Editor.LEFT_GUTTER.dp) + .fillMaxSize() + .verticalScroll(stateVertical) + .padding(vertical = 4.dp) + ) { + PluginsPanel(settings) + } + VerticalScrollbar( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(8.dp) + .fillMaxHeight(), + adapter = rememberScrollbarAdapter(stateVertical) + ) + } + } + } + + @Composable + private fun PluginsPanel(settings: Settings) { + // Grab the installed plugins + val plugins = GradlePlugin.plugins + + // Grab the enabled plugins + val pluginSetting = (settings.get(GradlePlugin.PROPERTIES_KEY) ?: "") + .split(",") + .map { it.trim() } + .filter{ it.isNotEmpty() } + + // Link plugins in the settings to their installed counterparts + val enabledPlugins = pluginSetting + .map { id -> plugins.find { plugin -> plugin.id == id } } + Column { + Text( + text = text("gradle.settings.plugins"), + textAlign = TextAlign.Start, + fontSize = 10.sp, + fontWeight = FontWeight.Bold + ) + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + GradlePlugin.plugins.map { plugin -> + Row() { + Checkbox( + checked = enabledPlugins.contains(plugin), + modifier = Modifier + .padding(start = 0.dp, end = 8.dp) + .size(24.dp), + onCheckedChange = { checked -> + scope.launch { + // Work from the setting as we do not want to remove missing plugins + val current = pluginSetting.toMutableSet() + if (checked) { + current.add(plugin.id) + } else { + current.remove(plugin.id) + } + settings.set(GradlePlugin.PROPERTIES_KEY, current.joinToString(",")) + settings.save() + } + }, + ) + Column { + Text( + text = plugin.name, + textAlign = TextAlign.Start, + fontSize = 12.sp + ) + Text( + text = plugin.description, + textAlign = TextAlign.Start, + fontSize = 10.sp, + ) + } + } + + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/processing/app/gradle/Log.kt b/app/src/processing/app/gradle/Log.kt new file mode 100644 index 0000000000..30ca070cf6 --- /dev/null +++ b/app/src/processing/app/gradle/Log.kt @@ -0,0 +1,30 @@ +package processing.app.gradle + +import processing.app.Messages +import java.io.PrintStream +import java.net.ServerSocket + +class Log{ + companion object{ + fun startLogServer(port: Int, target: PrintStream){ + val server = ServerSocket(port) + Messages.Companion.log("Log server started on port $port") + val client = server.accept() + Messages.Companion.log("Log server client connected") + + val reader = client.getInputStream().bufferedReader() + try { + reader.forEachLine { line -> + if (line.isNotBlank()) { + target.println(line) + } + } + } catch (e: Exception) { + Messages.Companion.log("Error while reading from log server: ${e.message}") + } finally { + client.close() + server.close() + } + } + } +} diff --git a/app/src/processing/app/gradle/api/Sketch.kt b/app/src/processing/app/gradle/api/Sketch.kt new file mode 100644 index 0000000000..635094ec83 --- /dev/null +++ b/app/src/processing/app/gradle/api/Sketch.kt @@ -0,0 +1,66 @@ +package processing.app.gradle.api + +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import processing.app.Base +import processing.app.Platform +import processing.app.Preferences +import processing.app.contrib.ModeContribution +import processing.app.gradle.GradleJob +import processing.app.gradle.GradleService + +class Sketch : SuspendingCliktCommand("sketch") { + init { + subcommands( + Run() + ) + } + + override fun help(context: Context): String { + return """Manage sketches in the Processing environment.""" + } + + override suspend fun run() { + System.setProperty("java.awt.headless", "true") + } + + class Run : SuspendingCliktCommand(name = "run") { + val sketch by option("--sketch", help = "The sketch to run") + .required() + + val mode by option("--mode", help = "The mode to use for running the sketch (only java is supported for now)") + + override fun help(context: Context): String { + return "Run the Processing sketch." + } + + override suspend fun run() { + Base.setCommandLine() + Platform.init() + Preferences.init() + Base.locateSketchbookFolder() + + // TODO: Support modes other than Java + val mode = ModeContribution.load( + null, Platform.getContentFile("modes/java"), + "processing.mode.java.JavaMode" + ).mode ?: throw IllegalStateException("Java mode not found") + + System.setProperty("java.awt.headless", "false") + + val service = GradleService(mode,null) + service.sketch.value = processing.app.Sketch(sketch, mode) + service.run() + + // TODO: Use an async way to wait for the job to finish + //Wait for the service to finish + while (service.jobs.any { it.state.value != GradleJob.State.DONE }) { + Thread.sleep(100) + } + } + } +} \ No newline at end of file diff --git a/app/src/processing/app/ui/Editor.java b/app/src/processing/app/ui/Editor.java index d710890a95..1cbb5f1319 100644 --- a/app/src/processing/app/ui/Editor.java +++ b/app/src/processing/app/ui/Editor.java @@ -48,7 +48,6 @@ import javax.swing.text.html.*; import javax.swing.undo.*; -import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.util.SystemInfo; import processing.app.Base; import processing.app.Formatter; @@ -63,6 +62,7 @@ import processing.app.SketchCode; import processing.utils.SketchException; import processing.app.contrib.ContributionManager; +import processing.app.gradle.GradleService; import processing.app.laf.PdeMenuItemUI; import processing.app.syntax.*; import processing.core.*; @@ -75,6 +75,7 @@ public abstract class Editor extends JFrame implements RunnerListener { protected Base base; protected EditorState state; protected Mode mode; + protected GradleService service; // There may be certain gutter sizes that cause text bounds // inside the console to be calculated incorrectly. @@ -157,6 +158,7 @@ protected Editor(final Base base, String path, final EditorState state, this.base = base; this.state = state; this.mode = mode; + this.service = new GradleService(this.mode,this); // Make sure Base.getActiveEditor() never returns null base.checkFirstEditor(this); @@ -389,6 +391,9 @@ public EditorFooter createFooter() { return ef; } + public EditorFooter getFooter() { + return footer; + } public void addErrorTable(EditorFooter ef) { JScrollPane scrollPane = new JScrollPane(); @@ -478,6 +483,9 @@ public Mode getMode() { return mode; } + public GradleService getService() { + return service; + } public void repaintHeader() { header.repaint(); @@ -588,6 +596,7 @@ protected int getDividerLocation() { * with things in the Preferences window. */ public void applyPreferences() { + service.setEnabled(Preferences.getBoolean("run.use_gradle")); // Even though this is only updating the theme (colors, icons), // subclasses use this to apply other preferences. // For instance, Java Mode applies changes to error checking. @@ -2265,6 +2274,7 @@ protected void handleOpenInternal(String path) throws EditorException { } catch (IOException e) { throw new EditorException("Could not create the sketch.", e); } + service.setSketch(sketch); header.rebuild(); updateTitle(); diff --git a/app/src/processing/app/ui/EditorConsole.java b/app/src/processing/app/ui/EditorConsole.java index c8c40ee487..f49283823f 100644 --- a/app/src/processing/app/ui/EditorConsole.java +++ b/app/src/processing/app/ui/EditorConsole.java @@ -254,7 +254,14 @@ private boolean suppressMessage(String what, boolean err) { // "java.lang.NoSuchMethodError: accessibilityHitTest" // https://github.com/processing/processing4/issues/368 return true; + } else if (what.contains("__MOVE__")) { + // Don't display the "Move" message that is used to position the sketch window + return true; + }else if (what.startsWith("SLF4J: ")) { + // Don't display the SLF4J messages + return true; } + } else { // !err if (what.contains("Listening for transport dt_socket at address")) { // Message from the JVM about the socket launch for debug diff --git a/app/src/processing/app/ui/EditorFooter.java b/app/src/processing/app/ui/EditorFooter.java index 276b78364c..b1d710cb52 100644 --- a/app/src/processing/app/ui/EditorFooter.java +++ b/app/src/processing/app/ui/EditorFooter.java @@ -152,13 +152,19 @@ public void addPanel(Component comp, String name) { public void addPanel(Component comp, String name, String icon) { tabs.add(new Tab(comp, name, icon)); cardPanel.add(name, comp); + repaint(); } - -// public void setPanel(int index) { -// cardLayout.show(cardPanel, tabs.get(index).name); -// } - + /** + * Remove a panel from the footer. + * @param comp Component that links to this tab. + * */ + public void removePanel(Component comp){ + cardLayout.show(cardPanel, tabs.get(0).title); + tabs.removeIf(tab -> tab.comp == comp); + cardPanel.remove(comp); + repaint(); + } public void setPanel(Component comp) { for (Tab tab : tabs) { diff --git a/app/src/processing/app/ui/PreferencesFrame.java b/app/src/processing/app/ui/PreferencesFrame.java index a8cf68c27d..28424a2ea3 100644 --- a/app/src/processing/app/ui/PreferencesFrame.java +++ b/app/src/processing/app/ui/PreferencesFrame.java @@ -85,6 +85,7 @@ public class PreferencesFrame { JCheckBox hidpiDisableBox; // JLabel hidpiRestartLabel; JCheckBox syncSketchNameBox; + JCheckBox useModernBuildSystem; JComboBox displaySelectionBox; JComboBox languageSelectionBox; @@ -554,6 +555,9 @@ public void mouseExited(MouseEvent e) { runningPanel.setBorder(new TitledBorder("Running")); runningPanel.setLayout(new BoxLayout(runningPanel, BoxLayout.Y_AXIS)); + useModernBuildSystem = new JCheckBox(Language.text("preferences.use_modern_build_system")); + addRow(runningPanel, useModernBuildSystem); + addRow(runningPanel, displayLabel, displaySelectionBox); addRow(runningPanel, backgroundColorLabel, presentColor); addRow(runningPanel, memoryOverrideBox, memoryField, mbLabel); @@ -827,6 +831,8 @@ protected void applyFrame() { Preferences.setBoolean("pdex.completion", codeCompletionBox.isSelected()); Preferences.setBoolean("pdex.suggest.imports", importSuggestionsBox.isSelected()); + Preferences.setBoolean("run.use_gradle", useModernBuildSystem.isSelected()); + for (Editor editor : base.getEditors()) { editor.applyPreferences(); } @@ -902,6 +908,11 @@ public void showFrame() { if (autoAssociateBox != null) { autoAssociateBox.setSelected(Preferences.getBoolean("platform.auto_file_type_associations")); //$NON-NLS-1$ } + + if(useModernBuildSystem != null) { + useModernBuildSystem.setSelected(Preferences.getBoolean("run.use_gradle")); + } + // The OK Button has to be set as the default button every time the // PrefWindow is to be displayed frame.getRootPane().setDefaultButton(okButton); diff --git a/app/test/processing/app/gradle/GradleServiceTest.kt b/app/test/processing/app/gradle/GradleServiceTest.kt new file mode 100644 index 0000000000..64a04d447c --- /dev/null +++ b/app/test/processing/app/gradle/GradleServiceTest.kt @@ -0,0 +1,13 @@ +package processing.app.gradle + +import org.junit.jupiter.api.Assertions.* +import processing.app.ui.Editor +import kotlin.test.Test +import org.mockito.kotlin.mock + +class GradleServiceTest{ + + @Test + fun testRunningSketch(){ + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 0675c2db38..bc23e286b0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,4 +8,13 @@ plugins { // Set the build directory to not /build to prevent accidental deletion through the clean action // Can be deleted after the migration to Gradle is complete -layout.buildDirectory = file(".build") \ No newline at end of file +layout.buildDirectory = file(".build") + +allprojects{ + tasks.withType { + options.encoding = "UTF-8" + } + tasks.withType { + options.encoding = "UTF-8" + } +} \ No newline at end of file diff --git a/build/shared/lib/defaults.txt b/build/shared/lib/defaults.txt index 1cfc190ca9..a91ccad1ed 100644 --- a/build/shared/lib/defaults.txt +++ b/build/shared/lib/defaults.txt @@ -190,6 +190,9 @@ console.temp.days = 7 console.scrollback.lines = 500 console.scrollback.chars = 40000 +# run java sketches with Gradle aka the Modern Build System +run.use_gradle = false + # Any additional Java options when running. # If you change this and can't run things, it's your own durn fault. run.options = diff --git a/build/shared/lib/footer/settings.svg b/build/shared/lib/footer/settings.svg new file mode 100644 index 0000000000..25a9736fff --- /dev/null +++ b/build/shared/lib/footer/settings.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index 2f1946b795..6e101f402a 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -236,6 +236,7 @@ preferences.launch_programs_in = Launch programs in preferences.launch_programs_in.mode = mode preferences.file = More preferences can be edited directly in the file: preferences.file.hint = (Edit only when Processing is not running.) +preferences.use_modern_build_system = Use modern build system (see Processing GitHub Wiki more details) # Sketchbook Location (Frame) sketchbook_location = Select new sketchbook folder @@ -323,6 +324,13 @@ debugger.name = Name debugger.value = Value debugger.type = Type +# Gradle +gradle.instructions = About this file: \nProcessing creates this file when you run your sketch. \nIt configures the tools needed to build and export your code. \nLearn more: [Gradle Primer link]\n \nTo customize this file: \n1. Delete the line above that begins with '@processing-auto-generated'. \nThis will prevent Processing from overwriting this file in the future. \n2. Make your desired changes. +gradle.using_gradle = Building sketch using the new build system. (See settings to switch to the legacy build system.) +gradle.using_eclipse = Building sketch using the legacy build system. (See settings to switch to the new build system.) +gradle.settings = Settings +gradle.settings.plugins = Plugins (experimental) + # --------------------------------------- # Toolbars @@ -625,6 +633,24 @@ update_check = Update update_check.updates_available.core = A new version of Processing is available,\nwould you like to visit the Processing download page? update_check.updates_available.contributions = There are updates available for some of the installed contributions,\nwould you like to open the the Contribution Manager now? + +# --------------------------------------- +# Welcome +welcome.intro.title = Welcome to Processing +welcome.intro.message = A flexible software sketchbook and a language for learning how to code. +welcome.intro.suggestion = Is it your first time using Processing? Try one of the examples on the right. +welcome.action.examples = More examples +welcome.action.tutorials = Tutorials +welcome.action.startup = Show this window at startup +welcome.action.go = Let's go! + +# --------------------------------------- +# Beta +beta.window.title = Welcome to Beta +beta.title = Welcome to the Processing Beta +beta.message = Thank you for trying out the new version of Processing. We're very grateful!\n\nPlease report any bugs on the forums. +beta.button = Got it! + # --------------------------------------- # Beta beta.window.title = Welcome to Beta diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 8f7211b131..f646bc9b8b 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -15,6 +15,7 @@ sourceSets{ main{ java{ srcDirs("src") + exclude("**/*.jnilib") } resources{ srcDirs("src") @@ -34,10 +35,21 @@ dependencies { testImplementation(libs.junit) } +publishing{ + repositories{ + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} mavenPublishing{ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) - signAllPublications() + + // Only sign if signing is set up + if(project.hasProperty("signing.keyId") || project.hasProperty("signing.signingInMemoryKey")) + signAllPublications() pom{ name.set("Processing Core") @@ -77,3 +89,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 d1297ec6fb..5187ff5c0a 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 @@ -9932,19 +9932,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/gradle/libs.versions.toml b/gradle/libs.versions.toml index dfacae1ead..236949d8c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ kotlin = "2.0.20" compose-plugin = "1.7.1" jogl = "2.5.0" +antlr = "4.13.2" jupiter = "5.12.0" [libraries] @@ -25,6 +26,11 @@ netbeansSwing = { module = "org.netbeans.api:org-netbeans-swing-outline", versio ant = { module = "org.apache.ant:ant", version = "1.10.14" } lsp4j = { module = "org.eclipse.lsp4j:org.eclipse.lsp4j", version = "0.22.0" } jsoup = { module = "org.jsoup:jsoup", version = "1.17.2" } +antlr4 = { module = "org.antlr:antlr4", version.ref = "antlr" } +antlr4Runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr" } +composeGradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-plugin" } +kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotlinComposePlugin = { module = "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin", version.ref = "kotlin" } 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" } @@ -36,4 +42,5 @@ kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } download = { id = "de.undercouch.download", version = "5.6.0" } -mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.30.0" } \ No newline at end of file +mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.30.0" } +gradlePublish = { id = "com.gradle.plugin-publish", version = "1.2.1" } \ No newline at end of file diff --git a/java/README.md b/java/README.md index a4be6e9a87..0a905208d0 100644 --- a/java/README.md +++ b/java/README.md @@ -3,8 +3,9 @@ This the Java Mode in Processing. It compiles your sketches and runs them. It is the primary mode of Processing. ## Folders -- `application` assets for exporting applications within the mode -- `generated` generated antlr code for the mode, should be moved to a proper `antlr` plugin within gradle +- `application` assets for exporting applications within the mode (Deprecated) +- `generated` generated antlr code for the mode, should be moved to a proper `antlr` plugin within gradle (Deprecated) +- `gradle` the Processing java gradle plugin - `libraries` libraries that are available within the mode - `lsp` gradle build system for the language server protocol, in the future we should decouple the lsp from the java mode and pde and move all relevant code here. For now it can be found in `src/.../lsp` - `mode` legacy files for `Ant` @@ -13,8 +14,25 @@ This the Java Mode in Processing. It compiles your sketches and runs them. It is - `test` tests for the mode - `theme` assets for the mode, related to autocomplete and syntax highlighting -## Future plans -- Decouple the `lsp` and `preprocessor` from the mode and move them to their own repositories -- Move the `antlr` code to a proper plugin within gradle -- Create a gradle plugin to convert `.pde` file to `.java` files -- Create a gradle based version of Java mode. \ No newline at end of file +## The Modern Build system + +Since 2025 work has started on creating a new internal build system for the Java Mode based on Gradle. +The goal is to simplify by leaning more on Gradle, which provides a lot of the functionality that was build before out of the box and a lot more. + +### How it used to work + +The build system used to be based on some parts Ant, some parts eclipse (org.eclipse.jdt.core) and a lot of custom work build up over the years. + +### How it will work going forward + +The modern build system is based around Gradle, the main service (GradleService) for building a sketch with Gradle is included in `app` instead of into the Java mode as future modes are most likely also based on Gradle if they use `core` in some way. Most _Modes_ should/could probably be a Gradle plugin going forward. +Breaking the build system away from the java mode will mean that we create an island of isolation when it comes to the build system, allowing contributors to work on the build system without running the editor. +Another upside is that when we publish the Gradle plugin to the Gradle Plugin repository, it will become trivial to run Processing sketches outside the PDE and improvements made to the build system will be usable for everyone. +There is now also an opportunity for creating contributions that modify the build system in more subtle ways rather than having to make a complete new mode, e.g. a compilation step for shaders or some setup tweaks to make JavaFX work out of the box. +Furthermore, this change will embed Processing more into the wider Java ecosystem, if users want to upgrade from using Processing to Java whilst still using `core` that will become possible and won't need a rewrite of what they already created. + +### How to work on the modern build system + +If you want to work on the build system without the PDE, open `/java/gradle/example` into a new intellij IDEA window, this is set up to compile the Processing Java plugin and run sketches standalone. + +Within the editor, the gradle plugin is embedded in Processing's embedded maven repository so that Gradle can find it. \ No newline at end of file diff --git a/java/build.gradle.kts b/java/build.gradle.kts index 2f87d39bdd..69877d5fce 100644 --- a/java/build.gradle.kts +++ b/java/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform + plugins { id("java") } @@ -49,23 +51,33 @@ tasks.compileJava{ // Most of these are shims to be compatible with the old build system // They should be removed in the future, as we work towards making things more Gradle-native tasks.register("extraResources"){ - dependsOn(":java:copyCore") - from(".") - include("keywords.txt") - include("theme/**/*") - include("application/**/*") - into( layout.buildDirectory.dir("resources-bundled/common/modes/java")) + dependsOn("copyCore") + val os = DefaultNativePlatform.getCurrentOperatingSystem() + val platform = when { + os.isWindows -> "windows" + os.isMacOsX -> "macos" + else -> "linux" + } + from(layout.projectDirectory){ + include("keywords.txt") + include("theme/**/*") + include("application/**/*") + exclude("application/launch4j/bin/*") + } + from(layout.projectDirectory){ + include ("application/launch4j/bin/*$platform") + rename("(.*)-$platform(.*)", "$1$2") + } + into(layout.buildDirectory.dir("resources-bundled/common/modes/java")) } tasks.register("copyCore"){ val coreProject = project(":core") dependsOn(coreProject.tasks.jar) - from(coreProject.tasks.jar) { - include("core*.jar") - } + from(coreProject.tasks.jar) + include("core*.jar") rename("core.+\\.jar", "core.jar") into(coreProject.layout.projectDirectory.dir("library")) } - val libraries = arrayOf("dxf","io","net","pdf","serial","svg") libraries.forEach { library -> tasks.register("library-$library-extraResources"){ @@ -78,7 +90,7 @@ libraries.forEach { library -> include("*.properties") include("library/**/*") include("examples/**/*") - into( layout.buildDirectory.dir("resources-bundled/common/modes/java/libraries/$library")) + into(layout.buildDirectory.dir("resources-bundled/common/modes/java/libraries/$library")) } tasks.named("extraResources"){ dependsOn("library-$library-extraResources") } } diff --git a/java/gradle/build.gradle.kts b/java/gradle/build.gradle.kts new file mode 100644 index 0000000000..0171384f44 --- /dev/null +++ b/java/gradle/build.gradle.kts @@ -0,0 +1,41 @@ +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) +} + +// TODO: CI/CD for publishing the plugin to the Gradle Plugin Portal +gradlePlugin{ + plugins{ + create("processing.java"){ + id = "org.processing.java" + 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) + } + } +} \ No newline at end of file diff --git a/java/gradle/example/.idea/.gitignore b/java/gradle/example/.idea/.gitignore new file mode 100644 index 0000000000..a0ccf77bc5 --- /dev/null +++ b/java/gradle/example/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Environment-dependent path to Maven home directory +/mavenHomeManager.xml diff --git a/java/gradle/example/.idea/.name b/java/gradle/example/.idea/.name new file mode 100644 index 0000000000..fb61c9d808 --- /dev/null +++ b/java/gradle/example/.idea/.name @@ -0,0 +1 @@ +processing-gradle-plugin-demo \ No newline at end of file diff --git a/java/gradle/example/.idea/compiler.xml b/java/gradle/example/.idea/compiler.xml new file mode 100644 index 0000000000..b589d56e9f --- /dev/null +++ b/java/gradle/example/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/java/gradle/example/.idea/gradle.xml b/java/gradle/example/.idea/gradle.xml new file mode 100644 index 0000000000..ae55d4d4a2 --- /dev/null +++ b/java/gradle/example/.idea/gradle.xml @@ -0,0 +1,54 @@ + + + + + + + \ No newline at end of file diff --git a/java/gradle/example/.idea/kotlinc.xml b/java/gradle/example/.idea/kotlinc.xml new file mode 100644 index 0000000000..d4b7accbaa --- /dev/null +++ b/java/gradle/example/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/java/gradle/example/.idea/misc.xml b/java/gradle/example/.idea/misc.xml new file mode 100644 index 0000000000..5a50b6cd23 --- /dev/null +++ b/java/gradle/example/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/java/gradle/example/.idea/vcs.xml b/java/gradle/example/.idea/vcs.xml new file mode 100644 index 0000000000..c2365ab11f --- /dev/null +++ b/java/gradle/example/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ 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..17e3141d9c --- /dev/null +++ b/java/gradle/example/brightness.pde @@ -0,0 +1,34 @@ +/** + * 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; + +/* +import controlP5.*; + +ControlP5 cp5; +*/ + + +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/hotreload/build.gradle.kts b/java/gradle/hotreload/build.gradle.kts new file mode 100644 index 0000000000..e4873a8c31 --- /dev/null +++ b/java/gradle/hotreload/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + `java-gradle-plugin` + kotlin("jvm") version libs.versions.kotlin + alias(libs.plugins.gradlePublish) + +} + + +repositories { + mavenCentral() +} + +dependencies{ + implementation("org.jetbrains.compose.hot-reload:hot-reload-gradle-plugin:1.0.0-beta03") +} + +gradlePlugin{ + plugins{ + create("processing.java.hotreload"){ + id = "org.processing.java.hotreload" + implementationClass = "org.processing.java.gradle.ProcessingHotReloadPlugin" + } + } +} +publishing{ + repositories{ + mavenLocal() + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } +} \ No newline at end of file diff --git a/java/gradle/hotreload/src/main/kotlin/org/processing/java/gradle/ProcessingHotReloadPlugin.kt b/java/gradle/hotreload/src/main/kotlin/org/processing/java/gradle/ProcessingHotReloadPlugin.kt new file mode 100644 index 0000000000..4776d4ebce --- /dev/null +++ b/java/gradle/hotreload/src/main/kotlin/org/processing/java/gradle/ProcessingHotReloadPlugin.kt @@ -0,0 +1,29 @@ +package org.processing.java.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.jvm.toolchain.JvmVendorSpec +import org.jetbrains.compose.reload.gradle.ComposeHotReloadPlugin + +class ProcessingHotReloadPlugin: Plugin { + override fun apply(project: Project) { + project.plugins.apply(ComposeHotReloadPlugin::class.java) + + project.repositories.google() + project.extensions.getByType(JavaPluginExtension::class.java).toolchain { + it.languageVersion.set(JavaLanguageVersion.of(21)) + it.vendor.set(JvmVendorSpec.JETBRAINS) + } + + project.afterEvaluate { + project.tasks.named("build").configure { task -> + task.finalizedBy("reload") + } + project.tasks.named("run").configure { task -> + task.dependsOn("hotRun") + } + } + } +} \ 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..472d9e3b77 --- /dev/null +++ b/java/gradle/src/main/kotlin/LibrariesTask.kt @@ -0,0 +1,84 @@ +package org.processing.java.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Optional +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() { + + // TODO: Allow multiple directories + @InputDirectory + @Optional + val librariesDirectory: DirectoryProperty = project.objects.directoryProperty() + + @OutputFile + val librariesMetaData: RegularFileProperty = project.objects.fileProperty() + + init{ + librariesMetaData.convention(project.layout.buildDirectory.file("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() { + if (!librariesDirectory.isPresent) { + logger.error("Libraries directory is not set. Libraries will not be imported.") + val meta = ObjectOutputStream(librariesMetaData.get().asFile.outputStream()) + meta.writeObject(arrayListOf()) + meta.close() + return + } + val libraries = librariesDirectory.get().asFile + .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 of 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() + + val meta = ObjectOutputStream(librariesMetaData.get().asFile.outputStream()) + meta.writeObject(libraries) + 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..a45f2ff84b --- /dev/null +++ b/java/gradle/src/main/kotlin/ProcessingPlugin.kt @@ -0,0 +1,207 @@ +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 java.util.Properties +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? ?: "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? + + // 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) } + + if(logPort != null) task.standardOutput = Socket("localhost", logPort.toInt()).outputStream + if(errPort != null) task.errorOutput = Socket("localhost", errPort.toInt()).outputStream + + } + + 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") + ).apply { + filter.include("**/*.pde") + filter.exclude("${project.layout.buildDirectory.asFile.get().name}/**") + + srcDir("./") + srcDir("$workingDir/unsaved") + } + sourceSet.allSource.source(pdeSourceSet) +// sourceSet.java.srcDir(project.layout.projectDirectory).apply { +// include("*.java") +// } + + 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.librariesDirectory.set(sketchbook?.let { File(it, "libraries") }) + // TODO: Save the libraries metadata to settings folder to share between sketches + } + + 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 -> + task.librariesMetaData + task.dependsOn(pdeTask, librariesScan) + // TODO: Save the libraries metadata to settings folder to share between sketches + } + + // Make sure that the PDE task 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..190e0e7362 --- /dev/null +++ b/java/gradle/src/test/kotlin/ProcessingPluginTest.kt @@ -0,0 +1,203 @@ +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("org.processing.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 + ) + } + + @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(){ + // TODO: Implement a test that imports a Processing library and uses it in the sketch + } + + 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/preprocessor/build.gradle.kts b/java/preprocessor/build.gradle.kts index f2bc2a2d9f..e108b58a4a 100644 --- a/java/preprocessor/build.gradle.kts +++ b/java/preprocessor/build.gradle.kts @@ -1,7 +1,8 @@ import com.vanniktech.maven.publish.SonatypeHost plugins{ - id("java") + java + antlr alias(libs.plugins.mavenPublish) } @@ -14,24 +15,43 @@ repositories{ sourceSets{ main{ java{ - srcDirs("src/main/java", "../src/", "../generated/") + srcDirs("src/main/java", "../src/") include("processing/mode/java/preproc/**/*", "processing/app/**/*") } } - +} +afterEvaluate{ + tasks.withType(Jar::class.java){ + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + dependsOn(tasks.generateGrammarSource) + } } dependencies{ + implementation(project(":app:utils")) + implementation(libs.antlr) implementation(libs.eclipseJDT) - implementation(project(":core")) - implementation(project(":app:utils")) + antlr(libs.antlr4) + implementation(libs.antlr4Runtime) +} + +publishing{ + repositories{ + maven { + name = "App" + url = uri(project(":app").layout.buildDirectory.dir("resources-bundled/common/repository").get().asFile.absolutePath) + } + } } mavenPublishing{ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) - signAllPublications() + + // Only sign if signing is set up + if(project.hasProperty("signing.keyId") || project.hasProperty("signing.signingInMemoryKey")) + signAllPublications() pom{ name.set("Processing Pre-processor") @@ -59,13 +79,4 @@ mavenPublishing{ developerConnection.set("scm:git:ssh://git@github.com/processing/processing4.git") } } -} -tasks.withType { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE -} -tasks.compileJava{ - dependsOn("ant-preproc") -} -ant.importBuild("../build.xml"){ antTaskName -> - "ant-$antTaskName" } \ No newline at end of file diff --git a/java/preprocessor/src/main/antlr/JavaLexer.g4 b/java/preprocessor/src/main/antlr/JavaLexer.g4 new file mode 100644 index 0000000000..b924864ea2 --- /dev/null +++ b/java/preprocessor/src/main/antlr/JavaLexer.g4 @@ -0,0 +1,235 @@ +/* + [The "BSD licence"] + Copyright (c) 2013 Terence Parr, Sam Harwell + Copyright (c) 2017 Ivan Kochurkin (upgrade to Java 8) + Copyright (c) 2021 Michał Lorek (upgrade to Java 11) + Copyright (c) 2022 Michał Lorek (upgrade to Java 17) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// $antlr-format alignTrailingComments true, columnLimit 150, maxEmptyLinesToKeep 1, reflowComments false, useTab false +// $antlr-format allowShortRulesOnASingleLine true, allowShortBlocksOnASingleLine true, minEmptyLines 0, alignSemicolons ownLine +// $antlr-format alignColons trailing, singleLineOverrulesHangingColon true, alignLexerCommands true, alignLabels true, alignTrailers true + +lexer grammar JavaLexer; + +// Keywords + +ABSTRACT : 'abstract'; +ASSERT : 'assert'; +BOOLEAN : 'boolean'; +BREAK : 'break'; +BYTE : 'byte'; +CASE : 'case'; +CATCH : 'catch'; +CHAR : 'char'; +CLASS : 'class'; +CONST : 'const'; +CONTINUE : 'continue'; +DEFAULT : 'default'; +DO : 'do'; +DOUBLE : 'double'; +ELSE : 'else'; +ENUM : 'enum'; +EXTENDS : 'extends'; +FINAL : 'final'; +FINALLY : 'finally'; +FLOAT : 'float'; +FOR : 'for'; +IF : 'if'; +GOTO : 'goto'; +IMPLEMENTS : 'implements'; +IMPORT : 'import'; +INSTANCEOF : 'instanceof'; +INT : 'int'; +INTERFACE : 'interface'; +LONG : 'long'; +NATIVE : 'native'; +NEW : 'new'; +PACKAGE : 'package'; +PRIVATE : 'private'; +PROTECTED : 'protected'; +PUBLIC : 'public'; +RETURN : 'return'; +SHORT : 'short'; +STATIC : 'static'; +STRICTFP : 'strictfp'; +SUPER : 'super'; +SWITCH : 'switch'; +SYNCHRONIZED : 'synchronized'; +THIS : 'this'; +THROW : 'throw'; +THROWS : 'throws'; +TRANSIENT : 'transient'; +TRY : 'try'; +VOID : 'void'; +VOLATILE : 'volatile'; +WHILE : 'while'; + +// Module related keywords +MODULE : 'module'; +OPEN : 'open'; +REQUIRES : 'requires'; +EXPORTS : 'exports'; +OPENS : 'opens'; +TO : 'to'; +USES : 'uses'; +PROVIDES : 'provides'; +WITH : 'with'; +TRANSITIVE : 'transitive'; + +// Local Variable Type Inference +VAR: 'var'; // reserved type name + +// Switch Expressions +YIELD: 'yield'; // reserved type name from Java 14 + +// Records +RECORD: 'record'; + +// Sealed Classes +SEALED : 'sealed'; +PERMITS : 'permits'; +NON_SEALED : 'non-sealed'; + +// Literals + +DECIMAL_LITERAL : ('0' | [1-9] (Digits? | '_'+ Digits)) [lL]?; +HEX_LITERAL : '0' [xX] [0-9a-fA-F] ([0-9a-fA-F_]* [0-9a-fA-F])? [lL]?; +OCT_LITERAL : '0' '_'* [0-7] ([0-7_]* [0-7])? [lL]?; +BINARY_LITERAL : '0' [bB] [01] ([01_]* [01])? [lL]?; + +FLOAT_LITERAL: + (Digits '.' Digits? | '.' Digits) ExponentPart? [fFdD]? + | Digits (ExponentPart [fFdD]? | [fFdD]) +; + +HEX_FLOAT_LITERAL: '0' [xX] (HexDigits '.'? | HexDigits? '.' HexDigits) [pP] [+-]? Digits [fFdD]?; + +BOOL_LITERAL: 'true' | 'false'; + +CHAR_LITERAL: '\'' (~['\\\r\n] | EscapeSequence) '\''; + +STRING_LITERAL: '"' (~["\\\r\n] | EscapeSequence)* '"'; + +MULTI_STRING_LIT: '"""' (~[\\] | EscapeSequence)*? '"""'; + +TEXT_BLOCK: '"""' [ \t]* [\r\n] (. | EscapeSequence)*? '"""'; + +NULL_LITERAL: 'null'; + +// Separators + +LPAREN : '('; +RPAREN : ')'; +LBRACE : '{'; +RBRACE : '}'; +LBRACK : '['; +RBRACK : ']'; +SEMI : ';'; +COMMA : ','; +DOT : '.'; + +// Operators + +ASSIGN : '='; +GT : '>'; +LT : '<'; +BANG : '!'; +TILDE : '~'; +QUESTION : '?'; +COLON : ':'; +EQUAL : '=='; +LE : '<='; +GE : '>='; +NOTEQUAL : '!='; +AND : '&&'; +OR : '||'; +INC : '++'; +DEC : '--'; +ADD : '+'; +SUB : '-'; +MUL : '*'; +DIV : '/'; +BITAND : '&'; +BITOR : '|'; +CARET : '^'; +MOD : '%'; + +ADD_ASSIGN : '+='; +SUB_ASSIGN : '-='; +MUL_ASSIGN : '*='; +DIV_ASSIGN : '/='; +AND_ASSIGN : '&='; +OR_ASSIGN : '|='; +XOR_ASSIGN : '^='; +MOD_ASSIGN : '%='; +LSHIFT_ASSIGN : '<<='; +RSHIFT_ASSIGN : '>>='; +URSHIFT_ASSIGN : '>>>='; + +// Java 8 tokens + +ARROW : '->'; +COLONCOLON : '::'; + +// Additional symbols not defined in the lexical specification + +AT : '@'; +ELLIPSIS : '...'; + +// Whitespace and comments + +WS : [ \t\r\n\u000C]+ -> channel(HIDDEN); +COMMENT : '/*' .*? '*/' -> channel(HIDDEN); +LINE_COMMENT : '//' ~[\r\n]* -> channel(HIDDEN); + +// Identifiers + +IDENTIFIER: Letter LetterOrDigit*; + +// Fragment rules + +fragment ExponentPart: [eE] [+-]? Digits; + +fragment EscapeSequence: + '\\' 'u005c'? [btnfr"'\\] + | '\\' 'u005c'? ([0-3]? [0-7])? [0-7] + | '\\' 'u'+ HexDigit HexDigit HexDigit HexDigit +; + +fragment HexDigits: HexDigit ((HexDigit | '_')* HexDigit)?; + +fragment HexDigit: [0-9a-fA-F]; + +fragment Digits: [0-9] ([0-9_]* [0-9])?; + +fragment LetterOrDigit: Letter | [0-9]; + +fragment Letter: + [a-zA-Z$_] // these are the "java letters" below 0x7F + | ~[\u0000-\u007F\uD800-\uDBFF] // covers all characters above 0x7F which are not a surrogate + | [\uD800-\uDBFF] [\uDC00-\uDFFF] // covers UTF-16 surrogate pairs encodings for U+10000 to U+10FFFF +; \ No newline at end of file diff --git a/java/preprocessor/src/main/antlr/JavaParser.g4 b/java/preprocessor/src/main/antlr/JavaParser.g4 new file mode 100644 index 0000000000..d273fa8885 --- /dev/null +++ b/java/preprocessor/src/main/antlr/JavaParser.g4 @@ -0,0 +1,826 @@ +/* + [The "BSD licence"] + Copyright (c) 2013 Terence Parr, Sam Harwell + Copyright (c) 2017 Ivan Kochurkin (upgrade to Java 8) + Copyright (c) 2021 Michał Lorek (upgrade to Java 11) + Copyright (c) 2022 Michał Lorek (upgrade to Java 17) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +// $antlr-format alignTrailingComments true, columnLimit 150, minEmptyLines 1, maxEmptyLinesToKeep 1, reflowComments false, useTab false +// $antlr-format allowShortRulesOnASingleLine false, allowShortBlocksOnASingleLine true, alignSemicolons hanging, alignColons hanging + +parser grammar JavaParser; + +options { + tokenVocab = JavaLexer; +} + +compilationUnit + : packageDeclaration? (importDeclaration | ';')* (typeDeclaration | ';')* EOF + | moduleDeclaration EOF + ; + +packageDeclaration + : annotation* PACKAGE qualifiedName ';' + ; + +importDeclaration + : IMPORT STATIC? qualifiedName ('.' '*')? ';' + ; + +typeDeclaration + : classOrInterfaceModifier* ( + classDeclaration + | enumDeclaration + | interfaceDeclaration + | annotationTypeDeclaration + | recordDeclaration + ) + ; + +modifier + : classOrInterfaceModifier + | NATIVE + | SYNCHRONIZED + | TRANSIENT + | VOLATILE + ; + +classOrInterfaceModifier + : annotation + | PUBLIC + | PROTECTED + | PRIVATE + | STATIC + | ABSTRACT + | FINAL // FINAL for class only -- does not apply to interfaces + | STRICTFP + | SEALED // Java17 + | NON_SEALED // Java17 + ; + +variableModifier + : FINAL + | annotation + ; + +classDeclaration + : CLASS identifier typeParameters? (EXTENDS typeType)? (IMPLEMENTS typeList)? ( + PERMITS typeList + )? // Java17 + classBody + ; + +typeParameters + : '<' typeParameter (',' typeParameter)* '>' + ; + +typeParameter + : annotation* identifier (EXTENDS annotation* typeBound)? + ; + +typeBound + : typeType ('&' typeType)* + ; + +enumDeclaration + : ENUM identifier (IMPLEMENTS typeList)? '{' enumConstants? ','? enumBodyDeclarations? '}' + ; + +enumConstants + : enumConstant (',' enumConstant)* + ; + +enumConstant + : annotation* identifier arguments? classBody? + ; + +enumBodyDeclarations + : ';' classBodyDeclaration* + ; + +interfaceDeclaration + : INTERFACE identifier typeParameters? (EXTENDS typeList)? (PERMITS typeList)? interfaceBody + ; + +classBody + : '{' classBodyDeclaration* '}' + ; + +interfaceBody + : '{' interfaceBodyDeclaration* '}' + ; + +classBodyDeclaration + : ';' + | STATIC? block + | modifier* memberDeclaration + ; + +memberDeclaration + : recordDeclaration //Java17 + | methodDeclaration + | genericMethodDeclaration + | fieldDeclaration + | constructorDeclaration + | genericConstructorDeclaration + | interfaceDeclaration + | annotationTypeDeclaration + | classDeclaration + | enumDeclaration + ; + +/* We use rule this even for void methods which cannot have [] after parameters. + This simplifies grammar and we can consider void to be a type, which + renders the [] matching as a context-sensitive issue or a semantic check + for invalid return type after parsing. + */ +methodDeclaration + : typeTypeOrVoid identifier formalParameters ('[' ']')* (THROWS qualifiedNameList)? methodBody + ; + +methodBody + : block + | ';' + ; + +typeTypeOrVoid + : typeType + | VOID + ; + +genericMethodDeclaration + : typeParameters methodDeclaration + ; + +genericConstructorDeclaration + : typeParameters constructorDeclaration + ; + +constructorDeclaration + : identifier formalParameters (THROWS qualifiedNameList)? constructorBody = block + ; + +compactConstructorDeclaration + : modifier* identifier constructorBody = block + ; + +fieldDeclaration + : typeType variableDeclarators ';' + ; + +interfaceBodyDeclaration + : modifier* interfaceMemberDeclaration + | ';' + ; + +interfaceMemberDeclaration + : recordDeclaration // Java17 + | constDeclaration + | interfaceMethodDeclaration + | genericInterfaceMethodDeclaration + | interfaceDeclaration + | annotationTypeDeclaration + | classDeclaration + | enumDeclaration + ; + +constDeclaration + : typeType constantDeclarator (',' constantDeclarator)* ';' + ; + +constantDeclarator + : identifier ('[' ']')* '=' variableInitializer + ; + +// Early versions of Java allows brackets after the method name, eg. +// public int[] return2DArray() [] { ... } +// is the same as +// public int[][] return2DArray() { ... } +interfaceMethodDeclaration + : interfaceMethodModifier* interfaceCommonBodyDeclaration + ; + +// Java8 +interfaceMethodModifier + : annotation + | PUBLIC + | ABSTRACT + | DEFAULT + | STATIC + | STRICTFP + ; + +genericInterfaceMethodDeclaration + : interfaceMethodModifier* typeParameters interfaceCommonBodyDeclaration + ; + +interfaceCommonBodyDeclaration + : annotation* typeTypeOrVoid identifier formalParameters ('[' ']')* (THROWS qualifiedNameList)? methodBody + ; + +variableDeclarators + : variableDeclarator (',' variableDeclarator)* + ; + +variableDeclarator + : variableDeclaratorId ('=' variableInitializer)? + ; + +variableDeclaratorId + : identifier ('[' ']')* + ; + +variableInitializer + : arrayInitializer + | expression + ; + +arrayInitializer + : '{' (variableInitializer (',' variableInitializer)* ','?)? '}' + ; + +classOrInterfaceType + : (identifier typeArguments? '.')* typeIdentifier typeArguments? + ; + +typeArgument + : typeType + | annotation* '?' ((EXTENDS | SUPER) typeType)? + ; + +qualifiedNameList + : qualifiedName (',' qualifiedName)* + ; + +formalParameters + : '(' ( + receiverParameter? + | receiverParameter (',' formalParameterList)? + | formalParameterList? + ) ')' + ; + +receiverParameter + : typeType (identifier '.')* THIS + ; + +formalParameterList + : formalParameter (',' formalParameter)* (',' lastFormalParameter)? + | lastFormalParameter + ; + +formalParameter + : variableModifier* typeType variableDeclaratorId + ; + +lastFormalParameter + : variableModifier* typeType annotation* '...' variableDeclaratorId + ; + +// local variable type inference +lambdaLVTIList + : lambdaLVTIParameter (',' lambdaLVTIParameter)* + ; + +lambdaLVTIParameter + : variableModifier* VAR identifier + ; + +qualifiedName + : identifier ('.' identifier)* + ; + +baseStringLiteral + : STRING_LITERAL + ; + +multilineStringLiteral + : MULTI_STRING_LIT + ; + +stringLiteral + : baseStringLiteral + | multilineStringLiteral + ; + +literal + : integerLiteral + | floatLiteral + | CHAR_LITERAL + | stringLiteral + | BOOL_LITERAL + | NULL_LITERAL + | TEXT_BLOCK // Java17 + ; + +integerLiteral + : DECIMAL_LITERAL + | HEX_LITERAL + | OCT_LITERAL + | BINARY_LITERAL + ; + +floatLiteral + : FLOAT_LITERAL + | HEX_FLOAT_LITERAL + ; + +// ANNOTATIONS +altAnnotationQualifiedName + : (identifier DOT)* '@' identifier + ; + +annotation + : ('@' qualifiedName | altAnnotationQualifiedName) ( + '(' ( elementValuePairs | elementValue)? ')' + )? + ; + +elementValuePairs + : elementValuePair (',' elementValuePair)* + ; + +elementValuePair + : identifier '=' elementValue + ; + +elementValue + : expression + | annotation + | elementValueArrayInitializer + ; + +elementValueArrayInitializer + : '{' (elementValue (',' elementValue)*)? ','? '}' + ; + +annotationTypeDeclaration + : '@' INTERFACE identifier annotationTypeBody + ; + +annotationTypeBody + : '{' annotationTypeElementDeclaration* '}' + ; + +annotationTypeElementDeclaration + : modifier* annotationTypeElementRest + | ';' // this is not allowed by the grammar, but apparently allowed by the actual compiler + ; + +annotationTypeElementRest + : typeType annotationMethodOrConstantRest ';' + | classDeclaration ';'? + | interfaceDeclaration ';'? + | enumDeclaration ';'? + | annotationTypeDeclaration ';'? + | recordDeclaration ';'? // Java17 + ; + +annotationMethodOrConstantRest + : annotationMethodRest + | annotationConstantRest + ; + +annotationMethodRest + : identifier '(' ')' defaultValue? + ; + +annotationConstantRest + : variableDeclarators + ; + +defaultValue + : DEFAULT elementValue + ; + +// MODULES - Java9 + +moduleDeclaration + : OPEN? MODULE qualifiedName moduleBody + ; + +moduleBody + : '{' moduleDirective* '}' + ; + +moduleDirective + : REQUIRES requiresModifier* qualifiedName ';' + | EXPORTS qualifiedName (TO qualifiedName)? ';' + | OPENS qualifiedName (TO qualifiedName)? ';' + | USES qualifiedName ';' + | PROVIDES qualifiedName WITH qualifiedName ';' + ; + +requiresModifier + : TRANSITIVE + | STATIC + ; + +// RECORDS - Java 17 + +recordDeclaration + : RECORD identifier typeParameters? recordHeader (IMPLEMENTS typeList)? recordBody + ; + +recordHeader + : '(' recordComponentList? ')' + ; + +recordComponentList + : recordComponent (',' recordComponent)* + ; + +recordComponent + : typeType identifier + ; + +recordBody + : '{' (classBodyDeclaration | compactConstructorDeclaration)* '}' + ; + +// STATEMENTS / BLOCKS + +block + : '{' blockStatement* '}' + ; + +blockStatement + : localVariableDeclaration ';' + | localTypeDeclaration + | statement + ; + +localVariableDeclaration + : variableModifier* (VAR identifier '=' expression | typeType variableDeclarators) + ; + +identifier + : IDENTIFIER + | MODULE + | OPEN + | REQUIRES + | EXPORTS + | OPENS + | TO + | USES + | PROVIDES + | WITH + | TRANSITIVE + | YIELD + | SEALED + | PERMITS + | RECORD + | VAR + ; + +typeIdentifier // Identifiers that are not restricted for type declarations + : IDENTIFIER + | MODULE + | OPEN + | REQUIRES + | EXPORTS + | OPENS + | TO + | USES + | PROVIDES + | WITH + | TRANSITIVE + | SEALED + | PERMITS + | RECORD + ; + +localTypeDeclaration + : classOrInterfaceModifier* (classDeclaration | interfaceDeclaration | recordDeclaration) + ; + +statement + : blockLabel = block + | ASSERT expression (':' expression)? ';' + | IF parExpression statement (ELSE statement)? + | FOR '(' forControl ')' statement + | WHILE parExpression statement + | DO statement WHILE parExpression ';' + | TRY block (catchClause+ finallyBlock? | finallyBlock) + | TRY resourceSpecification block catchClause* finallyBlock? + | SWITCH parExpression '{' switchBlockStatementGroup* switchLabel* '}' + | SYNCHRONIZED parExpression block + | RETURN expression? ';' + | THROW expression ';' + | BREAK identifier? ';' + | CONTINUE identifier? ';' + | YIELD expression ';' // Java17 + | SEMI + | statementExpression = expression ';' + | switchExpression ';'? // Java17 + | identifierLabel = identifier ':' statement + ; + +catchClause + : CATCH '(' variableModifier* catchType identifier ')' block + ; + +catchType + : qualifiedName ('|' qualifiedName)* + ; + +finallyBlock + : FINALLY block + ; + +resourceSpecification + : '(' resources ';'? ')' + ; + +resources + : resource (';' resource)* + ; + +resource + : variableModifier* (classOrInterfaceType variableDeclaratorId | VAR identifier) '=' expression + | qualifiedName + ; + +/** Matches cases then statements, both of which are mandatory. + * To handle empty cases at the end, we add switchLabel* to statement. + */ +switchBlockStatementGroup + : switchLabel+ blockStatement+ + ; + +switchLabel + : CASE ( + constantExpression = expression + | enumConstantName = IDENTIFIER + | typeType varName = identifier + ) ':' + | DEFAULT ':' + ; + +forControl + : enhancedForControl + | forInit? ';' expression? ';' forUpdate = expressionList? + ; + +forInit + : localVariableDeclaration + | expressionList + ; + +enhancedForControl + : variableModifier* (typeType | VAR) variableDeclaratorId ':' expression + ; + +// EXPRESSIONS + +parExpression + : '(' expression ')' + ; + +expressionList + : expression (',' expression)* + ; + +methodCall + : (identifier | THIS | SUPER) arguments + ; + +expression + // Expression order in accordance with https://introcs.cs.princeton.edu/java/11precedence/ + // Level 16, Primary, array and member access + : primary #PrimaryExpression + | expression '[' expression ']' #SquareBracketExpression + | expression bop = '.' ( + identifier + | methodCall + | THIS + | NEW nonWildcardTypeArguments? innerCreator + | SUPER superSuffix + | explicitGenericInvocation + ) #MemberReferenceExpression + // Method calls and method references are part of primary, and hence level 16 precedence + | methodCall #MethodCallExpression + | expression '::' typeArguments? identifier #MethodReferenceExpression + | typeType '::' (typeArguments? identifier | NEW) #MethodReferenceExpression + | classType '::' typeArguments? NEW #MethodReferenceExpression + + // Java17 + | switchExpression #ExpressionSwitch + + // Level 15 Post-increment/decrement operators + | expression postfix = ('++' | '--') #PostIncrementDecrementOperatorExpression + + // Level 14, Unary operators + | prefix = ('+' | '-' | '++' | '--' | '~' | '!') expression #UnaryOperatorExpression + + // Level 13 Cast and object creation + | '(' annotation* typeType ('&' typeType)* ')' expression #CastExpression + | NEW creator #ObjectCreationExpression + + // Level 12 to 1, Remaining operators + // Level 12, Multiplicative operators + | expression bop = ('*' | '/' | '%') expression #BinaryOperatorExpression + // Level 11, Additive operators + | expression bop = ('+' | '-') expression #BinaryOperatorExpression + // Level 10, Shift operators + | expression ('<' '<' | '>' '>' '>' | '>' '>') expression #BinaryOperatorExpression + // Level 9, Relational operators + | expression bop = ('<=' | '>=' | '>' | '<') expression #BinaryOperatorExpression + | expression bop = INSTANCEOF (typeType | pattern) #InstanceOfOperatorExpression + // Level 8, Equality Operators + | expression bop = ('==' | '!=') expression #BinaryOperatorExpression + // Level 7, Bitwise AND + | expression bop = '&' expression #BinaryOperatorExpression + // Level 6, Bitwise XOR + | expression bop = '^' expression #BinaryOperatorExpression + // Level 5, Bitwise OR + | expression bop = '|' expression #BinaryOperatorExpression + // Level 4, Logic AND + | expression bop = '&&' expression #BinaryOperatorExpression + // Level 3, Logic OR + | expression bop = '||' expression #BinaryOperatorExpression + // Level 2, Ternary + | expression bop = '?' expression ':' expression #TernaryExpression + // Level 1, Assignment + | expression bop = ( + '=' + | '+=' + | '-=' + | '*=' + | '/=' + | '&=' + | '|=' + | '^=' + | '>>=' + | '>>>=' + | '<<=' + | '%=' + ) expression #BinaryOperatorExpression + + // Level 0, Lambda Expression // Java8 + | lambdaExpression #ExpressionLambda + ; + +// Java17 +pattern + : variableModifier* typeType annotation* identifier + ; + +// Java8 +lambdaExpression + : lambdaParameters '->' lambdaBody + ; + +// Java8 +lambdaParameters + : identifier + | '(' formalParameterList? ')' + | '(' identifier (',' identifier)* ')' + | '(' lambdaLVTIList? ')' + ; + +// Java8 +lambdaBody + : expression + | block + ; + +primary + : '(' expression ')' + | THIS + | SUPER + | literal + | identifier + | typeTypeOrVoid '.' CLASS + | nonWildcardTypeArguments (explicitGenericInvocationSuffix | THIS arguments) + ; + +// Java17 +switchExpression + : SWITCH parExpression '{' switchLabeledRule* '}' + ; + +// Java17 +switchLabeledRule + : CASE (expressionList | NULL_LITERAL | guardedPattern) (ARROW | COLON) switchRuleOutcome + | DEFAULT (ARROW | COLON) switchRuleOutcome + ; + +// Java17 +guardedPattern + : '(' guardedPattern ')' + | variableModifier* typeType annotation* identifier ('&&' expression)* + | guardedPattern '&&' expression + ; + +// Java17 +switchRuleOutcome + : block + | blockStatement* + ; + +classType + : (classOrInterfaceType '.')? annotation* identifier typeArguments? + ; + +creator + : nonWildcardTypeArguments? createdName classCreatorRest + | createdName arrayCreatorRest + ; + +createdName + : identifier typeArgumentsOrDiamond? ('.' identifier typeArgumentsOrDiamond?)* + | primitiveType + ; + +innerCreator + : identifier nonWildcardTypeArgumentsOrDiamond? classCreatorRest + ; + +arrayCreatorRest + : ('[' ']')+ arrayInitializer + | ('[' expression ']')+ ('[' ']')* + ; + +classCreatorRest + : arguments classBody? + ; + +explicitGenericInvocation + : nonWildcardTypeArguments explicitGenericInvocationSuffix + ; + +typeArgumentsOrDiamond + : '<' '>' + | typeArguments + ; + +nonWildcardTypeArgumentsOrDiamond + : '<' '>' + | nonWildcardTypeArguments + ; + +nonWildcardTypeArguments + : '<' typeList '>' + ; + +typeList + : typeType (',' typeType)* + ; + +typeType + : annotation* (classOrInterfaceType | primitiveType) (annotation* '[' ']')* + ; + +primitiveType + : BOOLEAN + | CHAR + | BYTE + | SHORT + | INT + | LONG + | FLOAT + | DOUBLE + ; + +typeArguments + : '<' typeArgument (',' typeArgument)* '>' + ; + +superSuffix + : arguments + | '.' typeArguments? identifier arguments? + ; + +explicitGenericInvocationSuffix + : SUPER superSuffix + | identifier arguments + ; + +arguments + : '(' expressionList? ')' + ; \ No newline at end of file diff --git a/java/preprocessor/src/main/antlr/processing/mode/java/preproc/Processing.g4 b/java/preprocessor/src/main/antlr/processing/mode/java/preproc/Processing.g4 new file mode 100644 index 0000000000..2d4edc041a --- /dev/null +++ b/java/preprocessor/src/main/antlr/processing/mode/java/preproc/Processing.g4 @@ -0,0 +1,147 @@ +/** + * Based on Java 1.7 grammar for ANTLR 4, see Java.g4 + * + * - changes main entry point to reflect sketch types 'static' | 'active' + * - adds support for type converter functions like "int()" + * - adds pseudo primitive type "color" + * - adds HTML hex notation with hash symbol: #ff5522 + * - allow color to appear as part of qualified names (like in imports) + */ + +grammar Processing; + +@lexer::members { + public static final int WHITESPACE = 1; + public static final int COMMENTS = 2; +} + +@header { + package processing.mode.java.preproc; +} + +// import Java grammar +import JavaParser, JavaLexer; + +// main entry point, select sketch type +processingSketch + : staticProcessingSketch + | javaProcessingSketch + | activeProcessingSketch +// | warnMixedModes + ; + +// java mode, is a compilation unit +javaProcessingSketch + : packageDeclaration? importDeclaration* typeDeclaration+ EOF + ; + +// No method declarations, just statements +staticProcessingSketch + : (importDeclaration | blockStatement | typeDeclaration)* EOF + ; + +// active mode, has function definitions +activeProcessingSketch + : (importDeclaration | classBodyDeclaration)* EOF + ; + +// User incorrectly mixing modes. Included to allow for kind error message. +warnMixedModes + : (importDeclaration | classBodyDeclaration | blockStatement)* blockStatement classBodyDeclaration (importDeclaration | classBodyDeclaration | blockStatement)* + | (importDeclaration | classBodyDeclaration | blockStatement)* classBodyDeclaration blockStatement (importDeclaration | classBodyDeclaration | blockStatement)* + ; + +variableDeclaratorId + : warnTypeAsVariableName + | IDENTIFIER ('[' ']')* + ; + +// bug #93 +// https://github.com/processing/processing/issues/93 +// prevent from types being used as variable names +warnTypeAsVariableName + : primitiveType ('[' ']')* { + notifyErrorListeners("Type names are not allowed as variable names: "+$primitiveType.text); + } + ; + +// catch special API function calls that we are interested in +methodCall + : functionWithPrimitiveTypeName + | IDENTIFIER '(' expressionList? ')' + | THIS '(' expressionList? ')' + | SUPER '(' expressionList? ')' + ; + +// these are primitive type names plus "()" +// "color" is a special Processing primitive (== int) +functionWithPrimitiveTypeName + : ( 'boolean' + | 'byte' + | 'char' + | 'float' + | 'int' + | 'color' + ) '(' expressionList? ')' + ; + +// adding support for "color" primitive +primitiveType + : BOOLEAN + | CHAR + | BYTE + | SHORT + | INT + | LONG + | FLOAT + | DOUBLE + | colorPrimitiveType + ; + +colorPrimitiveType + : 'color' + ; + +qualifiedName + : (IDENTIFIER | colorPrimitiveType) ('.' (IDENTIFIER | colorPrimitiveType))* + ; + +// added HexColorLiteral +literal + : integerLiteral + | floatLiteral + | CHAR_LITERAL + | stringLiteral + | BOOL_LITERAL + | NULL_LITERAL + | hexColorLiteral + ; + +// As parser rule so this produces a separate listener +// for us to alter its value. +hexColorLiteral + : HexColorLiteral + ; + +// add color literal notations for +// #ff5522 +HexColorLiteral + : '#' (HexDigit HexDigit)? HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit + ; + +// hide but do not remove whitespace and comments + +WS : [ \t\r\n\u000C]+ -> channel(1) + ; + +COMMENT + : '/*' .*? '*/' -> channel(2) + ; + +LINE_COMMENT + : '//' ~[\r\n]* -> channel(2) + ; + +CHAR_LITERAL + : '\'' (~['\\\r\n] | EscapeSequence)* '\'' // A bit nasty but let JDT tackle invalid chars + ; \ No newline at end of file diff --git a/java/preprocessor/src/main/java/processing/app/Platform.java b/java/preprocessor/src/main/java/processing/app/Platform.java index 079d9d79cd..b90974dd39 100644 --- a/java/preprocessor/src/main/java/processing/app/Platform.java +++ b/java/preprocessor/src/main/java/processing/app/Platform.java @@ -15,7 +15,21 @@ static public File getSettingsFolder() { } settingsFolder = new File(appData + "\\Processing"); } else { - settingsFolder = new File(System.getProperty("user.home") + "/.processing"); + // Check to see if the user has set a different location for their config + String configHomeEnv = System.getenv("XDG_CONFIG_HOME"); + if (configHomeEnv != null && !configHomeEnv.isBlank()) { + settingsFolder = new File(configHomeEnv); + if (!settingsFolder.exists()) { + settingsFolder = null; // don't use non-existent folder + } + } + String snapUserCommon = System.getenv("SNAP_USER_COMMON"); + if (snapUserCommon != null && !snapUserCommon.isBlank()) { + settingsFolder = new File(snapUserCommon); + } + if (settingsFolder == null) { + settingsFolder = new File(System.getProperty("user.home"), ".config"); + } } return settingsFolder; } diff --git a/java/preprocessor/src/main/java/processing/app/Preferences.java b/java/preprocessor/src/main/java/processing/app/Preferences.java index 7ce476fdea..eab3a23974 100644 --- a/java/preprocessor/src/main/java/processing/app/Preferences.java +++ b/java/preprocessor/src/main/java/processing/app/Preferences.java @@ -58,7 +58,7 @@ static public String get(String attribute /*, String defaultValue */) { } } static public boolean getBoolean(String attribute) { - String value = get(attribute); //, null); + String value = get(attribute); return Boolean.parseBoolean(value); } static public int getInteger(String attribute /*, int defaultValue*/) { diff --git a/java/src/processing/mode/java/Compiler.java b/java/src/processing/mode/java/Compiler.java index c7ec613b45..a2ac110455 100644 --- a/java/src/processing/mode/java/Compiler.java +++ b/java/src/processing/mode/java/Compiler.java @@ -52,6 +52,7 @@ public class Compiler { * @throws SketchException Only if there's a problem. Only then. */ static public boolean compile(JavaBuild build) throws SketchException { + System.out.println(Language.text("gradle.using_eclipse")); // This will be filled in if anyone gets angry SketchException exception = null; diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 3fab2c8b17..abf505784c 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -29,7 +29,6 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -63,6 +62,8 @@ import processing.mode.java.tweak.SketchParser; import processing.mode.java.tweak.TweakClient; +import static processing.app.gradle.GradleSettings.addGradleSettings; + public class JavaEditor extends Editor { JavaMode jmode; @@ -207,6 +208,7 @@ public void rebuild() { public EditorFooter createFooter() { EditorFooter footer = super.createFooter(); addErrorTable(footer); + addGradleSettings(footer, service); return footer; } @@ -501,6 +503,10 @@ public String getCommentPrefix() { * Handler for Sketch → Export Application */ public void handleExportApplication() { + if(service.getEnabled()){ + service.export(); + return; + } if (handleExportCheckModified()) { statusNotice(Language.text("export.notice.exporting")); ExportPrompt ep = new ExportPrompt(this, () -> { @@ -651,6 +657,14 @@ public void handleTweak() { protected void handleLaunch(boolean present, boolean tweak) { prepareRun(); toolbar.activateRun(); + + if(this.service.getEnabled()){ + System.setProperty("processing.fullscreen", present ? "true" : "false"); + System.setProperty("processing.tweak", tweak ? "true" : "false"); + this.service.run(); + return; + } + synchronized (runtimeLock) { runtimeLaunchRequested = true; } @@ -679,6 +693,11 @@ protected void handleLaunch(boolean present, boolean tweak) { * session or performs standard stop action if not currently debugging. */ public void handleStop() { + if(this.service.getEnabled()){ + this.service.stop(); + return; + } + if (debugger.isStarted()) { debugger.stopDebug(); diff --git a/java/src/processing/mode/java/preproc/PdeParseTreeListener.java b/java/src/processing/mode/java/preproc/PdeParseTreeListener.java index cb4fd00010..c3edc810ad 100644 --- a/java/src/processing/mode/java/preproc/PdeParseTreeListener.java +++ b/java/src/processing/mode/java/preproc/PdeParseTreeListener.java @@ -32,7 +32,6 @@ import processing.app.Base; import processing.app.Preferences; -import processing.core.PApplet; import processing.mode.java.preproc.PdePreprocessor.Mode; /** @@ -1234,19 +1233,35 @@ protected void writeMain(PrintWriterWithEditGen footerWriter, { // assemble line with applet args StringJoiner argsJoiner = new StringJoiner(", "); - boolean shouldFullScreen = Preferences.getBoolean("export.application.present"); - shouldFullScreen = shouldFullScreen || Preferences.getBoolean("export.application.fullscreen"); + boolean shouldFullScreen; + String presentProp = System.getProperty("processing.fullscreen"); + + if (presentProp != null) { + shouldFullScreen = presentProp.equals("true"); + } else { + boolean isExportedApp = Preferences.getBoolean("export.application.present"); + boolean isFullscreenPref = Preferences.getBoolean("export.application.fullscreen"); + shouldFullScreen = isExportedApp || isFullscreenPref; + } + if (shouldFullScreen) { - argsJoiner.add("\"" + PApplet.ARGS_FULL_SCREEN + "\""); + argsJoiner.add("\"--full-screen\""); - String bgColor = Preferences.get("run.present.bgcolor"); - argsJoiner.add("\"" + PApplet.ARGS_BGCOLOR + "=" + bgColor + "\""); + String bgColor = System.getProperty("processing.window.color", Preferences.get("run.present.bgcolor")); + argsJoiner.add("\"--bgcolor=" + bgColor + "\""); - if (Preferences.getBoolean("export.application.stop")) { + boolean showStop; + var hideStop = System.getProperty("processing.stop.hide"); + if(hideStop != null){ + showStop = hideStop.equals("false"); + }else{ + showStop = Preferences.getBoolean("export.application.stop"); + } + if(showStop) { String stopColor = Preferences.get("run.present.stop.color"); - argsJoiner.add("\"" + PApplet.ARGS_STOP_COLOR + "=" + stopColor + "\""); + argsJoiner.add("\"--stop-color=" + stopColor + "\""); } else { - argsJoiner.add("\"" + PApplet.ARGS_HIDE_STOP + "\""); + argsJoiner.add("\"--hide-stop\""); } } diff --git a/java/src/processing/mode/java/preproc/TextTransform.java b/java/src/processing/mode/java/preproc/TextTransform.java index 77ae022f19..19ba8f3e53 100644 --- a/java/src/processing/mode/java/preproc/TextTransform.java +++ b/java/src/processing/mode/java/preproc/TextTransform.java @@ -8,8 +8,6 @@ import java.util.ListIterator; import java.util.stream.Collectors; -import processing.core.PApplet; - public class TextTransform { @@ -256,7 +254,7 @@ public int getInputOffset(int outputOffset) { i = -(i + 1); i -= 1; } - i = PApplet.constrain(i, 0, outMap.size()-1); + i = constrain(i, 0, outMap.size()-1); Edit edit = outMap.get(i); int diff = outputOffset - edit.toOffset; return edit.fromOffset + Math.min(diff, Math.max(0, edit.fromLength - 1)); @@ -271,7 +269,7 @@ public int getOutputOffset(int inputOffset) { i = -(i + 1); i -= 1; } - i = PApplet.constrain(i, 0, inMap.size()-1); + i = constrain(i, 0, inMap.size()-1); Edit edit = inMap.get(i); int diff = inputOffset - edit.fromOffset; return edit.toOffset + Math.min(diff, Math.max(0, edit.toLength - 1)); @@ -283,6 +281,10 @@ public OffsetMapper thenMapping(OffsetMapper mapper) { } } + static public final int constrain(int amt, int low, int high) { + return (amt < low) ? low : ((amt > high) ? high : amt); + } + private static class CompositeOffsetMapper implements OffsetMapper { private List mappers = new ArrayList<>(); diff --git a/java/test/resources/bug1532.pde b/java/test/resources/bug1532.pde index 66b24b7779..ae8ecdbf8b 100644 --- a/java/test/resources/bug1532.pde +++ b/java/test/resources/bug1532.pde @@ -20,9 +20,9 @@ flatCube[][] grid; void setup() { try { - quicktime.QTSession.open(); - } - catch (quicktime.QTException qte) { + // quicktime.QTSession.open(); + } + catch (quicktime.QTException qte) { qte.printStackTrace(); } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8f8cb74c7f..a2be58c694 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,7 @@ include( "app", "java", "java:preprocessor", + "java:gradle", "java:libraries:dxf", "java:libraries:io", "java:libraries:net",