@@ -6,35 +6,54 @@ import androidx.compose.foundation.border
6
6
import androidx.compose.foundation.layout.*
7
7
import androidx.compose.foundation.lazy.grid.GridCells
8
8
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
9
+ import androidx.compose.foundation.lazy.grid.items
9
10
import androidx.compose.foundation.shape.RoundedCornerShape
10
11
import androidx.compose.material.Checkbox
11
- import androidx.compose.material.MaterialTheme
12
12
import androidx.compose.material.MaterialTheme.colors
13
13
import androidx.compose.material.MaterialTheme.typography
14
- import androidx.compose.material.RadioButton
15
14
import androidx.compose.material.Text
16
- import androidx.compose.runtime.Composable
15
+ import androidx.compose.runtime.*
17
16
import androidx.compose.ui.Alignment
17
+ import androidx.compose.ui.ExperimentalComposeUiApi
18
18
import androidx.compose.ui.Modifier
19
19
import androidx.compose.ui.draw.clip
20
20
import androidx.compose.ui.geometry.Offset
21
21
import androidx.compose.ui.graphics.Brush
22
22
import androidx.compose.ui.graphics.Color
23
+ import androidx.compose.ui.graphics.ImageBitmap
24
+ import androidx.compose.ui.graphics.painter.BitmapPainter
25
+ import androidx.compose.ui.input.pointer.PointerEventType
26
+ import androidx.compose.ui.input.pointer.PointerIcon
27
+ import androidx.compose.ui.input.pointer.onPointerEvent
28
+ import androidx.compose.ui.input.pointer.pointerHoverIcon
29
+ import androidx.compose.ui.res.loadImageBitmap
23
30
import androidx.compose.ui.res.painterResource
24
31
import androidx.compose.ui.unit.dp
32
+ import com.formdev.flatlaf.util.SystemInfo
33
+ import kotlinx.coroutines.Dispatchers
34
+ import kotlinx.coroutines.launch
35
+ import kotlinx.coroutines.withContext
36
+ import org.jetbrains.compose.resources.ExperimentalResourceApi
37
+ import org.jetbrains.compose.resources.decodeToImageBitmap
25
38
import processing.app.Base
26
39
import processing.app.LocalPreferences
27
- import processing.app.ui.theme.LocalLocale
28
- import processing.app.ui.theme.PDEButton
29
- import processing.app.ui.theme.PDEWindow
30
- import processing.app.ui.theme.pdeapplication
40
+ import processing.app.Messages
41
+ import processing.app.Platform
42
+ import processing.app.ui.theme.*
43
+ import java.awt.Cursor
44
+ import java.io.File
31
45
import java.io.IOException
46
+ import java.nio.file.*
47
+ import java.nio.file.attribute.BasicFileAttributes
32
48
import javax.swing.SwingUtilities
49
+ import kotlin.io.path.exists
50
+ import kotlin.io.path.inputStream
51
+ import kotlin.io.path.isDirectory
33
52
34
53
class Welcome @Throws(IOException ::class ) constructor(base : Base ) {
35
54
init {
36
55
SwingUtilities .invokeLater {
37
- PDEWindow (" menu.help.welcome" ) {
56
+ PDEWindow (" menu.help.welcome" , fullWindowContent = true ) {
38
57
welcome()
39
58
}
40
59
}
@@ -44,17 +63,16 @@ class Welcome @Throws(IOException::class) constructor(base: Base) {
44
63
fun welcome () {
45
64
Row (
46
65
modifier = Modifier
47
- // .background(
48
- // Brush.linearGradient(
49
- // colorStops = arrayOf(0. 0f to Color.Transparent, 1f to Color.Blue ),
50
- // start = Offset(815f / 2 , 0f),
51
- // end = Offset(815f, 450f)
52
- // )
53
- // )
54
- .size(815 .dp, 450 .dp)
66
+ .background(
67
+ Brush .linearGradient(
68
+ colorStops = arrayOf(0f to Color .Transparent , 1f to Color ( " #C0D7FF " .toColorInt()) ),
69
+ start = Offset (815f , 0f ),
70
+ end = Offset (815f * 2 , 450f )
71
+ )
72
+ )
73
+ .size(815 .dp, 500 .dp)
55
74
.padding(32 .dp)
56
-
57
- ,
75
+ .padding(top = if (SystemInfo .isMacFullWindowContentSupported) 22 .dp else 0 .dp),
58
76
horizontalArrangement = Arrangement .spacedBy(32 .dp)
59
77
){
60
78
Box (modifier = Modifier
@@ -174,22 +192,129 @@ class Welcome @Throws(IOException::class) constructor(base: Base) {
174
192
}
175
193
}
176
194
}
195
+
196
+ data class Example (
197
+ val folder : Path ,
198
+ val library : Path ,
199
+ val path : String = library.resolve("examples").relativize(folder).toString(),
200
+ val title : String = folder.fileName.toString(),
201
+ val image : Path = folder.resolve("$title.png")
202
+ )
203
+
204
+ @Composable
205
+ fun loadExamples (): List <Example > {
206
+ val sketchbook = rememberSketchbookPath()
207
+ val resources = File (System .getProperty(" compose.application.resources.dir" ) ? : " " )
208
+ var examples by remember { mutableStateOf(emptyList<Example >()) }
209
+
210
+ val settingsFolder = Platform .getSettingsFolder()
211
+ val examplesCache = settingsFolder.resolve(" examples.cache" )
212
+ LaunchedEffect (sketchbook, resources){
213
+ if (! examplesCache.exists()) return @LaunchedEffect
214
+ withContext(Dispatchers .IO ) {
215
+ examples = examplesCache.readText().lines().map {
216
+ val (library, folder) = it.split(" ," )
217
+ Example (
218
+ folder = File (folder).toPath(),
219
+ library = File (library).toPath()
220
+ )
221
+ }
222
+ }
223
+ }
224
+
225
+ LaunchedEffect (sketchbook, resources){
226
+ withContext(Dispatchers .IO ) {
227
+ // TODO: Optimize
228
+ Messages .log(" Start scanning for examples in $sketchbook and $resources " )
229
+ // Folders that can contain contributions with examples
230
+ val scanned = listOf (" libraries" , " examples" , " modes" )
231
+ .flatMap { listOf (sketchbook.resolve(it), resources.resolve(it)) }
232
+ .filter { it.exists() && it.isDirectory() }
233
+ // Find contributions within those folders
234
+ .flatMap { Files .list(it.toPath()).toList() }
235
+ .filter { Files .isDirectory(it) }
236
+ // Find examples within those contributions
237
+ .flatMap { library ->
238
+ val fs = FileSystems .getDefault()
239
+ val matcher = fs.getPathMatcher(" glob:**/*.pde" )
240
+ val exampleFolders = mutableListOf<Path >()
241
+ val examples = library.resolve(" examples" )
242
+ if (! Files .exists(examples) || ! examples.isDirectory()) return @flatMap emptyList()
243
+
244
+ Files .walkFileTree(library, object : SimpleFileVisitor <Path >() {
245
+ override fun visitFile (file : Path , attrs : BasicFileAttributes ): FileVisitResult {
246
+ if (matcher.matches(file)) {
247
+ exampleFolders.add(file.parent)
248
+ }
249
+ return FileVisitResult .CONTINUE
250
+ }
251
+ })
252
+ return @flatMap exampleFolders.map { folder ->
253
+ Example (
254
+ folder,
255
+ library,
256
+ )
257
+ }
258
+ }
259
+ .filter { it.image.exists() }
260
+ Messages .log(" Done scanning for examples in $sketchbook and $resources " )
261
+ if (scanned.isEmpty()) return @withContext
262
+ examples = scanned
263
+ examplesCache.writeText(examples.joinToString(" \n " ) { " ${it.library} ,${it.folder} " })
264
+ }
265
+ }
266
+
267
+ return examples
268
+
269
+ }
270
+
271
+ @Composable
272
+ fun rememberSketchbookPath (): File {
273
+ val preferences = LocalPreferences .current
274
+ val sketchbookPath = remember(preferences[" sketchbook.path.four" ]) {
275
+ preferences[" sketchbook.path.four" ] ? : Platform .getDefaultSketchbookFolder().toString()
276
+ }
277
+ return File (sketchbookPath)
278
+ }
279
+
280
+
281
+ @OptIn(ExperimentalResourceApi ::class , ExperimentalComposeUiApi ::class )
177
282
@Composable
178
283
fun examples (){
284
+ val examples = loadExamples()
285
+ // grab 4 random ones
286
+ val randoms = examples.shuffled().take(4 )
287
+
179
288
LazyVerticalGrid (
180
289
columns = GridCells .Fixed (2 ),
181
290
verticalArrangement = Arrangement .spacedBy(16 .dp),
182
291
horizontalArrangement = Arrangement .spacedBy(16 .dp),
183
292
){
184
- items(4 ){
185
- Column {
186
- Box (
293
+ items(randoms){ example ->
294
+ Column (
295
+ modifier = Modifier
296
+ .onPointerEvent(PointerEventType .Press ) {
297
+ }
298
+ .onPointerEvent(PointerEventType .Release ) {
299
+ }
300
+ .onPointerEvent(PointerEventType .Enter ) {
301
+ }
302
+ .onPointerEvent(PointerEventType .Exit ) {
303
+ }
304
+ .pointerHoverIcon(PointerIcon (Cursor (Cursor .HAND_CURSOR )))
305
+ ) {
306
+ val imageBitmap: ImageBitmap = remember(example.image) {
307
+ example.image.inputStream().readAllBytes().decodeToImageBitmap()
308
+ }
309
+ Image (
310
+ painter = BitmapPainter (imageBitmap),
311
+ contentDescription = example.title,
187
312
modifier = Modifier
188
313
.background(colors.primary)
189
314
.width(185 .dp)
190
315
.aspectRatio(16f / 9f )
191
316
)
192
- Text (" Example $it " )
317
+ Text (example.title )
193
318
}
194
319
}
195
320
@@ -211,7 +336,7 @@ class Welcome @Throws(IOException::class) constructor(base: Base) {
211
336
212
337
@JvmStatic
213
338
fun main (args : Array <String >) {
214
- pdeapplication(" menu.help.welcome" ) {
339
+ pdeapplication(" menu.help.welcome" , fullWindowContent = true ) {
215
340
welcome()
216
341
}
217
342
}
0 commit comments