@@ -6,35 +6,54 @@ import androidx.compose.foundation.border
66import androidx.compose.foundation.layout.*
77import androidx.compose.foundation.lazy.grid.GridCells
88import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
9+ import androidx.compose.foundation.lazy.grid.items
910import androidx.compose.foundation.shape.RoundedCornerShape
1011import androidx.compose.material.Checkbox
11- import androidx.compose.material.MaterialTheme
1212import androidx.compose.material.MaterialTheme.colors
1313import androidx.compose.material.MaterialTheme.typography
14- import androidx.compose.material.RadioButton
1514import androidx.compose.material.Text
16- import androidx.compose.runtime.Composable
15+ import androidx.compose.runtime.*
1716import androidx.compose.ui.Alignment
17+ import androidx.compose.ui.ExperimentalComposeUiApi
1818import androidx.compose.ui.Modifier
1919import androidx.compose.ui.draw.clip
2020import androidx.compose.ui.geometry.Offset
2121import androidx.compose.ui.graphics.Brush
2222import 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
2330import androidx.compose.ui.res.painterResource
2431import 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
2538import processing.app.Base
2639import 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
3145import java.io.IOException
46+ import java.nio.file.*
47+ import java.nio.file.attribute.BasicFileAttributes
3248import javax.swing.SwingUtilities
49+ import kotlin.io.path.exists
50+ import kotlin.io.path.inputStream
51+ import kotlin.io.path.isDirectory
3352
3453class Welcome @Throws(IOException ::class ) constructor(base : Base ) {
3554 init {
3655 SwingUtilities .invokeLater {
37- PDEWindow (" menu.help.welcome" ) {
56+ PDEWindow (" menu.help.welcome" , fullWindowContent = true ) {
3857 welcome()
3958 }
4059 }
@@ -44,17 +63,16 @@ class Welcome @Throws(IOException::class) constructor(base: Base) {
4463 fun welcome () {
4564 Row (
4665 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)
5574 .padding(32 .dp)
56-
57- ,
75+ .padding(top = if (SystemInfo .isMacFullWindowContentSupported) 22 .dp else 0 .dp),
5876 horizontalArrangement = Arrangement .spacedBy(32 .dp)
5977 ){
6078 Box (modifier = Modifier
@@ -174,22 +192,129 @@ class Welcome @Throws(IOException::class) constructor(base: Base) {
174192 }
175193 }
176194 }
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 )
177282 @Composable
178283 fun examples (){
284+ val examples = loadExamples()
285+ // grab 4 random ones
286+ val randoms = examples.shuffled().take(4 )
287+
179288 LazyVerticalGrid (
180289 columns = GridCells .Fixed (2 ),
181290 verticalArrangement = Arrangement .spacedBy(16 .dp),
182291 horizontalArrangement = Arrangement .spacedBy(16 .dp),
183292 ){
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,
187312 modifier = Modifier
188313 .background(colors.primary)
189314 .width(185 .dp)
190315 .aspectRatio(16f / 9f )
191316 )
192- Text (" Example $it " )
317+ Text (example.title )
193318 }
194319 }
195320
@@ -211,7 +336,7 @@ class Welcome @Throws(IOException::class) constructor(base: Base) {
211336
212337 @JvmStatic
213338 fun main (args : Array <String >) {
214- pdeapplication(" menu.help.welcome" ) {
339+ pdeapplication(" menu.help.welcome" , fullWindowContent = true ) {
215340 welcome()
216341 }
217342 }
0 commit comments