diff --git a/app/src/main/resources/languages/PDE.properties b/app/src/main/resources/languages/PDE.properties
index 254a6c84f4..3c1ad2ab70 100644
--- a/app/src/main/resources/languages/PDE.properties
+++ b/app/src/main/resources/languages/PDE.properties
@@ -205,39 +205,91 @@ 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=Appearance
+preferences.pane.editor=Code
+preferences.pane.sketches=Sketches
+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
+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.diagnostics.button.copied=Report copied to clipboard
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.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
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
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)
+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.
+# 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/app/src/processing/app/Base.java b/app/src/processing/app/Base.java
index ec22572d85..a083d139a0 100644
--- a/app/src/processing/app/Base.java
+++ b/app/src/processing/app/Base.java
@@ -2174,10 +2174,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();
+ PDEPreferencesKt.show();
}
diff --git a/app/src/processing/app/Messages.kt b/app/src/processing/app/Messages.kt
index fa9bc54a63..9b4ac7cd00 100644
--- a/app/src/processing/app/Messages.kt
+++ b/app/src/processing/app/Messages.kt
@@ -29,9 +29,15 @@ import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import com.formdev.flatlaf.FlatLightLaf
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.awt.ComposeDialog
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
import processing.app.ui.Toolkit
import processing.app.ui.theme.PDETheme
import java.awt.EventQueue
+import java.awt.Dimension
import java.awt.Frame
import java.io.PrintWriter
import java.io.StringWriter
@@ -284,14 +290,65 @@ class Messages {
}
}
}
-fun main(){
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun showDialog(title: String, content: @Composable (modifier: Modifier, dismiss: () -> Unit) -> Unit) {
+ ComposeDialog().apply {
+ isModal = true
+ setTitle(title)
+ size = Dimension(400, 400)
+ rootPane.putClientProperty("apple.awt.fullWindowContent", true)
+ rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
+ rootPane.putClientProperty("apple.awt.windowTitleVisible", false);
+
+
+ setContent {
+ PDETheme {
+ val density = LocalDensity.current
+ content(Modifier.onSizeChanged {
+ size = Dimension((it.width / density.density).toInt(), (it.height / density.density).toInt())
+ setLocationRelativeTo(null)
+ }, ::dispose)
+ }
+ }
+ setLocationRelativeTo(null)
+ isVisible = true
+ }
+}
+
+fun main() {
val types = mapOf(
"message" to { Messages.showMessage("Test Title", "This is a test message.") },
"warning" to { Messages.showWarning("Test Warning", "This is a test warning.", Exception("dfdsfjk")) },
"trace" to { Messages.showTrace("Test Trace", "This is a test trace.", Exception("Test Exception"), false) },
- "tiered_warning" to { Messages.showWarningTiered("Test Tiered Warning", "Primary message", "Secondary message", null) },
- "yes_no" to { Messages.showYesNoQuestion(null, "Test Yes/No", "Do you want to continue?", "Choose yes or no.") },
- "custom_question" to { Messages.showCustomQuestion(null, "Test Custom Question", "Choose an option:", "Select one of the options below.", 1, "Option 1", "Option 2", "Option 3") },
+ "tiered_warning" to {
+ Messages.showWarningTiered(
+ "Test Tiered Warning",
+ "Primary message",
+ "Secondary message",
+ null
+ )
+ },
+ "yes_no" to {
+ Messages.showYesNoQuestion(
+ null,
+ "Test Yes/No",
+ "Do you want to continue?",
+ "Choose yes or no."
+ )
+ },
+ "custom_question" to {
+ Messages.showCustomQuestion(
+ null,
+ "Test Custom Question",
+ "Choose an option:",
+ "Select one of the options below.",
+ 1,
+ "Option 1",
+ "Option 2",
+ "Option 3"
+ )
+ },
"error" to { Messages.showError("Test Error", "This is a test error.", null) },
)
Platform.init()
@@ -322,6 +379,7 @@ fun String.formatClassName() = this
.replace(".", "/")
.padEnd(40)
.colorizePathParts()
+
fun String.colorizePathParts() = split("/").joinToString("/") { part ->
"\u001B[${31 + (part.hashCode() and 0x7).rem(6)}m$part\u001B[0m"
}
\ No newline at end of file
diff --git a/app/src/processing/app/Preferences.java b/app/src/processing/app/Preferences.java
index 52b8f8dc3e..b0371592eb 100644
--- a/app/src/processing/app/Preferences.java
+++ b/app/src/processing/app/Preferences.java
@@ -87,8 +87,13 @@ static public void init() {
setBoolean("editor.input_method_support", true);
}
+
// next load user preferences file
preferencesFile = Base.getSettingsFile(PREFS_FILE);
+ var preferencesFileOverride = System.getProperty("processing.app.preferences.file");
+ if (preferencesFileOverride != null && !preferencesFileOverride.isEmpty()) {
+ preferencesFile = new File(preferencesFileOverride);
+ }
boolean firstRun = !preferencesFile.exists();
if (!firstRun) {
try {
@@ -179,9 +184,11 @@ static public void load(InputStream input) throws IOException {
String[] lines = PApplet.loadStrings(input); // Reads as UTF-8
for (String line : lines) {
- if ((line.length() == 0) ||
+ if ((line.isEmpty()) ||
(line.charAt(0) == '#')) continue;
+ line = line.replace("\\", "/"); // normalize slashes in paths
+
// this won't properly handle = signs being in the text
int equals = line.indexOf('=');
if (equals != -1) {
diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt
index 490110f8ac..1332ada00b 100644
--- a/app/src/processing/app/Preferences.kt
+++ b/app/src/processing/app/Preferences.kt
@@ -87,6 +87,14 @@ fun PreferencesProvider(content: @Composable () -> Unit) {
preferencesFile.createNewFile()
}
+ remember {
+ // check if the file has backward slashes
+ if (preferencesFile.readText().contains("\\")) {
+ val correctedText = preferencesFile.readText().replace("\\", "/")
+ preferencesFile.writeText(correctedText)
+ }
+ }
+
val update = watchFile(preferencesFile)
diff --git a/app/src/processing/app/ui/EditorFooter.java b/app/src/processing/app/ui/EditorFooter.java
index bc09b2376a..94860a0abf 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 0000000000..ac5bf2609b
--- /dev/null
+++ b/app/src/processing/app/ui/PDEPreferences.kt
@@ -0,0 +1,762 @@
+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.*
+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.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.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.*
+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 {
+ PDESwingWindow(
+ titleKey = "preferences",
+ fullWindowContent = true,
+ size = Dimension(850, 600),
+ minSize = Dimension(700, 500),
+ ) {
+ 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() })
+ }
+
+ /**
+ * 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,
+ )
+ ) {
+ CapturePreferences {
+ 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()
+ Box {
+ 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(
+ 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)
+ )
+ }
+ ) { 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)
+ }
+ }
+ /**
+ * 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(300),
+ ),
+ exit = fadeOut(
+ animationSpec = tween(300)
+ ) + slideOutVertically(
+ targetOffsetY = { it },
+ animationSpec = tween(300),
+ ),
+ modifier = Modifier
+ .graphicsLayer {
+ translationX = wiggle.value
+ }
+ ) {
+ 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"])
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Main function to run the preferences window standalone for testing & development.
+ */
+ @JvmStatic
+ fun main(args: Array) {
+ application {
+ PDEComposeWindow(
+ titleKey = "preferences",
+ fullWindowContent = true,
+ size = DpSize(850.dp, 600.dp),
+ minSize = DpSize(850.dp, 600.dp),
+ ) {
+ PDETheme(darkTheme = true) {
+ preferences()
+ }
+ }
+ PDEComposeWindow(
+ titleKey = "preferences",
+ fullWindowContent = true,
+ size = DpSize(850.dp, 600.dp),
+ minSize = DpSize(850.dp, 600.dp),
+ ) {
+ PDETheme(darkTheme = false) {
+ preferences()
+ }
+ }
+ }
+ }
+ }
+}
+
+
+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
+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.surfaceContainerLowest,
+ ),
+ 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/PDEWelcome.kt b/app/src/processing/app/ui/PDEWelcome.kt
index bfea548dd9..0370fc7533 100644
--- a/app/src/processing/app/ui/PDEWelcome.kt
+++ b/app/src/processing/app/ui/PDEWelcome.kt
@@ -379,14 +379,16 @@ fun PDEWelcome(base: Base? = null) {
modifier = Modifier
.width(350.dp + scrollMargin)
) {
- val examples = remember { mutableStateListOf(
- *listOf(
- Platform.getContentFile("modes/java/examples/Basics/Arrays/Array"),
- Platform.getContentFile("modes/java/examples/Basics/Camera/Perspective"),
- Platform.getContentFile("modes/java/examples/Basics/Color/Brightness"),
- Platform.getContentFile("modes/java/examples/Basics/Shape/LoadDisplayOBJ")
- ).map{ Sketch(path = it.absolutePath, name = it.name) }.toTypedArray()
- )}
+ val examples = remember {
+ mutableStateListOf(
+ *listOf(
+ Platform.getContentFile("modes/java/examples/Basics/Arrays/Array"),
+ Platform.getContentFile("modes/java/examples/Basics/Camera/Perspective"),
+ Platform.getContentFile("modes/java/examples/Basics/Color/Brightness"),
+ Platform.getContentFile("modes/java/examples/Basics/Shape/LoadDisplayOBJ")
+ ).map { Sketch(path = it.absolutePath, name = it.name) }.toTypedArray()
+ )
+ }
remember {
val sketches = mutableListOf()
@@ -401,7 +403,7 @@ fun PDEWelcome(base: Base? = null) {
sketchFolders.forEach { folder ->
gatherSketches(folder)
}
- if(sketches.isEmpty()) {
+ if (sketches.isEmpty()) {
return@remember
}
examples.clear()
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 0000000000..21b87ad5a7
--- /dev/null
+++ b/app/src/processing/app/ui/preferences/Coding.kt
@@ -0,0 +1,90 @@
+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.Code
+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 {
+ val coding = PDEPreferencePane(
+ nameKey = "preferences.pane.editor",
+ icon = { Icon(Icons.Default.Code, 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,
+ noTitle = true,
+ control = { preference, setPreference ->
+ 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(
+ 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
new file mode 100644
index 0000000000..a8bd559033
--- /dev/null
+++ b/app/src/processing/app/ui/preferences/General.kt
@@ -0,0 +1,175 @@
+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.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
+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 {
+ companion object{
+ val general = PDEPreferencePane(
+ nameKey = "preferences.pane.general",
+ icon = {
+ Icon(Icons.Default.Settings, contentDescription = "General Preferences")
+ }
+ )
+
+ fun register() {
+ PDEPreferences.register(
+ PDEPreference(
+ key = "sketchbook.path.four",
+ descriptionKey = "preferences.sketchbook_location",
+ pane = general,
+ noTitle = true,
+ control = { preference, updatePreference ->
+ 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,
+ modifier = Modifier
+ .clickable {
+ selectFolder(
+ locale["preferences.sketchbook_location.popup"],
+ File(preference ?: "")
+ ) { selectedFile: File? ->
+ if (selectedFile != null) {
+ updatePreference(selectedFile.absolutePath)
+ }
+ }
+ }
+ )
+ }
+ )
+ }
+ ),
+ PDEPreference(
+ key = "sketch.name.approach",
+ descriptionKey = "preferences.sketch_naming",
+ pane = general,
+ control = { preference, updatePreference ->
+ Column {
+ val options = if (Preferences.isInitialized()) SketchName.getOptions() else arrayOf(
+ "timestamp",
+ "untitled",
+ "custom"
+ )
+ options.toList().chunked(2).forEach { row ->
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ row.forEach { option ->
+ InputChip(
+ selected = preference == option,
+ onClick = {
+ updatePreference(option)
+ },
+ label = {
+ Text(option)
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+ ),
+ PDEPreference(
+ key = "editor.sync_folder_and_filename",
+ labelKey = "preferences.experimental",
+ 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.update_check",
+ pane = general,
+ control = { preference, updatePreference ->
+ Switch(
+ checked = preference.toBoolean(),
+ onCheckedChange = {
+ updatePreference(it.toString())
+ }
+ )
+ }
+ )
+ )
+ PDEPreferences.register(
+ PDEPreference(
+ key = "welcome.four.show",
+ descriptionKey = "preferences.show_welcome_screen",
+ pane = general,
+ control = { preference, updatePreference ->
+ Switch(
+ checked = preference.toBoolean(),
+ onCheckedChange = {
+ updatePreference(it.toString())
+ }
+ )
+ }
+ )
+ )
+ PDEPreferences.register(
+ PDEPreference(
+ key = "welcome.show",
+ 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
+
+ }) {
+ if (!copied) {
+ Text(LocalLocale.current["preferences.diagnostics.button"])
+ } else {
+ Text(LocalLocale.current["preferences.diagnostics.button.copied"])
+ }
+ }
+ }
+ )
+ )
+ }
+ }
+}
\ 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
new file mode 100644
index 0000000000..be0ee833c0
--- /dev/null
+++ b/app/src/processing/app/ui/preferences/Interface.kt
@@ -0,0 +1,265 @@
+package processing.app.ui.preferences
+
+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.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.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.*
+
+class Interface {
+ companion object{
+ val interfaceAndFonts = PDEPreferencePane(
+ nameKey = "preferences.pane.interface",
+ icon = {
+ Icon(Icons.Default.Brush, contentDescription = "Interface")
+ },
+ after = general
+ )
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ fun register() {
+ 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.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",
+ pane = interfaceAndFonts,
+ control = { preference, updatePreference ->
+ val range = 100f..300f
+
+ val prefs = LocalPreferences.current
+ 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)
+ ) {
+ Column(
+ modifier = Modifier
+ .widthIn(max = 200.dp)
+ ) {
+ Text(
+ text = if (automatic) "Auto" else zoomPerc,
+ )
+ Slider(
+ value = currentZoom,
+ onValueChange = {
+ currentZoom = it
+ },
+ onValueChangeFinished = {
+ prefs["editor.zoom.auto"] = automatic
+ updatePreference(zoomPerc)
+ },
+ valueRange = range,
+ steps = 3
+ )
+ }
+ }
+ }
+ )
+ )
+
+ PDEPreferences.register(
+ PDEPreference(
+ key = "editor.font.family",
+ descriptionKey = "preferences.editor_and_console_font",
+ pane = interfaceAndFonts,
+ control = { preference, updatePreference ->
+ var showOptions by remember { mutableStateOf(false) }
+ 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 = {
+ showOptions = false
+ },
+ ) {
+ families.forEach { family ->
+ DropdownMenuItem(
+ text = { Text(family) },
+ onClick = {
+ updatePreference(family)
+ showOptions = false
+ }
+ )
+ }
+
+ }
+ }
+ ),
+ 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
+ )
+ }
+ }
+ ), 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
+ fun languagesDropdown(showOptions: MutableState) {
+ val locale = LocalLocale.current
+ val languages = if (Preferences.isInitialized()) Language.getLanguages() else mapOf("en" to "English")
+ DropdownMenu(
+ expanded = showOptions.value,
+ onDismissRequest = {
+ showOptions.value = false
+ },
+ ) {
+ languages.forEach { family ->
+ DropdownMenuItem(
+ text = { Text(family.value) },
+ onClick = {
+ locale.set(Locale(family.key))
+ showOptions.value = false
+ }
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/preferences/Other.kt b/app/src/processing/app/ui/preferences/Other.kt
new file mode 100644
index 0000000000..8544f76945
--- /dev/null
+++ b/app/src/processing/app/ui/preferences/Other.kt
@@ -0,0 +1,96 @@
+package processing.app.ui.preferences
+
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Lightbulb
+import androidx.compose.material3.Icon
+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.PDEPreference
+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{
+ val other = PDEPreferencePane(
+ nameKey = "preferences.pane.other",
+ icon = {
+ Icon(Icons.Default.Lightbulb, contentDescription = "Other Preferences")
+ },
+ after = sketches
+ )
+
+ fun register(panes: PDEPreferencePanes) {
+ PDEPreferences.register(
+ PDEPreference(
+ key = "preferences.show_other",
+ descriptionKey = "preferences.other",
+ pane = other,
+ control = { preference, setPreference ->
+ val showOther = preference?.toBoolean() ?: false
+ Switch(
+ checked = showOther,
+ onCheckedChange = {
+ setPreference(it.toString())
+ }
+ )
+ if (!showOther) {
+ 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 =
+ panes[other]?.find { group -> group.any { preference -> preference.key == "preferences.show_other" } } as? MutableList
+
+ 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 descriptionKey = "preferences.$prefKey"
+ val preference = PDEPreference(
+ key = prefKey,
+ descriptionKey = if (locale.containsKey(descriptionKey)) descriptionKey else 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)
+ }
+ )
+ }
+ )
+ group?.add(preference)
+ }
+ onDispose {
+ group?.apply {
+ removeIf { it.key != "preferences.show_other" }
+ }
+ }
+ }
+ }
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
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 0000000000..b3fef23cd0
--- /dev/null
+++ b/app/src/processing/app/ui/preferences/Sketches.kt
@@ -0,0 +1,219 @@
+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.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
+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(Select_window, 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 || (device == defaultDevice && preference == "-1")) {
+ ButtonDefaults.buttonColors()
+ } else {
+ ButtonDefaults.outlinedButtonColors()
+ },
+ shape = RoundedCornerShape(12.dp),
+ onClick = {
+ setPreference(if (device == defaultDevice) "-1" else 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",
+ 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") },
+ 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(64.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)
+ }
+ )
+ }
+ )
+ )
+ }
+ 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/app/src/processing/app/ui/theme/Theme.kt b/app/src/processing/app/ui/theme/Theme.kt
index c59c5025cd..80dadb1fc7 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
/**
@@ -49,13 +50,21 @@ import processing.app.PreferencesProvider
fun PDETheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
-){
+) {
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)) {
CompositionLocalProvider(
LocalScrollbarStyle provides ScrollbarStyle(
@@ -109,38 +118,98 @@ fun main() {
) {
ComponentPreview("Colors") {
val colors = listOf>(
- Triple("Primary", MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.onPrimary),
- Triple("Secondary", MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.onSecondary),
- Triple("Tertiary", MaterialTheme.colorScheme.tertiary, MaterialTheme.colorScheme.onTertiary),
- Triple("Primary Container", MaterialTheme.colorScheme.primaryContainer, MaterialTheme.colorScheme.onPrimaryContainer),
- Triple("Secondary Container", MaterialTheme.colorScheme.secondaryContainer, MaterialTheme.colorScheme.onSecondaryContainer),
- Triple("Tertiary Container", MaterialTheme.colorScheme.tertiaryContainer, MaterialTheme.colorScheme.onTertiaryContainer),
- Triple("Error Container", MaterialTheme.colorScheme.errorContainer, MaterialTheme.colorScheme.onErrorContainer),
- Triple("Background", MaterialTheme.colorScheme.background, MaterialTheme.colorScheme.onBackground),
- Triple("Surface", MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.onSurface),
- Triple("Surface Variant", MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant),
+ Triple(
+ "Primary",
+ MaterialTheme.colorScheme.primary,
+ MaterialTheme.colorScheme.onPrimary
+ ),
+ Triple(
+ "Secondary",
+ MaterialTheme.colorScheme.secondary,
+ MaterialTheme.colorScheme.onSecondary
+ ),
+ Triple(
+ "Tertiary",
+ MaterialTheme.colorScheme.tertiary,
+ MaterialTheme.colorScheme.onTertiary
+ ),
+ Triple(
+ "Primary Container",
+ MaterialTheme.colorScheme.primaryContainer,
+ MaterialTheme.colorScheme.onPrimaryContainer
+ ),
+ Triple(
+ "Secondary Container",
+ MaterialTheme.colorScheme.secondaryContainer,
+ MaterialTheme.colorScheme.onSecondaryContainer
+ ),
+ Triple(
+ "Tertiary Container",
+ MaterialTheme.colorScheme.tertiaryContainer,
+ MaterialTheme.colorScheme.onTertiaryContainer
+ ),
+ Triple(
+ "Error Container",
+ MaterialTheme.colorScheme.errorContainer,
+ MaterialTheme.colorScheme.onErrorContainer
+ ),
+ Triple(
+ "Background",
+ MaterialTheme.colorScheme.background,
+ MaterialTheme.colorScheme.onBackground
+ ),
+ Triple(
+ "Surface",
+ MaterialTheme.colorScheme.surface,
+ MaterialTheme.colorScheme.onSurface
+ ),
+ Triple(
+ "Surface Variant",
+ MaterialTheme.colorScheme.surfaceVariant,
+ MaterialTheme.colorScheme.onSurfaceVariant
+ ),
Triple("Error", MaterialTheme.colorScheme.error, MaterialTheme.colorScheme.onError),
- Triple("Surface Lowest", MaterialTheme.colorScheme.surfaceContainerLowest, MaterialTheme.colorScheme.onSurface),
- Triple("Surface Low", MaterialTheme.colorScheme.surfaceContainerLow, MaterialTheme.colorScheme.onSurface),
- Triple("Surface", MaterialTheme.colorScheme.surfaceContainer, MaterialTheme.colorScheme.onSurface),
- Triple("Surface High", MaterialTheme.colorScheme.surfaceContainerHigh, MaterialTheme.colorScheme.onSurface),
- Triple("Surface Highest", MaterialTheme.colorScheme.surfaceContainerHighest, MaterialTheme.colorScheme.onSurface),
+ Triple(
+ "Surface Lowest",
+ MaterialTheme.colorScheme.surfaceContainerLowest,
+ MaterialTheme.colorScheme.onSurface
+ ),
+ Triple(
+ "Surface Low",
+ MaterialTheme.colorScheme.surfaceContainerLow,
+ MaterialTheme.colorScheme.onSurface
+ ),
+ Triple(
+ "Surface",
+ MaterialTheme.colorScheme.surfaceContainer,
+ MaterialTheme.colorScheme.onSurface
+ ),
+ Triple(
+ "Surface High",
+ MaterialTheme.colorScheme.surfaceContainerHigh,
+ MaterialTheme.colorScheme.onSurface
+ ),
+ Triple(
+ "Surface Highest",
+ MaterialTheme.colorScheme.surfaceContainerHighest,
+ MaterialTheme.colorScheme.onSurface
+ ),
)
Column {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
- val section = colors.subList(0,3)
- for((name, color, onColor) in section){
+ val section = colors.subList(0, 3)
+ for ((name, color, onColor) in section) {
Button(
colors = ButtonDefaults.buttonColors(containerColor = color),
onClick = {}) {
Text(name, color = onColor)
- }
+ }
}
}
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
- val section = colors.subList(3,7)
- for((name, color, onColor) in section){
+ val section = colors.subList(3, 7)
+ for ((name, color, onColor) in section) {
Button(
colors = ButtonDefaults.buttonColors(containerColor = color),
onClick = {}) {
@@ -149,8 +218,8 @@ fun main() {
}
}
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
- val section = colors.subList(7,11)
- for((name, color, onColor) in section){
+ val section = colors.subList(7, 11)
+ for ((name, color, onColor) in section) {
Button(
colors = ButtonDefaults.buttonColors(containerColor = color),
onClick = {}) {
@@ -224,7 +293,10 @@ fun main() {
})
}
ComponentPreview("Progress Indicator") {
- Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)){
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
CircularProgressIndicator()
LinearProgressIndicator()
}
@@ -248,7 +320,7 @@ fun main() {
Switch(!state, enabled = false, onCheckedChange = { state = it })
}
ComponentPreview("Slider") {
- Column{
+ Column {
var state by remember { mutableStateOf(0.5f) }
Slider(state, onValueChange = { state = it })
var rangeState by remember { mutableStateOf(0.25f..0.75f) }
@@ -266,7 +338,7 @@ fun main() {
ComponentPreview("Number Field") {
var number by remember { mutableStateOf("123") }
TextField(number, onValueChange = {
- if(it.all { char -> char.isDigit() }) {
+ if (it.all { char -> char.isDigit() }) {
number = it
}
}, label = { Text("Number Field") })
@@ -278,7 +350,7 @@ fun main() {
TextField(text, onValueChange = { text = it })
}
var text by remember { mutableStateOf("Outlined Text Field") }
- OutlinedTextField(text, onValueChange = { text = it})
+ OutlinedTextField(text, onValueChange = { text = it })
}
ComponentPreview("Dropdown Menu") {
var show by remember { mutableStateOf(false) }
@@ -307,7 +379,7 @@ fun main() {
}
ComponentPreview("Card") {
- Card{
+ Card {
Text("Hello, Tabs!", modifier = Modifier.padding(20.dp))
}
}
diff --git a/app/test/processing/app/PreferencesKtTest.kt b/app/test/processing/app/PreferencesKtTest.kt
index 6b5dbc5ea9..7dfcba3017 100644
--- a/app/test/processing/app/PreferencesKtTest.kt
+++ b/app/test/processing/app/PreferencesKtTest.kt
@@ -5,10 +5,11 @@ import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.*
-import java.util.Properties
+import java.util.*
import kotlin.io.path.createFile
import kotlin.io.path.createTempDirectory
import kotlin.test.Test
+import kotlin.test.assertEquals
class PreferencesKtTest{
@OptIn(ExperimentalTestApi::class)
@@ -58,4 +59,35 @@ class PreferencesKtTest{
onNodeWithTag("text").assertTextEquals(nextValue)
}
+
+ @OptIn(ExperimentalTestApi::class)
+ @Test
+ fun testWithBackwardSlashes() = runComposeUiTest {
+ val directory = createTempDirectory("preferences")
+ val tempPreferences = directory
+ .resolve("preferences.txt")
+ .createFile()
+ .toFile()
+
+ System.setProperty("processing.app.preferences.file", tempPreferences.absolutePath)
+ System.setProperty("processing.app.preferences.debounce", "0")
+ System.setProperty("processing.app.watchfile.forced", "true")
+ val testKey = "test.preferences.backward.slash"
+
+ val value = "C:\\Users\\Test\\Documents"
+ tempPreferences.writeText("$testKey=$value")
+ val replacedValue = value.replace("\\", "/")
+
+ setContent {
+ PreferencesProvider {
+ val preferences = LocalPreferences.current
+ Text(preferences[testKey] ?: "default", modifier = Modifier.testTag("text"))
+ }
+ }
+
+ onNodeWithTag("text").assertTextEquals(replacedValue)
+
+ Preferences.init()
+ assertEquals(replacedValue, Preferences.get(testKey))
+ }
}
\ No newline at end of file
diff --git a/core/src/processing/awt/ShimAWT.java b/core/src/processing/awt/ShimAWT.java
index 901f359bb2..304b8dd2ac 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);
}