Skip to content

Commit b0c61a4

Browse files
committed
Welcome Screen: Load & Display examples
1 parent 2f12d13 commit b0c61a4

File tree

2 files changed

+161
-27
lines changed

2 files changed

+161
-27
lines changed

app/src/processing/app/ui/Welcome.kt

Lines changed: 148 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,54 @@ import androidx.compose.foundation.border
66
import androidx.compose.foundation.layout.*
77
import androidx.compose.foundation.lazy.grid.GridCells
88
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
9+
import androidx.compose.foundation.lazy.grid.items
910
import androidx.compose.foundation.shape.RoundedCornerShape
1011
import androidx.compose.material.Checkbox
11-
import androidx.compose.material.MaterialTheme
1212
import androidx.compose.material.MaterialTheme.colors
1313
import androidx.compose.material.MaterialTheme.typography
14-
import androidx.compose.material.RadioButton
1514
import androidx.compose.material.Text
16-
import androidx.compose.runtime.Composable
15+
import androidx.compose.runtime.*
1716
import androidx.compose.ui.Alignment
17+
import androidx.compose.ui.ExperimentalComposeUiApi
1818
import androidx.compose.ui.Modifier
1919
import androidx.compose.ui.draw.clip
2020
import androidx.compose.ui.geometry.Offset
2121
import androidx.compose.ui.graphics.Brush
2222
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
2330
import androidx.compose.ui.res.painterResource
2431
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
2538
import processing.app.Base
2639
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
3145
import java.io.IOException
46+
import java.nio.file.*
47+
import java.nio.file.attribute.BasicFileAttributes
3248
import javax.swing.SwingUtilities
49+
import kotlin.io.path.exists
50+
import kotlin.io.path.inputStream
51+
import kotlin.io.path.isDirectory
3352

3453
class 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
}

app/src/processing/app/ui/theme/Window.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package processing.app.ui.theme
22

3+
import androidx.compose.foundation.background
34
import androidx.compose.foundation.layout.Box
45
import androidx.compose.foundation.layout.padding
56
import androidx.compose.material.MaterialTheme.colors
@@ -8,6 +9,9 @@ import androidx.compose.runtime.Composable
89
import androidx.compose.ui.Alignment
910
import androidx.compose.ui.Modifier
1011
import androidx.compose.ui.awt.ComposePanel
12+
import androidx.compose.ui.geometry.Offset
13+
import androidx.compose.ui.graphics.Brush
14+
import androidx.compose.ui.graphics.Color
1115
import androidx.compose.ui.unit.DpSize
1216
import androidx.compose.ui.unit.dp
1317
import androidx.compose.ui.window.Window
@@ -22,7 +26,7 @@ import java.awt.event.KeyEvent
2226
import javax.swing.JFrame
2327

2428

25-
class PDEWindow(titleKey: String = "", content: @Composable () -> Unit): JFrame(){
29+
class PDEWindow(titleKey: String = "", fullWindowContent: Boolean = false, content: @Composable () -> Unit): JFrame(){
2630
init{
2731
val mac = SystemInfo.isMacFullWindowContentSupported
2832

@@ -38,8 +42,11 @@ class PDEWindow(titleKey: String = "", content: @Composable () -> Unit): JFrame(
3842
val locale = LocalLocale.current
3943
this@PDEWindow.title = locale[titleKey]
4044

41-
Box(modifier = Modifier.padding(top = if (mac) 22.dp else 0.dp)) {
45+
Box(modifier = Modifier
46+
.padding(top = if (mac && !fullWindowContent) 22.dp else 0.dp)
47+
) {
4248
content()
49+
4350
}
4451
}
4552
}
@@ -60,7 +67,7 @@ class PDEWindow(titleKey: String = "", content: @Composable () -> Unit): JFrame(
6067
}
6168
}
6269

63-
fun pdeapplication(titleKey: String = "",content: @Composable () -> Unit){
70+
fun pdeapplication(titleKey: String = "", fullWindowContent: Boolean = false,content: @Composable () -> Unit){
6471
application {
6572
val windowState = rememberWindowState(
6673
size = DpSize.Unspecified,
@@ -75,7 +82,9 @@ fun pdeapplication(titleKey: String = "",content: @Composable () -> Unit){
7582
putClientProperty("apple.awt.transparentTitleBar", mac)
7683
}
7784
Surface(color = colors.background) {
78-
Box(modifier = Modifier.padding(top = if (mac) 22.dp else 0.dp)) {
85+
Box(modifier = Modifier
86+
.padding(top = if (mac && !fullWindowContent) 22.dp else 0.dp)
87+
) {
7988
content()
8089
}
8190
}

0 commit comments

Comments
 (0)