Skip to content

Commit bc063e8

Browse files
committed
Gradle Runner PoC
1 parent 47d3428 commit bc063e8

File tree

7 files changed

+303
-2
lines changed

7 files changed

+303
-2
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,5 @@ java/build/
120120
/app/windows/obj
121121
/java/gradle/build
122122
/java/gradle/example/.processing
123+
/java/android/example/build
124+
/java/android/example/.processing

app/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ dependencies {
120120
testImplementation(libs.mockitoKotlin)
121121
testImplementation(libs.junitJupiter)
122122
testImplementation(libs.junitJupiterParams)
123+
124+
implementation(gradleApi())
125+
runtimeOnly(libs.slf4j.simple)
123126
}
124127

125128
tasks.test {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package processing.app.gradle
2+
3+
import androidx.compose.runtime.mutableStateListOf
4+
import androidx.compose.runtime.mutableStateOf
5+
import kotlinx.coroutines.*
6+
import org.gradle.tooling.BuildLauncher
7+
import org.gradle.tooling.GradleConnector
8+
import org.gradle.tooling.ProjectConnection
9+
import org.gradle.tooling.events.ProgressListener
10+
import org.gradle.tooling.events.task.TaskFinishEvent
11+
import org.gradle.tooling.events.task.TaskStartEvent
12+
import processing.app.Base
13+
import processing.app.Messages
14+
import processing.app.ui.Editor
15+
import java.io.File
16+
import javax.swing.SwingUtilities
17+
import javax.swing.event.DocumentEvent
18+
import javax.swing.event.DocumentListener
19+
20+
21+
class GradleService(val editor: Editor) {
22+
val availableTasks = mutableStateListOf<String>()
23+
val finishedTasks = mutableStateListOf<String>()
24+
val running = mutableStateOf(false)
25+
26+
private var connection: ProjectConnection? = null
27+
private var preparation: Job? = null
28+
private var preparing = false
29+
30+
private var run: Job? = null
31+
private var cancel = GradleConnector.newCancellationTokenSource()
32+
33+
val folder: File get() = editor.sketch.folder
34+
35+
fun prepare(){
36+
if(preparing) return
37+
preparation?.cancel()
38+
preparation = CoroutineScope(Dispatchers.IO).launch {
39+
val connection = connection ?: return@launch
40+
delay(1000)
41+
preparing = true
42+
43+
connection.newSketchBuild()
44+
.forTasks("build")
45+
.run()
46+
47+
preparing = false
48+
}
49+
}
50+
51+
fun run(){
52+
val connection = connection ?: return
53+
if(!preparing) preparation?.cancel()
54+
55+
run?.cancel()
56+
run = CoroutineScope(Dispatchers.IO).launch {
57+
preparation?.join()
58+
cancel.cancel()
59+
cancel = GradleConnector.newCancellationTokenSource()
60+
connection.newSketchBuild()
61+
.forTasks("run")
62+
.withCancellationToken(cancel.token())
63+
.run()
64+
}
65+
66+
CoroutineScope(Dispatchers.IO).launch {
67+
running.value = true
68+
run?.join()
69+
running.value = false
70+
}
71+
}
72+
73+
fun stop(){
74+
cancel.cancel()
75+
}
76+
77+
fun startService(){
78+
Messages.log("Starting Gradle service at ${folder}")
79+
80+
connection = GradleConnector.newConnector()
81+
.forProjectDirectory(folder)
82+
.connect()
83+
84+
// TODO: recreate connection if sketch folder changes
85+
86+
SwingUtilities.invokeLater {
87+
editor.sketch.code.forEach {
88+
it.document.addDocumentListener(object : DocumentListener {
89+
override fun insertUpdate(e: DocumentEvent) {
90+
prepare()
91+
}
92+
93+
override fun removeUpdate(e: DocumentEvent) {
94+
prepare()
95+
}
96+
97+
override fun changedUpdate(e: DocumentEvent) {
98+
prepare()
99+
}
100+
})
101+
}
102+
103+
// TODO: Attach listener if new tab is created
104+
}
105+
}
106+
107+
108+
109+
110+
fun ProjectConnection.newSketchBuild(): BuildLauncher{
111+
finishedTasks.clear()
112+
113+
val buildGradle = folder.resolve("build.gradle.kts")
114+
if(!buildGradle.exists()){
115+
Messages.log("build.gradle.kts not found in ${folder}, creating one")
116+
val content = """
117+
plugins{
118+
id("processing.java.gradle") version "${Base.getVersionName()}"
119+
}
120+
""".trimIndent()
121+
buildGradle.writeText(content)
122+
}
123+
124+
val settingsGradle = folder.resolve("settings.gradle.kts")
125+
if(!settingsGradle.exists()){
126+
Messages.log("settings.gradle.kts not found in ${folder}, creating one")
127+
val content = """
128+
pluginManagement {
129+
repositories {
130+
mavenLocal()
131+
mavenCentral()
132+
}
133+
}
134+
""".trimIndent()
135+
settingsGradle.writeText(content)
136+
}
137+
138+
return this.newBuild()
139+
.addProgressListener(ProgressListener { event ->
140+
val name = event.descriptor.name
141+
if(event is TaskStartEvent) {
142+
if(!availableTasks.contains(name)) availableTasks.add(name)
143+
}
144+
if(event is TaskFinishEvent){
145+
finishedTasks.add(name)
146+
}
147+
})
148+
// .setStandardOutput(System.out)
149+
// .setJavaHome(Platform.getJavaHome())
150+
}
151+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package processing.app.gradle.ui
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.combinedClickable
5+
import androidx.compose.foundation.layout.*
6+
import androidx.compose.foundation.shape.CircleShape
7+
import androidx.compose.material.*
8+
import androidx.compose.material.icons.Icons
9+
import androidx.compose.material.icons.filled.Close
10+
import androidx.compose.material.icons.filled.PlayArrow
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.ExperimentalComposeUiApi
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.awt.ComposePanel
15+
import androidx.compose.ui.awt.SwingPanel
16+
import androidx.compose.ui.draw.clip
17+
import androidx.compose.ui.graphics.Brush
18+
import androidx.compose.ui.graphics.Color
19+
import androidx.compose.ui.graphics.colorspace.ColorSpace
20+
import androidx.compose.ui.graphics.colorspace.ColorSpaces
21+
import androidx.compose.ui.input.pointer.PointerEventType
22+
import androidx.compose.ui.input.pointer.onPointerEvent
23+
import androidx.compose.ui.unit.dp
24+
import processing.app.ui.Editor
25+
import processing.app.ui.EditorToolbar
26+
import processing.app.ui.Theme
27+
import javax.swing.JComponent
28+
29+
class Toolbar(val editor: Editor) {
30+
companion object{
31+
@JvmStatic
32+
fun legacyWrapped(editor: Editor, toolbar: EditorToolbar): JComponent {
33+
val bar = Toolbar(editor)
34+
val panel = ComposePanel().apply {
35+
setContent {
36+
// TODO: Dynamically switch between the toolbars
37+
val displayNew = true
38+
if(displayNew){
39+
bar.display()
40+
return@setContent
41+
}
42+
SwingPanel(factory = {
43+
toolbar
44+
}, modifier = Modifier.fillMaxWidth().height(56.dp))
45+
}
46+
}
47+
48+
return panel
49+
}
50+
}
51+
52+
@OptIn(ExperimentalComposeUiApi::class)
53+
@Composable
54+
fun display(){
55+
56+
val startColor = Theme.getColor("toolbar.gradient.top")
57+
val endColor = Theme.getColor("toolbar.gradient.bottom")
58+
val colorStops = arrayOf(
59+
0.0f to startColor.toComposeColor(),
60+
1f to endColor.toComposeColor()
61+
)
62+
Row(
63+
modifier = Modifier.background(Brush.verticalGradient(colorStops = colorStops))
64+
.fillMaxWidth()
65+
.padding(start = Editor.LEFT_GUTTER.dp)
66+
.padding(vertical = 11.dp)
67+
,
68+
horizontalArrangement = Arrangement.spacedBy(16.dp)
69+
){
70+
val available = editor.service.availableTasks
71+
val finished = editor.service.finishedTasks
72+
val isRunning = editor.service.running.value
73+
Surface(
74+
modifier = Modifier
75+
.onPointerEvent(PointerEventType.Press){
76+
editor.service.run()
77+
}
78+
.height(34.dp)
79+
.clip(CircleShape)
80+
.aspectRatio(1f)
81+
.background(Color.White)
82+
83+
){
84+
if(isRunning) {
85+
CircularProgressIndicator(
86+
progress = finished.count().toFloat() / available.count() ,
87+
color = startColor.toComposeColor()
88+
)
89+
}
90+
Icon(
91+
imageVector = Icons.Filled.PlayArrow,
92+
contentDescription = "Play",
93+
tint = Color.Black,
94+
modifier = Modifier
95+
.size(2.dp)
96+
)
97+
}
98+
if(isRunning){
99+
Surface(
100+
modifier = Modifier
101+
.onPointerEvent(PointerEventType.Press){
102+
editor.service.stop()
103+
}
104+
.height(34.dp)
105+
.clip(CircleShape)
106+
.aspectRatio(1f)
107+
.background(Color.White)
108+
) {
109+
Icon(
110+
imageVector = Icons.Filled.Close,
111+
contentDescription = "Stop",
112+
tint = Color.Black,
113+
modifier = Modifier.size(12.dp)
114+
)
115+
}
116+
}
117+
}
118+
}
119+
}
120+
121+
fun java.awt.Color.toComposeColor(): Color {
122+
return Color(
123+
red = this.red / 255f,
124+
green = this.green / 255f,
125+
blue = this.blue / 255f,
126+
alpha = this.alpha / 255f
127+
)
128+
}

app/src/processing/app/ui/Editor.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
import javax.swing.text.html.*;
4949
import javax.swing.undo.*;
5050

51-
import com.formdev.flatlaf.FlatLaf;
5251
import com.formdev.flatlaf.util.SystemInfo;
5352
import processing.app.Base;
5453
import processing.app.Formatter;
@@ -63,6 +62,8 @@
6362
import processing.app.SketchCode;
6463
import processing.app.SketchException;
6564
import processing.app.contrib.ContributionManager;
65+
import processing.app.gradle.GradleService;
66+
import processing.app.gradle.ui.Toolbar;
6667
import processing.app.laf.PdeMenuItemUI;
6768
import processing.app.syntax.*;
6869
import processing.core.*;
@@ -75,6 +76,7 @@ public abstract class Editor extends JFrame implements RunnerListener {
7576
protected Base base;
7677
protected EditorState state;
7778
protected Mode mode;
79+
protected GradleService service;
7880

7981
// There may be certain gutter sizes that cause text bounds
8082
// inside the console to be calculated incorrectly.
@@ -157,6 +159,7 @@ protected Editor(final Base base, String path, final EditorState state,
157159
this.base = base;
158160
this.state = state;
159161
this.mode = mode;
162+
this.service = new GradleService(this);
160163

161164
// Make sure Base.getActiveEditor() never returns null
162165
base.checkFirstEditor(this);
@@ -220,7 +223,9 @@ public void windowDeactivated(WindowEvent e) {
220223

221224
rebuildModePopup();
222225
toolbar = createToolbar();
223-
upper.add(toolbar);
226+
// Wrapping the toolbar to be able to switch build systems dynamically
227+
var wrapped = Toolbar.legacyWrapped(this, toolbar);
228+
upper.add(wrapped);
224229

225230
header = createHeader();
226231
upper.add(header);
@@ -477,6 +482,9 @@ public Mode getMode() {
477482
return mode;
478483
}
479484

485+
public GradleService getService() {
486+
return service;
487+
}
480488

481489
public void repaintHeader() {
482490
header.repaint();
@@ -2244,6 +2252,7 @@ protected void handleOpenInternal(String path) throws EditorException {
22442252
} catch (IOException e) {
22452253
throw new EditorException("Could not create the sketch.", e);
22462254
}
2255+
service.startService();
22472256

22482257
header.rebuild();
22492258
updateTitle();

build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ plugins {
66
alias(libs.plugins.jetbrainsCompose) apply false
77
}
88

9+
allprojects{
10+
repositories{
11+
maven { url = uri("https://repo.gradle.org/gradle/libs-releases") }
12+
}
13+
}
14+
915
// Set the build directory to not /build to prevent accidental deletion through the clean action
1016
// Can be deleted after the migration to Gradle is complete
1117
layout.buildDirectory = file(".build")

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", ver
3333
kotlinComposePlugin = { module = "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin", version.ref = "kotlin" }
3434
markdown = { module = "com.mikepenz:multiplatform-markdown-renderer-m2", version = "0.31.0" }
3535
markdownJVM = { module = "com.mikepenz:multiplatform-markdown-renderer-jvm", version = "0.31.0" }
36+
gradle-tooling-api = { module = "org.gradle:gradle-tooling-api", version = "8.12" }
37+
slf4j-simple = { module = "org.slf4j:slf4j-simple", version = "1.7.10" }
3638

3739
[plugins]
3840
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }

0 commit comments

Comments
 (0)