From 0ce1687abf5a0660d4dc15dafffd389fd461ab32 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 3 Nov 2025 12:01:42 +0100 Subject: [PATCH 1/8] Preferences screen Adds most of the options for the preferences screen based on the new design --- app/src/processing/app/Base.java | 33 +- app/src/processing/app/Preferences.kt | 12 +- app/src/processing/app/ui/EditorFooter.java | 51 +- app/src/processing/app/ui/PDEPreferences.kt | 544 ++++++++++++++++++ app/src/processing/app/ui/Preferences.kt | 323 ----------- .../processing/app/ui/preferences/Coding.kt | 69 +++ .../processing/app/ui/preferences/General.kt | 93 +-- .../app/ui/preferences/Interface.kt | 236 ++++---- .../processing/app/ui/preferences/Other.kt | 109 ++-- .../processing/app/ui/preferences/Sketches.kt | 146 +++++ build/shared/lib/languages/PDE.properties | 39 +- 11 files changed, 1096 insertions(+), 559 deletions(-) create mode 100644 app/src/processing/app/ui/PDEPreferences.kt delete mode 100644 app/src/processing/app/ui/Preferences.kt create mode 100644 app/src/processing/app/ui/preferences/Coding.kt create mode 100644 app/src/processing/app/ui/preferences/Sketches.kt diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index fe20b82da..1d3bb27b4 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -23,28 +23,29 @@ package processing.app; -import java.awt.*; -import java.awt.event.ActionListener; -import java.io.*; -import java.lang.reflect.InvocationTargetException; -import java.util.*; -import java.util.List; -import java.util.Map.Entry; - -import javax.swing.*; -import javax.swing.tree.DefaultMutableTreeNode; - import com.formdev.flatlaf.FlatDarkLaf; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.FlatLightLaf; import processing.app.contrib.*; import processing.app.tools.Tool; import processing.app.ui.*; -import processing.app.ui.PreferencesKt; import processing.app.ui.Toolkit; -import processing.core.*; +import processing.core.PApplet; import processing.data.StringList; +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import java.awt.*; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.List; +import java.util.Map.Entry; + /** * The base class for the main processing application. * Primary role of this class is for platform identification and @@ -2185,11 +2186,7 @@ static private Mode findSketchMode(File folder, List modeList) { * Show the Preferences window. */ public void handlePrefs() { -// if (preferencesFrame == null) { -// preferencesFrame = new PreferencesFrame(this); -// } -// preferencesFrame.showFrame(); - PreferencesKt.show(); + PDEPreferencesKt.show(); } diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt index c54cbbd81..c13309299 100644 --- a/app/src/processing/app/Preferences.kt +++ b/app/src/processing/app/Preferences.kt @@ -8,8 +8,11 @@ import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.launch import java.io.File import java.io.InputStream -import java.nio.file.* -import java.util.Properties +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds +import java.nio.file.WatchEvent +import java.util.* /* The ReactiveProperties class extends the standard Java Properties class @@ -28,6 +31,11 @@ class ReactiveProperties: Properties() { return snapshotStateMap[key] ?: super.getProperty(key) } + override fun remove(key: Any?): Any? { + snapshotStateMap.remove(key as String) + return super.remove(key) + } + operator fun get(key: String): String? = getProperty(key) operator fun set(key: String, value: String) { diff --git a/app/src/processing/app/ui/EditorFooter.java b/app/src/processing/app/ui/EditorFooter.java index bc09b2376..94860a0ab 100644 --- a/app/src/processing/app/ui/EditorFooter.java +++ b/app/src/processing/app/ui/EditorFooter.java @@ -22,15 +22,14 @@ package processing.app.ui; -import java.awt.CardLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.datatransfer.Clipboard; +import processing.app.Base; +import processing.app.Mode; +import processing.app.Sketch; +import processing.app.contrib.ContributionManager; +import processing.data.StringDict; + +import javax.swing.*; +import java.awt.*; import java.awt.datatransfer.StringSelection; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -39,14 +38,6 @@ import java.util.ArrayList; import java.util.List; -import javax.swing.*; - -import processing.app.Base; -import processing.app.Mode; -import processing.app.Sketch; -import processing.app.contrib.ContributionManager; -import processing.data.StringDict; - /** * Console/error/whatever tabs at the bottom of the editor window. @@ -118,6 +109,18 @@ public void mousePressed(MouseEvent e) { Base.DEBUG = !Base.DEBUG; editor.updateDevelopMenu(); } + copyDebugInformationToClipboard(); + } + }); + + tabBar.add(version); + + add(tabBar); + + updateTheme(); + } + + public static void copyDebugInformationToClipboard() { var debugInformation = String.join("\n", "Version: " + Base.getVersionName(), "Revision: " + Base.getRevision(), @@ -127,18 +130,12 @@ public void mousePressed(MouseEvent e) { var stringSelection = new StringSelection(debugInformation); var clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); - } - }); - - tabBar.add(version); - - add(tabBar); - - updateTheme(); - } + } - /** Add a panel with no icon. */ + /** + * Add a panel with no icon. + */ public void addPanel(Component comp, String name) { addPanel(comp, name, null); } diff --git a/app/src/processing/app/ui/PDEPreferences.kt b/app/src/processing/app/ui/PDEPreferences.kt new file mode 100644 index 000000000..2ec90ebdb --- /dev/null +++ b/app/src/processing/app/ui/PDEPreferences.kt @@ -0,0 +1,544 @@ +package processing.app.ui + +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import com.mikepenz.markdown.compose.Markdown +import com.mikepenz.markdown.m3.markdownColor +import com.mikepenz.markdown.m3.markdownTypography +import processing.app.LocalPreferences +import processing.app.ui.PDEPreferences.Companion.preferences +import processing.app.ui.preferences.* +import processing.app.ui.theme.LocalLocale +import processing.app.ui.theme.PDESwingWindow +import processing.app.ui.theme.PDETheme +import java.awt.Dimension +import javax.swing.SwingUtilities + +fun show() { + SwingUtilities.invokeLater { + PDESwingWindow( + titleKey = "preferences", + fullWindowContent = true, + size = Dimension(800, 600) + ) { + PDETheme { + preferences() + } + } + } +} + +class PDEPreferences { + companion object{ + private val panes: PDEPreferencePanes = mutableStateMapOf() + + /** + * Registers a new preference in the preferences' system. + * If the preference's pane does not exist, it will be created. + * Usage: + * ``` + * PDEPreferences.register( + * PDEPreference( + * key = "preference.key", + * descriptionKey = "preference.description", + * pane = somePreferencePane, + * control = { preference, updatePreference -> + * // Composable UI to modify the preference + * } + * ) + * ) + * ``` + * + * @param preferences The preference to register. + */ + fun register(vararg preferences: PDEPreference) { + if (preferences.map { it.pane }.toSet().size != 1) { + throw IllegalArgumentException("All preferences must belong to the same pane") + } + val pane = preferences.first().pane + + val group = mutableStateListOf() + group.addAll(preferences) + + val groups = panes[pane] as? SnapshotStateList ?: mutableStateListOf() + groups.add(group) + panes[pane] = groups + } + + /** + * Static initializer to register default preference panes. + */ + init{ + General.register() + Interface.register() + Coding.register() + Sketches.register() + Other.register(panes) + } + + /** + * Composable function to display the preferences UI. + */ + @OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) + @Composable + fun preferences(){ + val locale = LocalLocale.current + var preferencesQuery by remember { mutableStateOf("") } + + /** + * Filter panes based on the search query. + */ + val panesQuierried = remember(preferencesQuery, panes) { + if (preferencesQuery.isBlank()) { + panes.toMutableMap() + } else { + panes.entries.associate { (pane, preferences) -> + val matching = preferences.map { group -> + group.filter { preference -> + val description = locale[preference.descriptionKey] + when { + preference.key == "other" -> true + preference.key.contains(preferencesQuery, ignoreCase = true) -> true + description.contains(preferencesQuery, ignoreCase = true) -> true + else -> false + } + } + } + pane to matching + }.toMutableMap() + } + } + + /** + * Sort panes based on their 'after' property and name. + */ + val panesSorted = remember(panesQuierried) { + panesQuierried.keys.sortedWith { a, b -> + when { + a === b -> 0 + a.after == b -> 1 + b.after == a -> -1 + a.after == null && b.after != null -> -1 + b.after == null && a.after != null -> 1 + else -> a.nameKey.compareTo(b.nameKey) + } + } + } + + + /** + * Pre-select a pane that has at least one preference to show + * Also reset the selection when the query changes + * */ + var selected by remember(panesQuierried) { + mutableStateOf(panesSorted.firstOrNull() { panesQuierried[it].isNotEmpty() }) + } + + Column { + /** + * Header + */ + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 36.dp, top = 48.dp, end = 24.dp, bottom = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom + ) { + Column( + modifier = Modifier + .weight(1f) + ) { + Text( + text = locale["preferences"], + style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Medium), + ) + Text( + text = locale["preferences.description"], + style = MaterialTheme.typography.bodySmall, + ) + } + SearchBar( + modifier = Modifier, + inputField = { + SearchBarDefaults.InputField( + query = preferencesQuery, + onQueryChange = { + preferencesQuery = it + }, + onSearch = { + + }, + trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + expanded = false, + onExpandedChange = { }, + placeholder = { Text("Search") } + ) + }, + expanded = false, + onExpandedChange = {}, + ) { + + } + } + HorizontalDivider() + Row( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceVariant) + ) { + /** + * Sidebar + */ + Column( + modifier = Modifier + .width(IntrinsicSize.Min) + .padding(30.dp) + .background(MaterialTheme.colorScheme.surfaceVariant) + ) { + + for (pane in panesSorted) { + val shape = RoundedCornerShape(12.dp) + val isSelected = selected == pane + TextButton( + onClick = { + selected = pane + }, + enabled = panesQuierried[pane].isNotEmpty(), + colors = if (isSelected) ButtonDefaults.buttonColors() else ButtonDefaults.textButtonColors(), + shape = shape + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 4.dp, top = 8.dp, end = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + pane.icon() + Text(locale[pane.nameKey]) + } + } + } + } + + /** + * Content Area + */ + AnimatedContent( + targetState = selected, + transitionSpec = { + fadeIn( + animationSpec = tween(300) + ) togetherWith fadeOut( + animationSpec = tween(300) + ) + } + ) { selected -> + if (selected == null) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(30.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = locale["preferences.no_results"], + style = MaterialTheme.typography.bodyMedium + ) + } + return@AnimatedContent + } + + val groups = panesQuierried[selected] ?: emptyList() + selected.showPane(groups) + } + } + } + } + + /** + * Main function to run the preferences window standalone for testing & development. + */ + @JvmStatic + fun main(args: Array) { + application { + Window(onCloseRequest = ::exitApplication) { + remember { + window.rootPane.putClientProperty("apple.awt.fullWindowContent", true) + window.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) + } + PDETheme(darkTheme = true) { + preferences() + } + } + Window(onCloseRequest = ::exitApplication) { + remember { + window.rootPane.putClientProperty("apple.awt.fullWindowContent", true) + window.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) + } + PDETheme(darkTheme = false) { + preferences() + } + } + } + } + } +} + +typealias PDEPreferencePanes = MutableMap +typealias PDEPreferenceGroups = List +typealias PDEPreferenceGroup = List +typealias PDEPreferenceControl = @Composable (preference: String?, updatePreference: (newValue: String) -> Unit) -> Unit + +/** + * Data class representing a pane of preferences. + */ +data class PDEPreferencePane( + /** + * The name key of this pane from the Processing locale. + */ + val nameKey: String, + /** + * The icon representing this pane. + */ + val icon: @Composable () -> Unit, + /** + * The pane that comes before this one in the list. + */ + val after: PDEPreferencePane? = null, +) + +/** + * Composable function to display the contents of a preference pane. + */ +@Composable +fun PDEPreferencePane.showPane(groups: PDEPreferenceGroups) { + Box { + val locale = LocalLocale.current + val state = rememberLazyListState() + LazyColumn( + state = state, + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(20.dp), + contentPadding = PaddingValues(top = 30.dp, end = 30.dp, bottom = 30.dp) + ) { + item { + Text( + text = locale[nameKey], + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Medium), + ) + } + items(groups) { group -> + Card( + modifier = Modifier + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant + ), + ) { + group.forEachIndexed { index, preference -> + preference.showControl() + if (index != group.lastIndex) { + HorizontalDivider() + } + } + + } + } + item { + val prefs = LocalPreferences.current + TextButton( + onClick = { + groups.forEach { group -> + group.forEach { pref -> + prefs.remove(pref.key) + } + } + } + ) { + Text( + text = locale["preferences.reset"], + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + VerticalScrollbar( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(12.dp) + .fillMaxHeight(), + adapter = rememberScrollbarAdapter(state) + ) + } +} + +/** + * Data class representing a single preference in the preferences' system. + * + * Usage: + * ``` + * PDEPreferences.register( + * PDEPreference( + * key = "preference.key", + * descriptionKey = "preference.description", + * group = somePreferenceGroup, + * control = { preference, updatePreference -> + * // Composable UI to modify the preference + * } + * ) + * ) + * ``` + */ +data class PDEPreference( + /** + * The key in the preferences file used to store this preference. + */ + val key: String, + /** + * The key for the description of this preference, used for localization. + */ + val descriptionKey: String, + + /** + * The key for the label of this preference, used for localization. + * If null, the label will not be shown. + */ + val labelKey: String? = null, + /** + * The group this preference belongs to. + */ + val pane: PDEPreferencePane, + /** + * A Composable function that defines the control used to modify this preference. + * It takes the current preference value and a function to update the preference. + */ + val control: PDEPreferenceControl = { preference, updatePreference -> }, + + /** + * If true, no padding will be applied around this preference's UI. + */ + val noPadding: Boolean = false, + /** + * If true, the title will be omitted from this preference's UI. + */ + val noTitle: Boolean = false, +) + +/** + * Extension function to check if a list of preference groups is not empty. + */ +fun PDEPreferenceGroups?.isNotEmpty(): Boolean { + if (this == null) return false + for (group in this) { + if (group.isNotEmpty()) return true + } + return false +} + +/** + * Composable function to display the preference's description and control. + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun PDEPreference.showControl() { + val locale = LocalLocale.current + val prefs = LocalPreferences.current + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + if (!noTitle) { + Column( + modifier = Modifier + .weight(1f) + + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(horizontal = 20.dp) + ) { + Text( + text = locale[descriptionKey], + style = MaterialTheme.typography.bodyMedium + ) + if (labelKey != null && locale.containsKey(labelKey)) { + Card { + Text( + text = locale[labelKey], + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(8.dp, 4.dp) + ) + } + } + } + if (locale.containsKey("$descriptionKey.tip")) { + Markdown( + content = locale["$descriptionKey.tip"], + colors = markdownColor( + text = MaterialTheme.colorScheme.onSurfaceVariant, + ), + typography = markdownTypography( + text = MaterialTheme.typography.bodySmall, + paragraph = MaterialTheme.typography.bodySmall, + textLink = TextLinkStyles( + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline + ).toSpanStyle() + ) + ), + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 4.dp) + ) + } + } + } + val show = @Composable { + control(prefs[key]) { newValue -> + prefs[key] = newValue + } + } + + if (noPadding) { + show() + } else { + Box( + modifier = Modifier + .padding(horizontal = 20.dp) + ) { + show() + } + } + } +} \ No newline at end of file diff --git a/app/src/processing/app/ui/Preferences.kt b/app/src/processing/app/ui/Preferences.kt deleted file mode 100644 index 12e7c25ce..000000000 --- a/app/src/processing/app/ui/Preferences.kt +++ /dev/null @@ -1,323 +0,0 @@ -package processing.app.ui - -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.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationRail -import androidx.compose.material3.NavigationRailItem -import androidx.compose.material3.SearchBar -import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -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.geometry.Size -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.WindowPlacement -import androidx.compose.ui.window.WindowPosition -import androidx.compose.ui.window.application -import androidx.compose.ui.window.rememberWindowState -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.debounce -import processing.app.LocalPreferences -import processing.app.ui.PDEPreferences.Companion.preferences -import processing.app.ui.preferences.General -import processing.app.ui.preferences.Interface -import processing.app.ui.preferences.Other -import processing.app.ui.theme.LocalLocale -import processing.app.ui.theme.PDESwingWindow -import processing.app.ui.theme.PDETheme -import java.awt.Dimension -import javax.swing.SwingUtilities - -val LocalPreferenceGroups = compositionLocalOf>> { - error("No Preference Groups Set") -} - -class PDEPreferences { - companion object{ - val groups = mutableStateMapOf>() - fun register(preference: PDEPreference) { - val list = groups[preference.group]?.toMutableList() ?: mutableListOf() - list.add(preference) - groups[preference.group] = list - } - init{ - General.register() - Interface.register() - Other.register() - } - - /** - * Composable function to display the preferences UI. - */ - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun preferences(){ - var visible by remember { mutableStateOf(groups) } - val sortedGroups = remember { - val keys = visible.keys - keys.toSortedSet { - a, b -> - when { - a.after == b -> 1 - b.after == a -> -1 - else -> a.name.compareTo(b.name) - } - } - } - var selected by remember { mutableStateOf(sortedGroups.first()) } - CompositionLocalProvider( - LocalPreferenceGroups provides visible - ) { - Row { - NavigationRail( - header = { - Text( - "Settings", - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(top = 42.dp) - ) - }, - modifier = Modifier - .defaultMinSize(minWidth = 200.dp) - ) { - - for (group in sortedGroups) { - NavigationRailItem( - selected = selected == group, - enabled = visible.keys.contains(group), - onClick = { - selected = group - }, - icon = { - group.icon() - }, - label = { - Text(group.name) - } - ) - } - } - Box(modifier = Modifier.padding(top = 42.dp)) { - Column(modifier = Modifier - .fillMaxSize() - ) { - var query by remember { mutableStateOf("") } - val locale = LocalLocale.current - LaunchedEffect(query){ - - snapshotFlow { query } - .debounce(100) - .collect{ - if(it.isBlank()){ - visible = groups - return@collect - } - val filtered = mutableStateMapOf>() - for((group, preferences) in groups){ - val matching = preferences.filter { preference -> - if(preference.key == "other"){ - return@filter true - } - if(preference.key.contains(it, ignoreCase = true)){ - return@filter true - } - val description = locale[preference.descriptionKey] - description.contains(it, ignoreCase = true) - } - if(matching.isNotEmpty()){ - filtered[group] = matching - } - } - visible = filtered - } - - } - SearchBar( - inputField = { - SearchBarDefaults.InputField( - query = query, - onQueryChange = { - query = it - }, - onSearch = { - - }, - expanded = false, - onExpandedChange = { }, - placeholder = { Text("Search") } - ) - }, - expanded = false, - onExpandedChange = {}, - modifier = Modifier.align(Alignment.End).padding(16.dp) - ) { - - } - - val preferences = visible[selected] ?: emptyList() - LazyColumn( - verticalArrangement = Arrangement.spacedBy(20.dp) - ) { - items(preferences){ preference -> - preference.showControl() - } - } - } - } - } - } - } - - - - @JvmStatic - fun main(args: Array) { - application { - Window(onCloseRequest = ::exitApplication){ - remember{ - window.rootPane.putClientProperty("apple.awt.fullWindowContent", true) - window.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) - } - PDETheme(darkTheme = true) { - preferences() - } - } - Window(onCloseRequest = ::exitApplication){ - remember{ - window.rootPane.putClientProperty("apple.awt.fullWindowContent", true) - window.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) - } - PDETheme(darkTheme = false) { - preferences() - } - } - } - } - } -} - -/** - * Data class representing a single preference in the preferences system. - * - * Usage: - * ``` - * PDEPreferences.register( - * PDEPreference( - * key = "preference.key", - * descriptionKey = "preference.description", - * group = somePreferenceGroup, - * control = { preference, updatePreference -> - * // Composable UI to modify the preference - * } - * ) - * ) - * ``` - */ -data class PDEPreference( - /** - * The key in the preferences file used to store this preference. - */ - val key: String, - /** - * The key for the description of this preference, used for localization. - */ - val descriptionKey: String, - /** - * The group this preference belongs to. - */ - val group: PDEPreferenceGroup, - /** - * A Composable function that defines the control used to modify this preference. - * It takes the current preference value and a function to update the preference. - */ - val control: @Composable (preference: String?, updatePreference: (newValue: String) -> Unit) -> Unit = { preference, updatePreference -> }, - - /** - * If true, no padding will be applied around this preference's UI. - */ - val noPadding: Boolean = false, -) - -/** - * Composable function to display the preference's description and control. - */ -@Composable -private fun PDEPreference.showControl() { - val locale = LocalLocale.current - val prefs = LocalPreferences.current - Text( - text = locale[descriptionKey], - modifier = Modifier.padding(horizontal = 20.dp), - style = MaterialTheme.typography.titleMedium - ) - val show = @Composable { - control(prefs[key]) { newValue -> - prefs[key] = newValue - } - } - - if(noPadding){ - show() - }else{ - Box(modifier = Modifier.padding(horizontal = 20.dp)) { - show() - } - } -} - -/** - * Data class representing a group of preferences. - */ -data class PDEPreferenceGroup( - /** - * The name of this group. - */ - val name: String, - /** - * The icon representing this group. - */ - val icon: @Composable () -> Unit, - /** - * The group that comes before this one in the list. - */ - val after: PDEPreferenceGroup? = null, -) - -fun show(){ - SwingUtilities.invokeLater { - PDESwingWindow( - titleKey = "preferences", - fullWindowContent = true, - size = Dimension(800, 600) - ) { - PDETheme { - preferences() - } - } - } -} \ No newline at end of file diff --git a/app/src/processing/app/ui/preferences/Coding.kt b/app/src/processing/app/ui/preferences/Coding.kt new file mode 100644 index 000000000..daee85b7f --- /dev/null +++ b/app/src/processing/app/ui/preferences/Coding.kt @@ -0,0 +1,69 @@ +package processing.app.ui.preferences + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.EditNote +import androidx.compose.material3.Icon +import androidx.compose.material3.Switch +import processing.app.ui.PDEPreference +import processing.app.ui.PDEPreferencePane +import processing.app.ui.PDEPreferences +import processing.app.ui.preferences.Interface.Companion.interfaceAndFonts + +class Coding { + companion object { + val coding = PDEPreferencePane( + nameKey = "preferences.pane.editor", + icon = { Icon(Icons.Default.EditNote, contentDescription = null) }, + after = interfaceAndFonts, + ) + + fun register() { + PDEPreferences.register( + PDEPreference( + key = "pdex.errorCheckEnabled", + descriptionKey = "preferences.continuously_check", + pane = coding, + control = { preference, setPreference -> + Switch( + checked = preference?.toBoolean() ?: false, + onCheckedChange = { setPreference(it.toString()) } + ) + } + ), + PDEPreference( + key = "pdex.warningsEnabled", + descriptionKey = "preferences.show_warnings", + pane = coding, + control = { preference, setPreference -> + Switch( + checked = preference?.toBoolean() ?: false, + onCheckedChange = { setPreference(it.toString()) } + ) + } + ), + PDEPreference( + key = "pdex.completion", + descriptionKey = "preferences.code_completion", + pane = coding, + control = { preference, setPreference -> + Switch( + checked = preference?.toBoolean() ?: false, + onCheckedChange = { setPreference(it.toString()) } + ) + } + ), + PDEPreference( + key = "pdex.suggest.imports", + descriptionKey = "preferences.suggest_imports", + pane = coding, + control = { preference, setPreference -> + Switch( + checked = preference?.toBoolean() ?: false, + onCheckedChange = { setPreference(it.toString()) } + ) + } + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/processing/app/ui/preferences/General.kt b/app/src/processing/app/ui/preferences/General.kt index 5f56187f4..c45abbf4c 100644 --- a/app/src/processing/app/ui/preferences/General.kt +++ b/app/src/processing/app/ui/preferences/General.kt @@ -1,32 +1,29 @@ package processing.app.ui.preferences -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.Button -import androidx.compose.material3.FilterChip -import androidx.compose.material3.Icon -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextField +import androidx.compose.material3.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import processing.app.Preferences import processing.app.SketchName +import processing.app.ui.EditorFooter.copyDebugInformationToClipboard import processing.app.ui.PDEPreference -import processing.app.ui.PDEPreferenceGroup +import processing.app.ui.PDEPreferencePane import processing.app.ui.PDEPreferences +import processing.app.ui.theme.LocalLocale class General { companion object{ - val general = PDEPreferenceGroup( - name = "General", + val general = PDEPreferencePane( + nameKey = "preferences.pane.general", icon = { - Icon(Icons.Default.Settings, contentDescription = "A settings icon") + Icon(Icons.Default.Settings, contentDescription = "General Preferences") } ) @@ -35,35 +32,27 @@ class General { PDEPreference( key = "sketchbook.path.four", descriptionKey = "preferences.sketchbook_location", - group = general, + pane = general, + noTitle = true, control = { preference, updatePreference -> - Row ( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - ) { - TextField( - value = preference ?: "", - onValueChange = { - updatePreference(it) - } - ) - Button( - onClick = { - - } - ) { - Text("Browse") + val locale = LocalLocale.current + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + label = { Text(locale["preferences.sketchbook_location"]) }, + value = preference ?: "", + onValueChange = { + updatePreference(it) + }, + trailingIcon = { + Icon(Icons.Default.Folder, contentDescription = null) } - } + ) } - ) - ) - PDEPreferences.register( + ), PDEPreference( key = "sketch.name.approach", descriptionKey = "preferences.sketch_naming", - group = general, + pane = general, control = { preference, updatePreference -> Row{ for (option in if(Preferences.isInitialized()) SketchName.getOptions() else arrayOf( @@ -84,13 +73,27 @@ class General { } } } + ), + PDEPreference( + key = "editor.sync_folder_and_filename", + labelKey = "preferences.new", + descriptionKey = "preferences.sync_folder_and_filename", + pane = general, + control = { preference, updatePreference -> + Switch( + checked = preference.toBoolean(), + onCheckedChange = { + updatePreference(it.toString()) + } + ) + } ) ) PDEPreferences.register( PDEPreference( key = "update.check", - descriptionKey = "preferences.check_for_updates_on_startup", - group = general, + descriptionKey = "preferences.update_check", + pane = general, control = { preference, updatePreference -> Switch( checked = preference.toBoolean(), @@ -104,8 +107,8 @@ class General { PDEPreferences.register( PDEPreference( key = "welcome.show", - descriptionKey = "preferences.show_welcome_screen_on_startup", - group = general, + descriptionKey = "preferences.show_welcome_screen", + pane = general, control = { preference, updatePreference -> Switch( checked = preference.toBoolean(), @@ -116,6 +119,20 @@ class General { } ) ) + PDEPreferences.register( + PDEPreference( + key = "welcome.show", + descriptionKey = "preferences.diagnostics", + pane = general, + control = { preference, updatePreference -> + Button(onClick = { + copyDebugInformationToClipboard() + }) { + Text(LocalLocale.current["preferences.diagnostics.button"]) + } + } + ) + ) } } } \ No newline at end of file diff --git a/app/src/processing/app/ui/preferences/Interface.kt b/app/src/processing/app/ui/preferences/Interface.kt index e9747a037..9b3413506 100644 --- a/app/src/processing/app/ui/preferences/Interface.kt +++ b/app/src/processing/app/ui/preferences/Interface.kt @@ -1,95 +1,131 @@ package processing.app.ui.preferences -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.TextIncrease -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.Slider -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import processing.app.Language +import processing.app.LocalPreferences import processing.app.Preferences import processing.app.ui.PDEPreference -import processing.app.ui.PDEPreferenceGroup +import processing.app.ui.PDEPreferencePane import processing.app.ui.PDEPreferences import processing.app.ui.Toolkit import processing.app.ui.preferences.General.Companion.general import processing.app.ui.theme.LocalLocale -import java.util.Locale +import java.util.* class Interface { companion object{ - val interfaceAndFonts = PDEPreferenceGroup( - name = "Interface", + val interfaceAndFonts = PDEPreferencePane( + nameKey = "preferences.pane.interface", icon = { Icon(Icons.Default.TextIncrease, contentDescription = "Interface") }, after = general ) + @OptIn(ExperimentalMaterial3Api::class) fun register() { - PDEPreferences.register(PDEPreference( - key = "language", - descriptionKey = "preferences.language", - group = interfaceAndFonts, - control = { preference, updatePreference -> - val locale = LocalLocale.current - val showOptions = remember { mutableStateOf(false) } - TextField( - value = locale.locale.displayName, - readOnly = true, - onValueChange = { }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "Select Font Family", + PDEPreferences.register( + PDEPreference( + key = "language", + descriptionKey = "preferences.language", + pane = interfaceAndFonts, + control = { preference, updatePreference -> + val locale = LocalLocale.current + val showOptions = remember { mutableStateOf(false) } + OutlinedButton( + onClick = { + showOptions.value = true + }, + shape = RoundedCornerShape(12.dp) + ) { + Icon(Icons.Default.Language, contentDescription = null) + Spacer(Modifier.width(8.dp)) + Text(locale.locale.displayName) + Icon(Icons.Default.ArrowDropDown, contentDescription = null) + } + languagesDropdown(showOptions) + } + ), + PDEPreference( + key = "editor.input_method_support", + descriptionKey = "preferences.enable_complex_text", + pane = interfaceAndFonts, + control = { preference, updatePreference -> + val enabled = preference?.toBoolean() ?: true + Switch( + checked = enabled, + onCheckedChange = { + updatePreference(it.toString()) + } + ) + } + ) + ) + PDEPreferences.register( + PDEPreference( + key = "editor.zoom", + descriptionKey = "preferences.interface_scale", + pane = interfaceAndFonts, + control = { preference, updatePreference -> + val prefs = LocalPreferences.current + var currentZoom by remember(preference) { mutableStateOf(preference?.toFloatOrNull() ?: 100f) } + val automatic = currentZoom == 100f + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Column( modifier = Modifier - .clickable{ - showOptions.value = true - } - ) + .widthIn(max = 200.dp) + ) { + Text( + text = if (automatic) "Auto" else "${currentZoom.toInt()}%", + ) + Slider( + value = currentZoom, + onValueChange = { + currentZoom = it + }, + onValueChangeFinished = { + prefs["editor.zoom.auto"] = automatic + updatePreference(String.format(Locale.US, "%.2f", currentZoom)) + }, + valueRange = 100f..300f, + steps = 3 + ) + } } - ) - languagesDropdown(showOptions) - } - )) + } + ) + ) PDEPreferences.register( PDEPreference( key = "editor.font.family", descriptionKey = "preferences.editor_and_console_font", - group = interfaceAndFonts, + pane = interfaceAndFonts, control = { preference, updatePreference -> var showOptions by remember { mutableStateOf(false) } - val families = if(Preferences.isInitialized()) Toolkit.getMonoFontFamilies() else arrayOf("Monospaced") - TextField( - value = preference ?: families.firstOrNull().orEmpty(), - readOnly = true, - onValueChange = { updatePreference (it) }, - trailingIcon = { - Icon( - Icons.Default.ArrowDropDown, - contentDescription = "Select Font Family", - modifier = Modifier - .clickable{ - showOptions = true - } - ) - } - ) + val families = + if (Preferences.isInitialized()) Toolkit.getMonoFontFamilies() else arrayOf("Monospaced") + OutlinedButton( + onClick = { + showOptions = true + }, + modifier = Modifier.width(200.dp), + shape = RoundedCornerShape(12.dp) + ) { + Text(preference ?: families.firstOrNull().orEmpty()) + Icon(Icons.Default.ArrowDropDown, contentDescription = null) + } DropdownMenu( expanded = showOptions, onDismissRequest = { @@ -108,47 +144,51 @@ class Interface { } } - ) - ) - - PDEPreferences.register(PDEPreference( - key = "editor.font.size", - descriptionKey = "preferences.editor_font_size", - group = interfaceAndFonts, - control = { preference, updatePreference -> - Column { - Text( - text = "${preference ?: "12"} pt", - modifier = Modifier.width(120.dp) - ) - Slider( - value = (preference ?: "12").toFloat(), - onValueChange = { updatePreference(it.toInt().toString()) }, - valueRange = 10f..48f, - steps = 18, - ) + ), + PDEPreference( + key = "editor.font.size", + descriptionKey = "preferences.editor_font_size", + pane = interfaceAndFonts, + control = { preference, updatePreference -> + Column( + modifier = Modifier + .widthIn(max = 300.dp) + ) { + Text( + text = "${preference ?: "12"} pt", + modifier = Modifier.width(120.dp) + ) + Slider( + value = (preference ?: "12").toFloat(), + onValueChange = { updatePreference(it.toInt().toString()) }, + valueRange = 10f..48f, + steps = 18 + ) + } } - } - )) - PDEPreferences.register(PDEPreference( - key = "console.font.size", - descriptionKey = "preferences.console_font_size", - group = interfaceAndFonts, - control = { preference, updatePreference -> - Column { - Text( - text = "${preference ?: "12"} pt", - modifier = Modifier.width(120.dp) - ) - Slider( - value = (preference ?: "12").toFloat(), - onValueChange = { updatePreference(it.toInt().toString()) }, - valueRange = 10f..48f, - steps = 18, - ) + ), PDEPreference( + key = "console.font.size", + descriptionKey = "preferences.console_font_size", + pane = interfaceAndFonts, + control = { preference, updatePreference -> + Column( + modifier = Modifier + .widthIn(max = 300.dp) + ) { + Text( + text = "${preference ?: "12"} pt", + modifier = Modifier.width(120.dp) + ) + Slider( + value = (preference ?: "12").toFloat(), + onValueChange = { updatePreference(it.toInt().toString()) }, + valueRange = 10f..48f, + steps = 18, + ) + } } - } - )) + ) + ) } @Composable diff --git a/app/src/processing/app/ui/preferences/Other.kt b/app/src/processing/app/ui/preferences/Other.kt index f5f65ea9c..79858b29b 100644 --- a/app/src/processing/app/ui/preferences/Other.kt +++ b/app/src/processing/app/ui/preferences/Other.kt @@ -1,70 +1,91 @@ package processing.app.ui.preferences -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Map +import androidx.compose.material.icons.filled.Science import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import processing.app.LocalPreferences -import processing.app.ui.LocalPreferenceGroups import processing.app.ui.PDEPreference -import processing.app.ui.PDEPreferenceGroup +import processing.app.ui.PDEPreferencePane +import processing.app.ui.PDEPreferencePanes import processing.app.ui.PDEPreferences -import processing.app.ui.preferences.Interface.Companion.interfaceAndFonts -import processing.app.ui.theme.LocalLocale +import processing.app.ui.preferences.Sketches.Companion.sketches class Other { companion object{ - val other = PDEPreferenceGroup( - name = "Other", + val other = PDEPreferencePane( + nameKey = "preferences.pane.other", icon = { - Icon(Icons.Default.Map, contentDescription = "A map icon") + Icon(Icons.Default.Science, contentDescription = "Other Preferences") }, - after = interfaceAndFonts + after = sketches ) - fun register() { + + fun register(panes: PDEPreferencePanes) { + // TODO: Move to individual preferences PDEPreferences.register( PDEPreference( - key = "other", + key = "preferences.show_other", descriptionKey = "preferences.other", - group = other, - noPadding = true, - control = { _, _ -> - val prefs = LocalPreferences.current - val groups = LocalPreferenceGroups.current - val restPrefs = remember { - val keys = prefs.keys.mapNotNull { it as? String } - val existing = groups.values.flatten().map { it.key } - keys.filter { it !in existing }.sorted() + pane = other, + control = { preference, setPreference -> + val showOther = preference?.toBoolean() ?: false + Switch( + checked = showOther, + onCheckedChange = { + setPreference(it.toString()) + } + ) + if (!showOther) { + return@PDEPreference } - val locale = LocalLocale.current + val prefs = LocalPreferences.current + DisposableEffect(Unit) { + // add all the other options to the same group as the current one + val group = + panes[other]?.find { group -> group.any { preference -> preference.key == "preferences.show_other" } } as? MutableList - for(prefKey in restPrefs){ - val value = prefs[prefKey] - Row ( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .padding(20.dp) - ){ - Text( - text = locale[prefKey], - modifier = Modifier.align(Alignment.CenterVertically) + val existing = panes.values.flatten().flatten().map { preference -> preference.key } + val keys = prefs.keys.mapNotNull { it as? String }.filter { it !in existing }.sorted() + + for (prefKey in keys) { + val preference = PDEPreference( + key = prefKey, + descriptionKey = prefKey, + pane = other, + control = { preference, updatePreference -> + if (preference?.toBooleanStrictOrNull() != null) { + Switch( + checked = preference.toBoolean(), + onCheckedChange = { + updatePreference(it.toString()) + } + ) + return@PDEPreference + } + + OutlinedTextField( + modifier = Modifier.widthIn(max = 300.dp), + value = preference ?: "", + onValueChange = { + updatePreference(it) + } + ) + } ) - TextField(value ?: "", onValueChange = { - prefs[prefKey] = it - }) + group?.add(preference) + } + onDispose { + group?.apply { + removeIf { it.key != "preferences.show_other" } + } } } - } ) ) diff --git a/app/src/processing/app/ui/preferences/Sketches.kt b/app/src/processing/app/ui/preferences/Sketches.kt new file mode 100644 index 000000000..92de623df --- /dev/null +++ b/app/src/processing/app/ui/preferences/Sketches.kt @@ -0,0 +1,146 @@ +package processing.app.ui.preferences + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Monitor +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import processing.app.ui.PDEPreference +import processing.app.ui.PDEPreferencePane +import processing.app.ui.PDEPreferences +import processing.app.ui.preferences.Coding.Companion.coding +import java.awt.GraphicsEnvironment +import javax.swing.JColorChooser + +class Sketches { + companion object { + val sketches = PDEPreferencePane( + nameKey = "preferences.pane.sketches", + icon = { Icon(Icons.Default.PlayArrow, contentDescription = null) }, + after = coding, + ) + + fun register() { + PDEPreferences.register( + PDEPreference( + key = "run.display", + descriptionKey = "preferences.run_sketches_on_display", + pane = sketches, + control = { preference, setPreference -> + val ge = GraphicsEnvironment.getLocalGraphicsEnvironment() + val defaultDevice = ge.defaultScreenDevice + val devices = ge.screenDevices + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + devices.toList().chunked(2).forEach { devices -> + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + devices.forEachIndexed { index, device -> + val displayNum = (index + 1).toString() + OutlinedButton( + colors = if (preference == displayNum) { + ButtonDefaults.buttonColors() + } else { + ButtonDefaults.outlinedButtonColors() + }, + shape = RoundedCornerShape(12.dp), + onClick = { + setPreference(displayNum) + } + ) { + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box { + Icon( + Icons.Default.Monitor, + modifier = Modifier.size(32.dp), + contentDescription = null + ) + Text( + text = displayNum, + modifier = Modifier + .align(Alignment.Center) + .offset(0.dp, (-2).dp), + style = MaterialTheme.typography.bodySmall, + ) + } + Text( + text = "${device.displayMode.width} x ${device.displayMode.height}", + style = MaterialTheme.typography.bodySmall, + ) + if (device == defaultDevice) { + Text( + text = "Default", + style = MaterialTheme.typography.bodySmall, + color = LocalContentColor.current.copy(0.5f), + ) + } + } + } + } + } + } + } + } + ), + PDEPreference( + key = "run.options.memory.maximum", + descriptionKey = "preferences.increase_max_memory", + pane = sketches, + control = { preference, setPreference -> + OutlinedTextField( + modifier = Modifier.widthIn(max = 300.dp), + value = preference ?: "", + trailingIcon = { Text("MB") }, + onValueChange = { + setPreference(it) + } + ) + } + ), + PDEPreference( + key = "run.present.bgcolor", + descriptionKey = "preferences.background_color", + pane = sketches, + control = { preference, setPreference -> + val color = try { + java.awt.Color.decode(preference) + } catch (e: Exception) { + java.awt.Color.BLACK + } + Box( + modifier = Modifier + .size(40.dp) + .padding(4.dp) + .background( + color = Color(color.red, color.green, color.blue), + shape = RoundedCornerShape(4.dp) + ) + .clickable { + // TODO: Replace with Compose color picker when available + val newColor = JColorChooser.showDialog( + null, + "Choose Background Color", + color + ) ?: color + val hexColor = + String.format("#%02x%02x%02x", newColor.red, newColor.green, newColor.blue) + setPreference(hexColor) + } + ) + } + ) + ) + } + } +} \ No newline at end of file diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index 8001796f5..bd1884cdc 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -205,23 +205,40 @@ close.unsaved_changes = Save changes to %s? # Preferences (Frame) preferences = Preferences +preferences.description=Change how Processing works on your computer. These settings affect all Processing windows and stay the same even after you restart. +preferences.pane.general=General +preferences.pane.interface=Interface +preferences.pane.editor=Coding Stuff +preferences.pane.sketches=Sketches +preferences.pane.other=Experimental +preferences.new=New +preferences.reset=Reset to Defaults +preferences.no_results=No results found +preferences.sync_folder_and_filename=Folder name matches sketch name +preferences.sync_folder_and_filename.tip=When enabled, renaming a sketch will also rename its folder to match the sketch name. [Learn more](https://discourse.processing.org/t/sketch-folder-and-sketch-name-syncing/15345) +preferences.show_welcome_screen=Show welcome screen at startup +preferences.diagnostics=Generate diagnostic report for support +preferences.diagnostics.tip=Copies information about your installation into your clipboard, useful for troubleshooting issues. +preferences.diagnostics.button=Generate Report preferences.button.width = 80 preferences.restart_required = Restart Processing to apply changes preferences.sketchbook_location = Sketchbook folder preferences.sketchbook_location.popup = Sketchbook folder preferences.sketch_naming = Sketch name -preferences.language = Language: -preferences.editor_and_console_font = Editor and Console font: -preferences.editor_and_console_font.tip = Select the font used in the Editor and the Console.
Only monospaced (fixed-width) fonts may be used,
though the list may be imperfect. -preferences.editor_font_size = Editor font size: -preferences.console_font_size = Console font size: -preferences.interface_scale = Interface scale: +preferences.sketch_naming.tip=Choose how new sketches are named and numbered. +preferences.language=Language +preferences.editor_and_console_font=Editor and Console font +preferences.editor_and_console_font.tip=Installed Monospaced fonts will be displayed as options. +preferences.editor_font_size=Editor font size +preferences.console_font_size=Console font size +preferences.interface_scale=Interface scale +preferences.interface_scale.tip=Adjust the size of interface elements. preferences.interface_scale.auto = Automatic preferences.background_color = Background color when Presenting: -preferences.background_color.tip = Select the background color used when using Present.
Present is used to present a sketch in full-screen,
accessible from the Sketch menu. +preferences.background_color.tip=Select the background color used when using Present. Present is used to present a sketch in full-screen, accessible from the Sketch menu. preferences.use_smooth_text = Use smooth text in editor window preferences.enable_complex_text = Enable complex text input -preferences.enable_complex_text.tip = Using languages such as Chinese, Japanese, and Arabic
in the Editor window require additional features to be enabled. +preferences.enable_complex_text.tip=Using languages such as Chinese, Japanese, and Arabic in the Editor window require additional features to be enabled. preferences.continuously_check = Continuously check for errors preferences.show_warnings = Show warnings preferences.code_completion = Code completion with @@ -231,13 +248,17 @@ preferences.suggest_imports = Suggest import statements preferences.increase_max_memory = Increase maximum available memory to # preferences.delete_previous_folder_on_export = Delete previous folder on export preferences.check_for_updates_on_startup = Allow update checking (see FAQ for information shared) +preferences.update_check=Check for updates on startup +preferences.update_check.tip=No personal information is sent during this process. See the [FAQ](https://github.com/processing/processing4/wiki/FAQ#checking-for-updates) preferences.run_sketches_on_display = Run sketches on display -preferences.run_sketches_on_display.tip = Sets the display where sketches are initially placed.
As usual, if the sketch window is moved, it will re-open
at the same location, however when running in present
(full screen) mode, this display will always be used. +preferences.run_sketches_on_display.tip=Sets the display where sketches are initially placed. As usual, if the sketch window is moved, it will re-open at the same location, however when running in present (full screen) mode, this display will always be used. preferences.automatically_associate_pde_files = Automatically associate .pde files with Processing preferences.launch_programs_in = Launch programs in preferences.launch_programs_in.mode = mode preferences.file = More preferences can be edited directly in the file: preferences.file.hint = (Edit only when Processing is not running.) +preferences.other=Show experimental settings +preferences.other.tip=These settings are contained in the preferences.txt file and are not officially supported. They may be removed or changed without notice in future versions of Processing. # Sketchbook Location (Frame) sketchbook_location = Select new sketchbook folder From f0b408a8985456716213f8a96f6b2a6ceab594e5 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 5 Nov 2025 16:39:37 +0100 Subject: [PATCH 2/8] Replace Row with Column in sketch naming options Changed the layout container from Row to Column for the sketch naming options in the General preferences UI. This improves vertical arrangement and removes unnecessary padding modifiers. --- app/src/processing/app/ui/preferences/General.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/processing/app/ui/preferences/General.kt b/app/src/processing/app/ui/preferences/General.kt index c45abbf4c..f8b387558 100644 --- a/app/src/processing/app/ui/preferences/General.kt +++ b/app/src/processing/app/ui/preferences/General.kt @@ -1,14 +1,12 @@ package processing.app.ui.preferences -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import processing.app.Preferences import processing.app.SketchName import processing.app.ui.EditorFooter.copyDebugInformationToClipboard @@ -54,7 +52,7 @@ class General { descriptionKey = "preferences.sketch_naming", pane = general, control = { preference, updatePreference -> - Row{ + Column { for (option in if(Preferences.isInitialized()) SketchName.getOptions() else arrayOf( "timestamp", "untitled", @@ -68,7 +66,6 @@ class General { label = { Text(option) }, - modifier = Modifier.padding(4.dp), ) } } From 373321e8dd568bd5c70a0e72decbee4a2d404299 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 6 Nov 2025 07:38:16 +0100 Subject: [PATCH 3/8] Enhance preferences UI and add memory options Refactored preferences UI to swap primary and tertiary colors, improved sidebar button color handling, and updated search bar logic. Added clickable folder icon for sketchbook location selection. Improved interface scale slider logic and display. Added new preferences for increasing available memory and max memory, with enable/disable logic. Updated experimental preferences to use localized description keys if available. Extended ShimAWT to support folder selection via callback and refactored file/folder selection logic for better composability. Updated language properties with new preference keys and descriptions. --- app/src/processing/app/ui/PDEPreferences.kt | 223 ++++++++++-------- .../processing/app/ui/preferences/General.kt | 21 +- .../app/ui/preferences/Interface.kt | 20 +- .../processing/app/ui/preferences/Other.kt | 5 +- .../processing/app/ui/preferences/Sketches.kt | 21 +- build/shared/lib/languages/PDE.properties | 20 ++ core/src/processing/awt/ShimAWT.java | 124 ++++++---- 7 files changed, 270 insertions(+), 164 deletions(-) diff --git a/app/src/processing/app/ui/PDEPreferences.kt b/app/src/processing/app/ui/PDEPreferences.kt index 2ec90ebdb..62d0eaef3 100644 --- a/app/src/processing/app/ui/PDEPreferences.kt +++ b/app/src/processing/app/ui/PDEPreferences.kt @@ -100,7 +100,7 @@ class PDEPreferences { */ @OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) @Composable - fun preferences(){ + fun preferences() { val locale = LocalLocale.current var preferencesQuery by remember { mutableStateOf("") } @@ -153,123 +153,144 @@ class PDEPreferences { mutableStateOf(panesSorted.firstOrNull() { panesQuierried[it].isNotEmpty() }) } - Column { - /** - * Header - */ - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 36.dp, top = 48.dp, end = 24.dp, bottom = 24.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom - ) { - Column( + /** + * Swapping primary and tertiary colors for the preferences window, probably should do that program-wide + */ + val originalScheme = MaterialTheme.colorScheme + MaterialTheme( + colorScheme = originalScheme.copy( + primary = originalScheme.tertiary, + onPrimary = originalScheme.onTertiary, + primaryContainer = originalScheme.tertiaryContainer, + onPrimaryContainer = originalScheme.onTertiaryContainer, + + tertiary = originalScheme.primary, + onTertiary = originalScheme.onPrimary, + tertiaryContainer = originalScheme.primaryContainer, + onTertiaryContainer = originalScheme.onPrimaryContainer, + ) + ) { + Column { + /** + * Header + */ + Row( modifier = Modifier - .weight(1f) + .fillMaxWidth() + .padding(start = 36.dp, top = 48.dp, end = 24.dp, bottom = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom ) { - Text( - text = locale["preferences"], - style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Medium), - ) - Text( - text = locale["preferences.description"], - style = MaterialTheme.typography.bodySmall, - ) - } - SearchBar( - modifier = Modifier, - inputField = { - SearchBarDefaults.InputField( - query = preferencesQuery, - onQueryChange = { - preferencesQuery = it - }, - onSearch = { - - }, - trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, - expanded = false, - onExpandedChange = { }, - placeholder = { Text("Search") } + Column( + modifier = Modifier + .weight(1f) + ) { + Text( + text = locale["preferences"], + style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Medium), ) - }, - expanded = false, - onExpandedChange = {}, - ) { + Text( + text = locale["preferences.description"], + style = MaterialTheme.typography.bodySmall, + ) + } + SearchBar( + modifier = Modifier, + inputField = { + SearchBarDefaults.InputField( + query = preferencesQuery, + onQueryChange = { + preferencesQuery = it + }, + onSearch = { + + }, + trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + expanded = false, + onExpandedChange = { }, + placeholder = { Text("Search") } + ) + }, + expanded = false, + onExpandedChange = {}, + ) { + } } - } - HorizontalDivider() - Row( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceVariant) - ) { - /** - * Sidebar - */ - Column( + HorizontalDivider() + Row( modifier = Modifier - .width(IntrinsicSize.Min) - .padding(30.dp) .background(MaterialTheme.colorScheme.surfaceVariant) ) { + /** + * Sidebar + */ + Column( + modifier = Modifier + .width(IntrinsicSize.Min) + .padding(30.dp) + .background(MaterialTheme.colorScheme.surfaceVariant) + ) { - for (pane in panesSorted) { - val shape = RoundedCornerShape(12.dp) - val isSelected = selected == pane - TextButton( - onClick = { - selected = pane - }, - enabled = panesQuierried[pane].isNotEmpty(), - colors = if (isSelected) ButtonDefaults.buttonColors() else ButtonDefaults.textButtonColors(), - shape = shape - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 4.dp, top = 8.dp, end = 8.dp, bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + for (pane in panesSorted) { + val shape = RoundedCornerShape(12.dp) + val isSelected = selected == pane + TextButton( + onClick = { + selected = pane + }, + enabled = panesQuierried[pane].isNotEmpty(), + colors = if (isSelected) ButtonDefaults.buttonColors() else ButtonDefaults.textButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + shape = shape ) { - pane.icon() - Text(locale[pane.nameKey]) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 4.dp, top = 8.dp, end = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + pane.icon() + Text(locale[pane.nameKey]) + } } } } - } - /** - * Content Area - */ - AnimatedContent( - targetState = selected, - transitionSpec = { - fadeIn( - animationSpec = tween(300) - ) togetherWith fadeOut( - animationSpec = tween(300) - ) - } - ) { selected -> - if (selected == null) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(30.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = locale["preferences.no_results"], - style = MaterialTheme.typography.bodyMedium + /** + * Content Area + */ + AnimatedContent( + targetState = selected, + transitionSpec = { + fadeIn( + animationSpec = tween(300) + ) togetherWith fadeOut( + animationSpec = tween(300) ) } - return@AnimatedContent - } + ) { selected -> + if (selected == null) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(30.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = locale["preferences.no_results"], + style = MaterialTheme.typography.bodyMedium + ) + } + return@AnimatedContent + } - val groups = panesQuierried[selected] ?: emptyList() - selected.showPane(groups) + val groups = panesQuierried[selected] ?: emptyList() + selected.showPane(groups) + } } } } diff --git a/app/src/processing/app/ui/preferences/General.kt b/app/src/processing/app/ui/preferences/General.kt index f8b387558..b560bbc1d 100644 --- a/app/src/processing/app/ui/preferences/General.kt +++ b/app/src/processing/app/ui/preferences/General.kt @@ -1,5 +1,6 @@ package processing.app.ui.preferences +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons @@ -14,6 +15,8 @@ import processing.app.ui.PDEPreference import processing.app.ui.PDEPreferencePane import processing.app.ui.PDEPreferences import processing.app.ui.theme.LocalLocale +import processing.awt.ShimAWT.selectFolder +import java.io.File class General { @@ -42,7 +45,21 @@ class General { updatePreference(it) }, trailingIcon = { - Icon(Icons.Default.Folder, contentDescription = null) + Icon( + Icons.Default.Folder, + contentDescription = null, + modifier = Modifier + .clickable { + selectFolder( + locale["preferences.sketchbook_location.popup"], + File(preference ?: "") + ) { selectedFile: File? -> + if (selectedFile != null) { + updatePreference(selectedFile.absolutePath) + } + } + } + ) } ) } @@ -103,7 +120,7 @@ class General { ) PDEPreferences.register( PDEPreference( - key = "welcome.show", + key = "welcome.four.show", descriptionKey = "preferences.show_welcome_screen", pane = general, control = { preference, updatePreference -> diff --git a/app/src/processing/app/ui/preferences/Interface.kt b/app/src/processing/app/ui/preferences/Interface.kt index 9b3413506..0edbde0c2 100644 --- a/app/src/processing/app/ui/preferences/Interface.kt +++ b/app/src/processing/app/ui/preferences/Interface.kt @@ -76,9 +76,19 @@ class Interface { descriptionKey = "preferences.interface_scale", pane = interfaceAndFonts, control = { preference, updatePreference -> + val range = 100f..300f + val prefs = LocalPreferences.current - var currentZoom by remember(preference) { mutableStateOf(preference?.toFloatOrNull() ?: 100f) } - val automatic = currentZoom == 100f + var currentZoom by remember(preference) { + mutableStateOf( + preference + ?.replace("%", "") + ?.toFloatOrNull() + ?: range.start + ) + } + val automatic = currentZoom == range.start + val zoomPerc = "${currentZoom.toInt()}%" Row( horizontalArrangement = Arrangement.spacedBy(16.dp) ) { @@ -87,7 +97,7 @@ class Interface { .widthIn(max = 200.dp) ) { Text( - text = if (automatic) "Auto" else "${currentZoom.toInt()}%", + text = if (automatic) "Auto" else zoomPerc, ) Slider( value = currentZoom, @@ -96,9 +106,9 @@ class Interface { }, onValueChangeFinished = { prefs["editor.zoom.auto"] = automatic - updatePreference(String.format(Locale.US, "%.2f", currentZoom)) + updatePreference(zoomPerc) }, - valueRange = 100f..300f, + valueRange = range, steps = 3 ) } diff --git a/app/src/processing/app/ui/preferences/Other.kt b/app/src/processing/app/ui/preferences/Other.kt index 79858b29b..b637a55f4 100644 --- a/app/src/processing/app/ui/preferences/Other.kt +++ b/app/src/processing/app/ui/preferences/Other.kt @@ -15,6 +15,7 @@ import processing.app.ui.PDEPreferencePane import processing.app.ui.PDEPreferencePanes import processing.app.ui.PDEPreferences import processing.app.ui.preferences.Sketches.Companion.sketches +import processing.app.ui.theme.LocalLocale class Other { companion object{ @@ -45,6 +46,7 @@ class Other { return@PDEPreference } val prefs = LocalPreferences.current + val locale = LocalLocale.current DisposableEffect(Unit) { // add all the other options to the same group as the current one val group = @@ -54,9 +56,10 @@ class Other { val keys = prefs.keys.mapNotNull { it as? String }.filter { it !in existing }.sorted() for (prefKey in keys) { + val descriptionKey = "preferences.$prefKey" val preference = PDEPreference( key = prefKey, - descriptionKey = prefKey, + descriptionKey = if (locale.containsKey(descriptionKey)) descriptionKey else prefKey, pane = other, control = { preference, updatePreference -> if (preference?.toBooleanStrictOrNull() != null) { diff --git a/app/src/processing/app/ui/preferences/Sketches.kt b/app/src/processing/app/ui/preferences/Sketches.kt index 92de623df..0a3b77375 100644 --- a/app/src/processing/app/ui/preferences/Sketches.kt +++ b/app/src/processing/app/ui/preferences/Sketches.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import processing.app.LocalPreferences import processing.app.ui.PDEPreference import processing.app.ui.PDEPreferencePane import processing.app.ui.PDEPreferences @@ -48,14 +49,14 @@ class Sketches { devices.forEachIndexed { index, device -> val displayNum = (index + 1).toString() OutlinedButton( - colors = if (preference == displayNum) { + colors = if (preference == displayNum || (device == defaultDevice && preference == "-1")) { ButtonDefaults.buttonColors() } else { ButtonDefaults.outlinedButtonColors() }, shape = RoundedCornerShape(12.dp), onClick = { - setPreference(displayNum) + setPreference(if (device == defaultDevice) "-1" else displayNum) } ) { @@ -93,12 +94,26 @@ class Sketches { } } ), + PDEPreference( + key = "run.options.memory", + descriptionKey = "preferences.increase_memory", + pane = sketches, + control = { preference, setPreference -> + Switch( + checked = preference?.toBoolean() ?: false, + onCheckedChange = { + setPreference(it.toString()) + } + ) + } + ), PDEPreference( key = "run.options.memory.maximum", descriptionKey = "preferences.increase_max_memory", pane = sketches, control = { preference, setPreference -> OutlinedTextField( + enabled = LocalPreferences.current["run.options.memory"]?.toBoolean() ?: false, modifier = Modifier.widthIn(max = 300.dp), value = preference ?: "", trailingIcon = { Text("MB") }, @@ -120,7 +135,7 @@ class Sketches { } Box( modifier = Modifier - .size(40.dp) + .size(64.dp) .padding(4.dp) .background( color = Color(color.red, color.green, color.blue), diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index bd1884cdc..f6b004e7a 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -245,6 +245,7 @@ preferences.code_completion = Code completion with preferences.trigger_with = Trigger with preferences.cmd_space = space preferences.suggest_imports = Suggest import statements +preferences.increase_memory=Increase maximum available memory preferences.increase_max_memory = Increase maximum available memory to # preferences.delete_previous_folder_on_export = Delete previous folder on export preferences.check_for_updates_on_startup = Allow update checking (see FAQ for information shared) @@ -259,6 +260,25 @@ preferences.file = More preferences can be edited directly in the file: preferences.file.hint = (Edit only when Processing is not running.) preferences.other=Show experimental settings preferences.other.tip=These settings are contained in the preferences.txt file and are not officially supported. They may be removed or changed without notice in future versions of Processing. +# Preferences (Experimental Pane) +# Keys from the comments of defaults.txt (Nov 2025) +preferences.contribution.backup.on_remove=Backup contributions when "Remove" button is pressed +preferences.contribution.backup.on_remove.tip=When enabled, a backup copy of the contribution will be created in your sketchbook "tools", "modes", "libraries", or "examples" folder when you remove it via the Contribution Manager. +preferences.contribution.backup.on_install=Backup contributions when installing a newer version +preferences.contribution.backup.on_install.tip=When enabled, a backup copy of the contribution will be created in your sketchbook "tools", "modes", "libraries", or "examples" folder when you install a newer version via the Contribution Manager. +preferences.recent.count=Number of recent sketches to show +preferences.chooser.files.native=Use native file chooser dialogs +preferences.theme.gradient.method=Gradient method for themes +preferences.theme.gradient.method.tip=Set to 'lab' to interpolate theme gradients using L*a*b* color space +preferences.platform.auto_file_type_associations=Automatically set file type associations (Windows only) +preferences.platform.auto_file_type_associations.tip=When enabled, Processing will attempt to set itself as the default application for .pde files on Windows systems. +preferences.editor.window.width.default=Default editor window width +preferences.editor.window.height.default=Default editor window height +preferences.editor.window.width.min=Minimum editor window width +preferences.editor.window.height.min=Minimum editor window height +preferences.editor.smooth=Enable antialiasing in the code editor +preferences.editor.caret.blink=Blink the caret +preferences.editor.caret.block=Use block caret # Sketchbook Location (Frame) sketchbook_location = Select new sketchbook folder diff --git a/core/src/processing/awt/ShimAWT.java b/core/src/processing/awt/ShimAWT.java index 901f359bb..304b8dd2a 100644 --- a/core/src/processing/awt/ShimAWT.java +++ b/core/src/processing/awt/ShimAWT.java @@ -1,34 +1,29 @@ package processing.awt; +import processing.core.PApplet; +import processing.core.PConstants; +import processing.core.PImage; + +import javax.imageio.*; +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import javax.swing.*; +import javax.swing.filechooser.FileSystemView; import java.awt.*; import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; import java.awt.image.*; -import java.io.*; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Iterator; -import java.awt.geom.AffineTransform; import java.util.Map; - -import javax.imageio.IIOImage; -import javax.imageio.ImageIO; -import javax.imageio.ImageTypeSpecifier; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import javax.imageio.metadata.IIOInvalidTreeException; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.metadata.IIOMetadataNode; -import javax.swing.ImageIcon; -import javax.swing.JFileChooser; -import javax.swing.UIManager; - -// used by desktopFile() method -import javax.swing.filechooser.FileSystemView; - -import processing.core.PApplet; -import processing.core.PConstants; -import processing.core.PImage; +import java.util.function.Consumer; /** @@ -809,41 +804,51 @@ static public void selectImpl(final String prompt, final Object callbackObject, final Frame parentFrame, final int mode) { - File selectedFile = null; + selectImpl(prompt, defaultSelection, parentFrame, mode, (selectedFile) -> + PApplet.selectCallback(selectedFile, callbackMethod, callbackObject) + ); + } - if (PApplet.useNativeSelect) { - FileDialog dialog = new FileDialog(parentFrame, prompt, mode); - if (defaultSelection != null) { - dialog.setDirectory(defaultSelection.getParent()); - dialog.setFile(defaultSelection.getName()); - } + static public void selectImpl(final String prompt, + final File defaultSelection, + final Frame parentFrame, + final int mode, + final Consumer callback) { + File selectedFile = null; + + if (PApplet.useNativeSelect) { + FileDialog dialog = new FileDialog(parentFrame, prompt, mode); + if (defaultSelection != null) { + dialog.setDirectory(defaultSelection.getParent()); + dialog.setFile(defaultSelection.getName()); + } - dialog.setVisible(true); - String directory = dialog.getDirectory(); - String filename = dialog.getFile(); - if (filename != null) { - selectedFile = new File(directory, filename); - } + dialog.setVisible(true); + String directory = dialog.getDirectory(); + String filename = dialog.getFile(); + if (filename != null) { + selectedFile = new File(directory, filename); + } - } else { - JFileChooser chooser = new JFileChooser(); - chooser.setDialogTitle(prompt); - if (defaultSelection != null) { - chooser.setSelectedFile(defaultSelection); - } + } else { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle(prompt); + if (defaultSelection != null) { + chooser.setSelectedFile(defaultSelection); + } - int result = -1; - if (mode == FileDialog.SAVE) { - result = chooser.showSaveDialog(parentFrame); - } else if (mode == FileDialog.LOAD) { - result = chooser.showOpenDialog(parentFrame); - } - if (result == JFileChooser.APPROVE_OPTION) { - selectedFile = chooser.getSelectedFile(); - } + int result = -1; + if (mode == FileDialog.SAVE) { + result = chooser.showSaveDialog(parentFrame); + } else if (mode == FileDialog.LOAD) { + result = chooser.showOpenDialog(parentFrame); + } + if (result == JFileChooser.APPROVE_OPTION) { + selectedFile = chooser.getSelectedFile(); + } + } + callback.accept(selectedFile); } - PApplet.selectCallback(selectedFile, callbackMethod, callbackObject); - } static public void selectFolder(final String prompt, @@ -854,6 +859,12 @@ static public void selectFolder(final String prompt, defaultSelection, callbackObject, null)); } + static public void selectFolder(final String prompt, + final File defaultSelection, + final Consumer callback) { + selectFolderImpl(prompt, defaultSelection, null, callback); + } + /* static public void selectFolder(final String prompt, @@ -886,6 +897,15 @@ static public void selectFolderImpl(final String prompt, final File defaultSelection, final Object callbackObject, final Frame parentFrame) { + selectFolderImpl(prompt, defaultSelection, parentFrame, (selectedFile) -> + PApplet.selectCallback(selectedFile, callbackMethod, callbackObject) + ); + } + + static public void selectFolderImpl(final String prompt, + final File defaultSelection, + final Frame parentFrame, + final Consumer callback) { File selectedFile = null; if (PApplet.platform == PConstants.MACOS && PApplet.useNativeSelect) { FileDialog fileDialog = @@ -914,7 +934,7 @@ static public void selectFolderImpl(final String prompt, selectedFile = fileChooser.getSelectedFile(); } } - PApplet.selectCallback(selectedFile, callbackMethod, callbackObject); + callback.accept(selectedFile); } From 891aa8092da83159a606e2e0013fe1b6def7abde Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 6 Nov 2025 07:45:14 +0100 Subject: [PATCH 4/8] Fixed a color issue --- app/src/processing/app/ui/PDEPreferences.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/processing/app/ui/PDEPreferences.kt b/app/src/processing/app/ui/PDEPreferences.kt index 62d0eaef3..735837fee 100644 --- a/app/src/processing/app/ui/PDEPreferences.kt +++ b/app/src/processing/app/ui/PDEPreferences.kt @@ -374,7 +374,7 @@ fun PDEPreferencePane.showPane(groups: PDEPreferenceGroups) { modifier = Modifier .fillMaxWidth(), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface, + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, ), border = BorderStroke( width = 1.dp, From 6ee68175cbdc99bb61d8f89aad306d1a1a22db5f Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 6 Nov 2025 07:58:57 +0100 Subject: [PATCH 5/8] Improve preferences UI layout and window size Increased the preferences window width from 800 to 850 pixels for better layout. Updated the General preferences to display FilterChip options in rows with spacing, improving visual organization and usability. --- app/src/processing/app/ui/PDEPreferences.kt | 2 +- .../processing/app/ui/preferences/General.kt | 30 ++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/app/src/processing/app/ui/PDEPreferences.kt b/app/src/processing/app/ui/PDEPreferences.kt index 735837fee..8474fba5c 100644 --- a/app/src/processing/app/ui/PDEPreferences.kt +++ b/app/src/processing/app/ui/PDEPreferences.kt @@ -38,7 +38,7 @@ fun show() { PDESwingWindow( titleKey = "preferences", fullWindowContent = true, - size = Dimension(800, 600) + size = Dimension(850, 600) ) { PDETheme { preferences() diff --git a/app/src/processing/app/ui/preferences/General.kt b/app/src/processing/app/ui/preferences/General.kt index b560bbc1d..282d10cd7 100644 --- a/app/src/processing/app/ui/preferences/General.kt +++ b/app/src/processing/app/ui/preferences/General.kt @@ -1,13 +1,16 @@ package processing.app.ui.preferences import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import processing.app.Preferences import processing.app.SketchName import processing.app.ui.EditorFooter.copyDebugInformationToClipboard @@ -70,20 +73,25 @@ class General { pane = general, control = { preference, updatePreference -> Column { - for (option in if(Preferences.isInitialized()) SketchName.getOptions() else arrayOf( + val options = if (Preferences.isInitialized()) SketchName.getOptions() else arrayOf( "timestamp", "untitled", "custom" - )) { - FilterChip( - selected = preference == option, - onClick = { - updatePreference(option) - }, - label = { - Text(option) - }, - ) + ) + options.toList().chunked(2).forEach { row -> + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + row.forEach { option -> + FilterChip( + selected = preference == option, + onClick = { + updatePreference(option) + }, + label = { + Text(option) + }, + ) + } + } } } } From af4228a13f79f2bd669222c2439e1c62e701d0a7 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 6 Nov 2025 14:58:17 +0100 Subject: [PATCH 6/8] Add theme selection and UI improvements to preferences Introduces a theme selector for the editor in the Interface preferences, allowing users to choose between system, dark, and light themes. Updates Coding and General preferences with improved layout and feedback, including a copied state for diagnostics. Updates localization strings to support new features and labels. --- app/src/processing/app/ui/PDEPreferences.kt | 7 +++- .../processing/app/ui/preferences/Coding.kt | 29 ++++++++++++-- .../processing/app/ui/preferences/General.kt | 21 ++++++++-- .../app/ui/preferences/Interface.kt | 39 +++++++++++++++++++ app/src/processing/app/ui/theme/Theme.kt | 11 +++++- build/shared/lib/languages/PDE.properties | 8 ++++ 6 files changed, 105 insertions(+), 10 deletions(-) diff --git a/app/src/processing/app/ui/PDEPreferences.kt b/app/src/processing/app/ui/PDEPreferences.kt index 8474fba5c..747b5d92b 100644 --- a/app/src/processing/app/ui/PDEPreferences.kt +++ b/app/src/processing/app/ui/PDEPreferences.kt @@ -38,7 +38,8 @@ fun show() { PDESwingWindow( titleKey = "preferences", fullWindowContent = true, - size = Dimension(850, 600) + size = Dimension(850, 600), + minSize = Dimension(700, 500), ) { PDETheme { preferences() @@ -194,8 +195,10 @@ class PDEPreferences { style = MaterialTheme.typography.bodySmall, ) } + Spacer(modifier = Modifier.width(96.dp)) SearchBar( - modifier = Modifier, + modifier = Modifier + .widthIn(max = 250.dp), inputField = { SearchBarDefaults.InputField( query = preferencesQuery, diff --git a/app/src/processing/app/ui/preferences/Coding.kt b/app/src/processing/app/ui/preferences/Coding.kt index daee85b7f..dc6d0cbca 100644 --- a/app/src/processing/app/ui/preferences/Coding.kt +++ b/app/src/processing/app/ui/preferences/Coding.kt @@ -1,13 +1,21 @@ package processing.app.ui.preferences +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.EditNote import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import processing.app.ui.PDEPreference import processing.app.ui.PDEPreferencePane import processing.app.ui.PDEPreferences import processing.app.ui.preferences.Interface.Companion.interfaceAndFonts +import processing.app.ui.theme.LocalLocale class Coding { companion object { @@ -45,11 +53,24 @@ class Coding { key = "pdex.completion", descriptionKey = "preferences.code_completion", pane = coding, + noTitle = true, control = { preference, setPreference -> - Switch( - checked = preference?.toBoolean() ?: false, - onCheckedChange = { setPreference(it.toString()) } - ) + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + val locale = LocalLocale.current + Text( + text = locale["preferences.code_completion"] + " Ctrl-" + locale["preferences.cmd_space"], + style = MaterialTheme.typography.bodyMedium + ) + Switch( + checked = preference?.toBoolean() ?: false, + onCheckedChange = { setPreference(it.toString()) } + ) + } } ), PDEPreference( diff --git a/app/src/processing/app/ui/preferences/General.kt b/app/src/processing/app/ui/preferences/General.kt index 282d10cd7..a8bd55903 100644 --- a/app/src/processing/app/ui/preferences/General.kt +++ b/app/src/processing/app/ui/preferences/General.kt @@ -9,8 +9,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay import processing.app.Preferences import processing.app.SketchName import processing.app.ui.EditorFooter.copyDebugInformationToClipboard @@ -81,7 +83,7 @@ class General { options.toList().chunked(2).forEach { row -> Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { row.forEach { option -> - FilterChip( + InputChip( selected = preference == option, onClick = { updatePreference(option) @@ -98,7 +100,7 @@ class General { ), PDEPreference( key = "editor.sync_folder_and_filename", - labelKey = "preferences.new", + labelKey = "preferences.experimental", descriptionKey = "preferences.sync_folder_and_filename", pane = general, control = { preference, updatePreference -> @@ -147,10 +149,23 @@ class General { descriptionKey = "preferences.diagnostics", pane = general, control = { preference, updatePreference -> + var copied by remember { mutableStateOf(false) } + LaunchedEffect(copied) { + if (copied) { + delay(2000) + copied = false + } + } Button(onClick = { copyDebugInformationToClipboard() + copied = true + }) { - Text(LocalLocale.current["preferences.diagnostics.button"]) + if (!copied) { + Text(LocalLocale.current["preferences.diagnostics.button"]) + } else { + Text(LocalLocale.current["preferences.diagnostics.button.copied"]) + } } } ) diff --git a/app/src/processing/app/ui/preferences/Interface.kt b/app/src/processing/app/ui/preferences/Interface.kt index 0edbde0c2..b494db69d 100644 --- a/app/src/processing/app/ui/preferences/Interface.kt +++ b/app/src/processing/app/ui/preferences/Interface.kt @@ -71,6 +71,45 @@ class Interface { ) ) PDEPreferences.register( + PDEPreference( + key = "editor.theme", + descriptionKey = "preferences.editor.theme", + pane = interfaceAndFonts, + control = { preference, updatePreference -> + val locale = LocalLocale.current + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + InputChip( + selected = (preference ?: "") == "", + onClick = { + updatePreference("") + }, + label = { + Text(locale["preferences.editor.theme.system"]) + } + ) + InputChip( + selected = preference == "dark", + onClick = { + updatePreference("dark") + }, + label = { + Text(locale["preferences.editor.theme.dark"]) + } + ) + InputChip( + selected = preference == "light", + onClick = { + updatePreference("light") + }, + label = { + Text(locale["preferences.editor.theme.light"]) + } + ) + } + } + ), PDEPreference( key = "editor.zoom", descriptionKey = "preferences.interface_scale", diff --git a/app/src/processing/app/ui/theme/Theme.kt b/app/src/processing/app/ui/theme/Theme.kt index c59c5025c..e9c9bb751 100644 --- a/app/src/processing/app/ui/theme/Theme.kt +++ b/app/src/processing/app/ui/theme/Theme.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import darkScheme import lightScheme +import processing.app.LocalPreferences import processing.app.PreferencesProvider /** @@ -52,8 +53,16 @@ fun PDETheme( ){ PreferencesProvider { LocaleProvider { + val preferences = LocalPreferences.current + val theme = when { + preferences["editor.theme"] == "dark" -> darkScheme + preferences["editor.theme"] == "light" -> lightScheme + darkTheme -> darkScheme + else -> lightScheme + + } MaterialTheme( - colorScheme = if(darkTheme) darkScheme else lightScheme, + colorScheme = theme, typography = PDETypography ){ Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainerLowest)) { diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index f6b004e7a..c36d59ff0 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -213,6 +213,7 @@ preferences.pane.sketches=Sketches preferences.pane.other=Experimental preferences.new=New preferences.reset=Reset to Defaults +preferences.experimental=Experimental preferences.no_results=No results found preferences.sync_folder_and_filename=Folder name matches sketch name preferences.sync_folder_and_filename.tip=When enabled, renaming a sketch will also rename its folder to match the sketch name. [Learn more](https://discourse.processing.org/t/sketch-folder-and-sketch-name-syncing/15345) @@ -220,6 +221,7 @@ preferences.show_welcome_screen=Show welcome screen at startup preferences.diagnostics=Generate diagnostic report for support preferences.diagnostics.tip=Copies information about your installation into your clipboard, useful for troubleshooting issues. preferences.diagnostics.button=Generate Report +preferences.diagnostics.button.copied=Report copied to clipboard preferences.button.width = 80 preferences.restart_required = Restart Processing to apply changes preferences.sketchbook_location = Sketchbook folder @@ -231,6 +233,12 @@ preferences.editor_and_console_font=Editor and Console font preferences.editor_and_console_font.tip=Installed Monospaced fonts will be displayed as options. preferences.editor_font_size=Editor font size preferences.console_font_size=Console font size +preferences.editor.theme=Theme +preferences.editor.theme.tip=Choose a color theme for windows except for the editor. +preferences.editor.theme.system=System +preferences.editor.theme.light=Light +preferences.editor.theme.dark=Dark +preferences.interface_theme=Interface theme preferences.interface_scale=Interface scale preferences.interface_scale.tip=Adjust the size of interface elements. preferences.interface_scale.auto = Automatic From 15afdf0dccf639287b7bac298ed902e71cf5c452 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 10 Nov 2025 17:18:20 +0100 Subject: [PATCH 7/8] Added the ability to undo the changes + icon/language changes --- app/src/processing/app/ui/PDEPreferences.kt | 438 +++++++++++++----- .../processing/app/ui/preferences/Coding.kt | 4 +- .../app/ui/preferences/Interface.kt | 4 +- .../processing/app/ui/preferences/Other.kt | 5 +- .../processing/app/ui/preferences/Sketches.kt | 62 ++- build/shared/lib/languages/PDE.properties | 9 +- 6 files changed, 388 insertions(+), 134 deletions(-) diff --git a/app/src/processing/app/ui/PDEPreferences.kt b/app/src/processing/app/ui/PDEPreferences.kt index 747b5d92b..87ca7298f 100644 --- a/app/src/processing/app/ui/PDEPreferences.kt +++ b/app/src/processing/app/ui/PDEPreferences.kt @@ -1,6 +1,8 @@ package processing.app.ui import androidx.compose.animation.* +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.EaseOutBounce import androidx.compose.animation.core.tween import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -15,23 +17,27 @@ import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import com.mikepenz.markdown.compose.Markdown import com.mikepenz.markdown.m3.markdownColor import com.mikepenz.markdown.m3.markdownTypography import processing.app.LocalPreferences +import processing.app.ReactiveProperties import processing.app.ui.PDEPreferences.Companion.preferences import processing.app.ui.preferences.* -import processing.app.ui.theme.LocalLocale -import processing.app.ui.theme.PDESwingWindow -import processing.app.ui.theme.PDETheme +import processing.app.ui.theme.* import java.awt.Dimension +import java.awt.event.WindowEvent +import java.awt.event.WindowListener import javax.swing.SwingUtilities +import javax.swing.WindowConstants + fun show() { SwingUtilities.invokeLater { @@ -171,128 +177,221 @@ class PDEPreferences { onTertiaryContainer = originalScheme.onPrimaryContainer, ) ) { - Column { - /** - * Header - */ - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 36.dp, top = 48.dp, end = 24.dp, bottom = 24.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom - ) { - Column( - modifier = Modifier - .weight(1f) - ) { - Text( - text = locale["preferences"], - style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Medium), - ) - Text( - text = locale["preferences.description"], - style = MaterialTheme.typography.bodySmall, - ) - } - Spacer(modifier = Modifier.width(96.dp)) - SearchBar( - modifier = Modifier - .widthIn(max = 250.dp), - inputField = { - SearchBarDefaults.InputField( - query = preferencesQuery, - onQueryChange = { - preferencesQuery = it - }, - onSearch = { - - }, - trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, - expanded = false, - onExpandedChange = { }, - placeholder = { Text("Search") } - ) - }, - expanded = false, - onExpandedChange = {}, - ) { - - } - } - HorizontalDivider() - Row( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceVariant) - ) { + CapturePreferences { + Column { /** - * Sidebar + * Header */ - Column( + Row( modifier = Modifier - .width(IntrinsicSize.Min) - .padding(30.dp) - .background(MaterialTheme.colorScheme.surfaceVariant) + .fillMaxWidth() + .padding(start = 36.dp, top = 48.dp, end = 24.dp, bottom = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom ) { + Column( + modifier = Modifier + .weight(1f) + ) { + Text( + text = locale["preferences"], + style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Medium), + ) + Text( + text = locale["preferences.description"], + style = MaterialTheme.typography.bodySmall, + ) + } + Spacer(modifier = Modifier.width(96.dp)) + SearchBar( + modifier = Modifier + .widthIn(max = 250.dp), + inputField = { + SearchBarDefaults.InputField( + query = preferencesQuery, + onQueryChange = { + preferencesQuery = it + }, + onSearch = { + + }, + trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + expanded = false, + onExpandedChange = { }, + placeholder = { Text("Search") } + ) + }, + expanded = false, + onExpandedChange = {}, + ) { - for (pane in panesSorted) { - val shape = RoundedCornerShape(12.dp) - val isSelected = selected == pane - TextButton( - onClick = { - selected = pane - }, - enabled = panesQuierried[pane].isNotEmpty(), - colors = if (isSelected) ButtonDefaults.buttonColors() else ButtonDefaults.textButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ), - shape = shape + } + } + HorizontalDivider() + Box { + Row( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceVariant) + ) { + /** + * Sidebar + */ + Column( + modifier = Modifier + .width(IntrinsicSize.Min) + .padding(30.dp) + .background(MaterialTheme.colorScheme.surfaceVariant) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 4.dp, top = 8.dp, end = 8.dp, bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - pane.icon() - Text(locale[pane.nameKey]) + + for (pane in panesSorted) { + val shape = RoundedCornerShape(12.dp) + val isSelected = selected == pane + TextButton( + onClick = { + selected = pane + }, + enabled = panesQuierried[pane].isNotEmpty(), + colors = if (isSelected) ButtonDefaults.buttonColors() else ButtonDefaults.textButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + shape = shape + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 4.dp, top = 8.dp, end = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + pane.icon() + Text(locale[pane.nameKey]) + } + } } } - } - } - /** - * Content Area - */ - AnimatedContent( - targetState = selected, - transitionSpec = { - fadeIn( - animationSpec = tween(300) - ) togetherWith fadeOut( - animationSpec = tween(300) - ) + /** + * Content Area + */ + AnimatedContent( + targetState = selected, + transitionSpec = { + fadeIn( + animationSpec = tween(300) + ) togetherWith fadeOut( + animationSpec = tween(300) + ) + } + ) { selected -> + if (selected == null) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(30.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = locale["preferences.no_results"], + style = MaterialTheme.typography.bodyMedium + ) + } + return@AnimatedContent + } + + val groups = panesQuierried[selected] ?: emptyList() + selected.showPane(groups) + } } - ) { selected -> - if (selected == null) { - Box( + /** + * Unconfirmed changes banner + */ + Column( + modifier = Modifier.align(Alignment.BottomEnd) + ) { + val modifiable = LocalModifiablePreferences.current + val wiggle = remember { Animatable(0f) } + if (modifiable.lastCloseAttempt != null) { + LaunchedEffect(modifiable.lastCloseAttempt) { + wiggle.animateTo( + targetValue = 50f, + animationSpec = tween(100, easing = EaseOutBounce) + ) + wiggle.animateTo( + targetValue = -50f, + animationSpec = tween(100, easing = EaseOutBounce) + ) + wiggle.animateTo( + targetValue = 0f, + animationSpec = tween(300, easing = EaseOutBounce) + ) + } + } + AnimatedVisibility( + visible = modifiable.isModified, + enter = fadeIn( + animationSpec = tween(300) + ) + slideInVertically( + initialOffsetY = { it }, + animationSpec = tween(500, easing = EaseOutBounce), + ), + exit = fadeOut( + animationSpec = tween(300) + ) + slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(300), + ), modifier = Modifier - .fillMaxSize() - .padding(30.dp), - contentAlignment = Alignment.Center + .graphicsLayer { + translationX = wiggle.value + } ) { - Text( - text = locale["preferences.no_results"], - style = MaterialTheme.typography.bodyMedium - ) + val shape = RoundedCornerShape(8.dp) + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + modifier = Modifier + .padding(24.dp) + .border( + 1.dp, + MaterialTheme.colorScheme.outlineVariant, + shape + ), + ) { + Row( + modifier = Modifier + .padding(16.dp, 8.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(locale["preferences.unconfirmed_changes"]) + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + onClick = { + modifiable.reset() + }, + shape = shape + ) { + Text(locale["preferences.reset_changes"]) + } + Button( + onClick = { + modifiable.apply() + }, + shape = shape + ) { + Text(locale["preferences.apply_changes"]) + } + } + } + } } - return@AnimatedContent } - - val groups = panesQuierried[selected] ?: emptyList() - selected.showPane(groups) } } } @@ -305,20 +404,22 @@ class PDEPreferences { @JvmStatic fun main(args: Array) { application { - Window(onCloseRequest = ::exitApplication) { - remember { - window.rootPane.putClientProperty("apple.awt.fullWindowContent", true) - window.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) - } + PDEComposeWindow( + titleKey = "preferences", + fullWindowContent = true, + size = DpSize(850.dp, 600.dp), + minSize = DpSize(850.dp, 600.dp), + ) { PDETheme(darkTheme = true) { preferences() } } - Window(onCloseRequest = ::exitApplication) { - remember { - window.rootPane.putClientProperty("apple.awt.fullWindowContent", true) - window.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) - } + PDEComposeWindow( + titleKey = "preferences", + fullWindowContent = true, + size = DpSize(850.dp, 600.dp), + minSize = DpSize(850.dp, 600.dp), + ) { PDETheme(darkTheme = false) { preferences() } @@ -328,6 +429,99 @@ class PDEPreferences { } } + +private data class ModifiablePreference( + val lastCloseAttempt: Long? = null, + val isModified: Boolean, + val apply: () -> Unit, + val reset: () -> Unit, +) + +private val LocalModifiablePreferences = + compositionLocalOf { ModifiablePreference(null, false, { }, {}) } + +/** + * Composable function that provides a modifiable copy of the current preferences. + * This allows for temporary changes to preferences that can be reset or applied later. + * + * @param content The composable content that will have access to the modifiable preferences. + */ +@Composable +private fun CapturePreferences(content: @Composable () -> Unit) { + val prefs = LocalPreferences.current + + var lastCloseAttempt by remember { mutableStateOf(null) } + val modified = remember { + ReactiveProperties().apply { + prefs.entries.forEach { (key, value) -> + setProperty(key as String, value as String) + } + } + } + val isModified = remember( + prefs, + // TODO: Learn how to modify preferences so listening to the object is enough + prefs.snapshotStateMap.toMap(), + modified, + modified.snapshotStateMap.toMap(), + ) { + prefs.entries.any { (key, value) -> + modified[key] != value + } + } + if (isModified) { + val window = LocalWindow.current + DisposableEffect(window) { + val operation = window.defaultCloseOperation + window.defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE + window.rootPane.putClientProperty("Window.documentModified", true); + val listener = object : WindowListener { + override fun windowOpened(e: WindowEvent?) {} + override fun windowClosing(e: WindowEvent?) { + lastCloseAttempt = System.currentTimeMillis() + } + + override fun windowClosed(e: WindowEvent?) {} + override fun windowIconified(e: WindowEvent?) {} + override fun windowDeiconified(e: WindowEvent?) {} + override fun windowActivated(e: WindowEvent?) {} + override fun windowDeactivated(e: WindowEvent?) {} + + } + window.addWindowListener(listener) + onDispose { + window.removeWindowListener(listener) + window.defaultCloseOperation = operation + window.rootPane.putClientProperty("Window.documentModified", false); + } + } + } + + val apply = { + modified.entries.forEach { (key, value) -> + prefs.setProperty(key as String, (value ?: "") as String) + } + } + val reset = { + modified.entries.forEach { (key, value) -> + modified.setProperty(key as String, prefs[key] ?: "") + } + } + val state = ModifiablePreference( + isModified = isModified, + apply = apply, + lastCloseAttempt = lastCloseAttempt, + reset = reset + ) + + CompositionLocalProvider( + LocalPreferences provides modified, + LocalModifiablePreferences provides state + ) { + content() + } +} + typealias PDEPreferencePanes = MutableMap typealias PDEPreferenceGroups = List typealias PDEPreferenceGroup = List diff --git a/app/src/processing/app/ui/preferences/Coding.kt b/app/src/processing/app/ui/preferences/Coding.kt index dc6d0cbca..21b87ad5a 100644 --- a/app/src/processing/app/ui/preferences/Coding.kt +++ b/app/src/processing/app/ui/preferences/Coding.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.EditNote +import androidx.compose.material.icons.filled.Code import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch @@ -21,7 +21,7 @@ class Coding { companion object { val coding = PDEPreferencePane( nameKey = "preferences.pane.editor", - icon = { Icon(Icons.Default.EditNote, contentDescription = null) }, + icon = { Icon(Icons.Default.Code, contentDescription = null) }, after = interfaceAndFonts, ) diff --git a/app/src/processing/app/ui/preferences/Interface.kt b/app/src/processing/app/ui/preferences/Interface.kt index b494db69d..be0ee833c 100644 --- a/app/src/processing/app/ui/preferences/Interface.kt +++ b/app/src/processing/app/ui/preferences/Interface.kt @@ -4,8 +4,8 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Brush import androidx.compose.material.icons.filled.Language -import androidx.compose.material.icons.filled.TextIncrease import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -26,7 +26,7 @@ class Interface { val interfaceAndFonts = PDEPreferencePane( nameKey = "preferences.pane.interface", icon = { - Icon(Icons.Default.TextIncrease, contentDescription = "Interface") + Icon(Icons.Default.Brush, contentDescription = "Interface") }, after = general ) diff --git a/app/src/processing/app/ui/preferences/Other.kt b/app/src/processing/app/ui/preferences/Other.kt index b637a55f4..8544f7694 100644 --- a/app/src/processing/app/ui/preferences/Other.kt +++ b/app/src/processing/app/ui/preferences/Other.kt @@ -2,7 +2,7 @@ package processing.app.ui.preferences import androidx.compose.foundation.layout.widthIn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Science +import androidx.compose.material.icons.filled.Lightbulb import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Switch @@ -22,13 +22,12 @@ class Other { val other = PDEPreferencePane( nameKey = "preferences.pane.other", icon = { - Icon(Icons.Default.Science, contentDescription = "Other Preferences") + Icon(Icons.Default.Lightbulb, contentDescription = "Other Preferences") }, after = sketches ) fun register(panes: PDEPreferencePanes) { - // TODO: Move to individual preferences PDEPreferences.register( PDEPreference( key = "preferences.show_other", diff --git a/app/src/processing/app/ui/preferences/Sketches.kt b/app/src/processing/app/ui/preferences/Sketches.kt index 0a3b77375..b3fef23cd 100644 --- a/app/src/processing/app/ui/preferences/Sketches.kt +++ b/app/src/processing/app/ui/preferences/Sketches.kt @@ -6,11 +6,13 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Monitor -import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp import processing.app.LocalPreferences import processing.app.ui.PDEPreference @@ -24,7 +26,7 @@ class Sketches { companion object { val sketches = PDEPreferencePane( nameKey = "preferences.pane.sketches", - icon = { Icon(Icons.Default.PlayArrow, contentDescription = null) }, + icon = { Icon(Select_window, contentDescription = null) }, after = coding, ) @@ -157,5 +159,61 @@ class Sketches { ) ) } + val Select_window: ImageVector + get() { + if (_Select_window != null) return _Select_window!! + + _Select_window = ImageVector.Builder( + name = "Select_window", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f + ).apply { + path( + fill = SolidColor(Color(0xFF000000)) + ) { + moveTo(160f, 880f) + quadToRelative(-33f, 0f, -56.5f, -23.5f) + reflectiveQuadTo(80f, 800f) + verticalLineToRelative(-360f) + quadToRelative(0f, -33f, 23.5f, -56.5f) + reflectiveQuadTo(160f, 360f) + horizontalLineToRelative(80f) + verticalLineToRelative(-200f) + quadToRelative(0f, -33f, 23.5f, -56.5f) + reflectiveQuadTo(320f, 80f) + horizontalLineToRelative(480f) + quadToRelative(33f, 0f, 56.5f, 23.5f) + reflectiveQuadTo(880f, 160f) + verticalLineToRelative(360f) + quadToRelative(0f, 33f, -23.5f, 56.5f) + reflectiveQuadTo(800f, 600f) + horizontalLineToRelative(-80f) + verticalLineToRelative(200f) + quadToRelative(0f, 33f, -23.5f, 56.5f) + reflectiveQuadTo(640f, 880f) + close() + moveToRelative(0f, -80f) + horizontalLineToRelative(480f) + verticalLineToRelative(-280f) + horizontalLineTo(160f) + close() + moveToRelative(560f, -280f) + horizontalLineToRelative(80f) + verticalLineToRelative(-280f) + horizontalLineTo(320f) + verticalLineToRelative(120f) + horizontalLineToRelative(320f) + quadToRelative(33f, 0f, 56.5f, 23.5f) + reflectiveQuadTo(720f, 440f) + close() + } + }.build() + + return _Select_window!! + } + + private var _Select_window: ImageVector? = null } } \ No newline at end of file diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index c36d59ff0..9814dbd99 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -207,12 +207,15 @@ close.unsaved_changes = Save changes to %s? preferences = Preferences preferences.description=Change how Processing works on your computer. These settings affect all Processing windows and stay the same even after you restart. preferences.pane.general=General -preferences.pane.interface=Interface -preferences.pane.editor=Coding Stuff +preferences.pane.interface=Appearance +preferences.pane.editor=Code preferences.pane.sketches=Sketches -preferences.pane.other=Experimental +preferences.pane.other=Advanced preferences.new=New preferences.reset=Reset to Defaults +preferences.reset_changes=Reset +preferences.unconfirmed_changes=You have unsaved changes! +preferences.apply_changes=Confirm Changes preferences.experimental=Experimental preferences.no_results=No results found preferences.sync_folder_and_filename=Folder name matches sketch name From 5e91e4e4c550a9e0aad41556f448e3ae1a9759df Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Mon, 10 Nov 2025 17:59:53 +0100 Subject: [PATCH 8/8] Update animation spec for slideInVertically Changed the animationSpec for slideInVertically from a 500ms EaseOutBounce to a 300ms default tween for consistency and smoother transitions. --- app/src/processing/app/ui/PDEPreferences.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/processing/app/ui/PDEPreferences.kt b/app/src/processing/app/ui/PDEPreferences.kt index 87ca7298f..ac5bf2609 100644 --- a/app/src/processing/app/ui/PDEPreferences.kt +++ b/app/src/processing/app/ui/PDEPreferences.kt @@ -333,7 +333,7 @@ class PDEPreferences { animationSpec = tween(300) ) + slideInVertically( initialOffsetY = { it }, - animationSpec = tween(500, easing = EaseOutBounce), + animationSpec = tween(300), ), exit = fadeOut( animationSpec = tween(300)