From 0c674adc54adbe1b420c844989d198764086a351 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 17 Jul 2025 12:13:56 +0200 Subject: [PATCH] Initial Gradle Plugin configuration --- app/src/processing/app/Preferences.kt | 1 + app/src/processing/app/Sketch.java | 4 +- app/src/processing/app/gradle/GradleJob.kt | 1 + app/src/processing/app/gradle/GradlePlugin.kt | 20 ++ .../processing/app/gradle/GradleService.kt | 28 +-- .../processing/app/gradle/GradleSettings.kt | 194 +++++++++++++++++- app/src/processing/app/gradle/api/Sketch.kt | 2 +- app/src/processing/app/ui/EditorFooter.java | 1 + build/shared/lib/footer/settings.svg | 9 +- build/shared/lib/languages/PDE.properties | 1 + .../src/main/kotlin/ProcessingPlugin.kt | 67 ++++-- java/src/processing/mode/java/JavaEditor.java | 3 + 12 files changed, 286 insertions(+), 45 deletions(-) create mode 100644 app/src/processing/app/gradle/GradlePlugin.kt diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt index c5645c9bb..87f36fe35 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/Sketch.java b/app/src/processing/app/Sketch.java index 8bb50352b..52c0de5b2 100644 --- a/app/src/processing/app/Sketch.java +++ b/app/src/processing/app/Sketch.java @@ -50,6 +50,8 @@ import javax.swing.border.EmptyBorder; * 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 @@ public class Sketch { } 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/GradleJob.kt b/app/src/processing/app/gradle/GradleJob.kt index 006b138f6..28b6b6433 100644 --- a/app/src/processing/app/gradle/GradleJob.kt +++ b/app/src/processing/app/gradle/GradleJob.kt @@ -118,6 +118,7 @@ class GradleJob( 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 val initGradle = workingDir.resolve("init.gradle.kts").apply { val content = """ beforeSettings{ diff --git a/app/src/processing/app/gradle/GradlePlugin.kt b/app/src/processing/app/gradle/GradlePlugin.kt new file mode 100644 index 000000000..0ebbdc36c --- /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 (experimental)", "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 index 23d25fba8..b6a4769c3 100644 --- a/app/src/processing/app/gradle/GradleService.kt +++ b/app/src/processing/app/gradle/GradleService.kt @@ -2,11 +2,13 @@ package processing.app.gradle import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.awt.ComposePanel 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 @@ -28,17 +30,7 @@ class GradleService( val editor: Editor?, ) { val active = mutableStateOf(Preferences.getBoolean("run.use_gradle")) - - val settings = GradleSettings() - - var sketch: Sketch? = null - set (value) { - field = value - if(value == null) return - // If the sketch is set, we start the build process to speed up the first run - startJob("build") - } - + var sketch = mutableStateOf(null) val jobs = mutableStateListOf() val workingDir = createTempDirectory() @@ -61,7 +53,7 @@ class GradleService( val job = GradleJob( tasks = tasks, workingDir = workingDir, - sketch = sketch ?: throw IllegalStateException("Sketch is not set"), + sketch = sketch.value ?: throw IllegalStateException("Sketch is not set"), editor = editor ) jobs.add(job) @@ -72,18 +64,16 @@ class GradleService( jobs.forEach(GradleJob::cancel) } - // Hooks for java to check if the Gradle service is running since mutableStateOf is not accessible in java + // Hooks for java to interact with the Gradle service since mutableStateOf is not accessible in java + fun setSketch(sketch: Sketch){ + this.sketch.value = sketch + startJob("build") + } fun getEnabled(): Boolean { return active.value } fun setEnabled(active: Boolean) { if(!active) stopJobs() this.active.value = active - val editor = editor ?: return - if(active){ - editor.footer?.addPanel(settings, text("gradle.settings"), "/lib/footer/settings") - }else{ - editor.footer?.removePanel(settings) - } } } \ No newline at end of file diff --git a/app/src/processing/app/gradle/GradleSettings.kt b/app/src/processing/app/gradle/GradleSettings.kt index 9c05658ed..8eebd7699 100644 --- a/app/src/processing/app/gradle/GradleSettings.kt +++ b/app/src/processing/app/gradle/GradleSettings.kt @@ -1,23 +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 : JPanel() { - init{ - val compose = ComposePanel() - compose.setContent { - Panel() - } - this.add(compose) - } +class GradleSettings{ companion object{ - @Composable - fun Panel(){ - Text("Gradle settings will be here soon") + 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/api/Sketch.kt b/app/src/processing/app/gradle/api/Sketch.kt index 30e6245d1..635094ec8 100644 --- a/app/src/processing/app/gradle/api/Sketch.kt +++ b/app/src/processing/app/gradle/api/Sketch.kt @@ -53,7 +53,7 @@ class Sketch : SuspendingCliktCommand("sketch") { System.setProperty("java.awt.headless", "false") val service = GradleService(mode,null) - service.sketch = processing.app.Sketch(sketch, mode) + service.sketch.value = processing.app.Sketch(sketch, mode) service.run() // TODO: Use an async way to wait for the job to finish diff --git a/app/src/processing/app/ui/EditorFooter.java b/app/src/processing/app/ui/EditorFooter.java index 827fd90a8..b1d710cb5 100644 --- a/app/src/processing/app/ui/EditorFooter.java +++ b/app/src/processing/app/ui/EditorFooter.java @@ -152,6 +152,7 @@ public class EditorFooter extends Box { public void addPanel(Component comp, String name, String icon) { tabs.add(new Tab(comp, name, icon)); cardPanel.add(name, comp); + repaint(); } /** diff --git a/build/shared/lib/footer/settings.svg b/build/shared/lib/footer/settings.svg index e2d89118a..25a9736ff 100644 --- a/build/shared/lib/footer/settings.svg +++ b/build/shared/lib/footer/settings.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + + + + + diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index a4b6fedf6..7d9b19c52 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -325,6 +325,7 @@ gradle.instructions = About this file: \nProcessing creates this file when you r 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 # --------------------------------------- # Toolbars diff --git a/java/gradle/src/main/kotlin/ProcessingPlugin.kt b/java/gradle/src/main/kotlin/ProcessingPlugin.kt index f84662820..fd7d1b5bf 100644 --- a/java/gradle/src/main/kotlin/ProcessingPlugin.kt +++ b/java/gradle/src/main/kotlin/ProcessingPlugin.kt @@ -11,10 +11,9 @@ import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.tasks.JavaExec import org.jetbrains.compose.ComposeExtension import org.jetbrains.compose.desktop.DesktopExtension -import processing.app.Preferences import java.io.File import java.net.Socket -import java.util.* +import java.util.Properties import javax.inject.Inject class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFactory) : Plugin { @@ -33,7 +32,10 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact val sketchbook = project.findProperty("processing.sketchbook") as String? val settings = project.findProperty("processing.settings") as String? - // Apply the Java plugin to the Project + // Apply the Java plugin to the Project, equivalent of + // plugins { + // java + // } project.plugins.apply(JavaPlugin::class.java) if(isProcessing){ @@ -46,25 +48,67 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact project.tasks.findByName("wrapper")?.enabled = false } - // Add kotlin support + // 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) + val propertiesFile = project.layout.projectDirectory.file("sketch.properties") + if (propertiesFile.asFile.exists()) { + val properties = Properties() + properties.load(propertiesFile.asFile.inputStream()) + + val pluginsSetting = properties.getProperty("sketch.plugins") + if (pluginsSetting != null && pluginsSetting.isNotEmpty()) { + val plugins = pluginsSetting.split(",").map { it.trim() } + + plugins.forEach { pluginId -> + // Apply the plugin to the project, equivalent of + // plugins { + // id("org.processing.java.hotreload") + // } + project.plugins.apply(pluginId) + } + } + } + + // 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 + // 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 + // 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 + // 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 @@ -140,10 +184,9 @@ class ProcessingPlugin @Inject constructor(private val objectFactory: ObjectFact srcDir("$workingDir/unsaved") } sourceSet.allSource.source(pdeSourceSet) - sourceSet.java.srcDir(project.layout.projectDirectory).apply { - include("**/*.java") - exclude("${project.layout.buildDirectory.asFile.get()}/**/*") - } +// 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 -> diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 537a60369..b9dcf0b06 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -62,6 +62,8 @@ import processing.mode.java.tweak.Handle; 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; @@ -205,6 +207,7 @@ public class JavaEditor extends Editor { public EditorFooter createFooter() { EditorFooter footer = super.createFooter(); addErrorTable(footer); + addGradleSettings(footer, service); return footer; }