From 4acb84bd7035235bdb5afb15ca4d945baed23294 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 19 Nov 2025 17:52:28 +0100 Subject: [PATCH 1/4] Rewrite of the `Base.getSettingsFolder()` and `Platform.getSettingsFolder()` Rewrote both function so they fit into a single file, negating the need for hopping around when looking into what this functionality does. Also rewrote it so it is no longer generates random awt windows through the `Messages` class --- app/src/processing/app/Base.java | 61 +++++++--- app/src/processing/app/Platform.java | 30 +---- app/src/processing/app/Preferences.kt | 9 +- .../app/platform/DefaultPlatform.java | 34 +----- .../app/platform/LinuxPlatform.java | 42 +------ .../processing/app/platform/MacPlatform.java | 41 ++----- .../app/platform/WindowsPlatform.java | 82 ++----------- app/src/processing/app/ui/theme/Locale.kt | 5 +- .../main/java/processing/utils/Platform.java | 26 ++++ .../main/java/processing/utils/Settings.java | 114 ++++++++++++++++++ 10 files changed, 220 insertions(+), 224 deletions(-) create mode 100644 app/utils/src/main/java/processing/utils/Platform.java create mode 100644 app/utils/src/main/java/processing/utils/Settings.java diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index 1ff90146fe..7b1b6891f3 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -2212,29 +2212,50 @@ static public InputStream getLibStream(String filename) throws IOException { * something similar on Windows, a dot folder on Linux.) Removed this as a * preference for 3.0a3 because we need this to be stable, but adding back * for 4.0 beta 4 so that folks can do 'portable' versions again. + * + * @deprecated use processing.utils.Settings.getFolder() instead, this method will invoke AWT */ static public File getSettingsFolder() { - File settingsFolder = null; - - try { - settingsFolder = Platform.getSettingsFolder(); - - // create the folder if it doesn't exist already - if (!settingsFolder.exists()) { - if (!settingsFolder.mkdirs()) { - Messages.showError("Settings issues", - "Processing cannot run because it could not\n" + - "create a folder to store your settings at\n" + - settingsFolder, null); - } + try { + return processing.utils.Settings.getFolder(); + } catch (processing.utils.Settings.SettingsFolderException e) { + switch (e.getType()) { + case COULD_NOT_CREATE_FOLDER -> Messages.showError("Settings issues", + """ + Processing cannot run because it could not + create a folder to store your settings at + """ + e.getMessage(), null); + case WINDOWS_APPDATA_NOT_FOUND -> Messages.showError("Settings issues", + """ + Processing cannot run because it could not + find the AppData or LocalAppData folder on your system. + """, null); + case MACOS_LIBRARY_FOLDER_NOT_FOUND -> Messages.showError("Settings issues", + """ + Processing cannot run because it could not + find the Library folder on your system. + """, null); + case LINUX_CONFIG_FOLDER_NOT_FOUND -> Messages.showError("Settings issues", + """ + Processing cannot run because either your + XDG_CONFIG_HOME or SNAP_USER_COMMON is set + but the folder does not exist. + """, null); + case LINUX_SUDO_USER_ERROR -> Messages.showError("Settings issues", + """ + Processing cannot run because it was started + with sudo and Processing could not resolve + the original users home directory. + """, null); + default -> Messages.showTrace("An rare and unknowable thing happened", + """ + Could not get the settings folder. Please report: + http://github.com/processing/processing4/issues/new + """, + e, true); + } } - } catch (Exception e) { - Messages.showTrace("An rare and unknowable thing happened", - "Could not get the settings folder. Please report:\n" + - "http://github.com/processing/processing/issues/new", - e, true); - } - return settingsFolder; + throw new RuntimeException("Unreachable code in Base.getSettingsFolder()"); } diff --git a/app/src/processing/app/Platform.java b/app/src/processing/app/Platform.java index ed76318b98..3372e3f9bc 100644 --- a/app/src/processing/app/Platform.java +++ b/app/src/processing/app/Platform.java @@ -40,7 +40,7 @@ import java.util.Map; -public class Platform { +public class Platform extends processing.utils.Platform { static DefaultPlatform inst; /* @@ -136,12 +136,7 @@ static public float getSystemZoom() { } - static public File getSettingsFolder() throws Exception { - return inst.getSettingsFolder(); - } - - - static public File getDefaultSketchbookFolder() throws Exception { + static public File getDefaultSketchbookFolder() throws Exception { return inst.getDefaultSketchbookFolder(); } @@ -303,28 +298,7 @@ static public int getIndex(String platformName) { // the MACOSX constant would instead read as the LINUX constant. - /** - * returns true if Processing is running on a Mac OS X machine. - */ - static public boolean isMacOS() { - return System.getProperty("os.name").contains("Mac"); //$NON-NLS-1$ //$NON-NLS-2$ - } - - /** - * returns true if running on windows. - */ - static public boolean isWindows() { - return System.getProperty("os.name").contains("Windows"); //$NON-NLS-1$ //$NON-NLS-2$ - } - - - /** - * true if running on linux. - */ - static public boolean isLinux() { - return System.getProperty("os.name").contains("Linux"); //$NON-NLS-1$ //$NON-NLS-2$ - } // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt index c5645c9bbc..94f7d1250f 100644 --- a/app/src/processing/app/Preferences.kt +++ b/app/src/processing/app/Preferences.kt @@ -3,10 +3,13 @@ package processing.app import androidx.compose.runtime.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import processing.utils.Settings 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.StandardWatchEventKinds +import java.nio.file.WatchEvent +import java.util.* const val PREFERENCES_FILE_NAME = "preferences.txt" @@ -20,7 +23,7 @@ fun PlatformStart(){ fun loadPreferences(): Properties{ PlatformStart() - val settingsFolder = Platform.getSettingsFolder() + val settingsFolder = Settings.getFolder() val preferencesFile = settingsFolder.resolve(PREFERENCES_FILE_NAME) if(!preferencesFile.exists()){ diff --git a/app/src/processing/app/platform/DefaultPlatform.java b/app/src/processing/app/platform/DefaultPlatform.java index 18997755b7..54f0ec2788 100644 --- a/app/src/processing/app/platform/DefaultPlatform.java +++ b/app/src/processing/app/platform/DefaultPlatform.java @@ -23,24 +23,19 @@ package processing.app.platform; -import java.awt.Desktop; -import java.awt.Font; -import java.io.File; - -import javax.swing.UIManager; -import javax.swing.border.EmptyBorder; - import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.FlatLightLaf; -import com.sun.jna.Library; -import com.sun.jna.Native; - import processing.app.Base; import processing.app.Preferences; import processing.app.ui.Toolkit; import processing.awt.ShimAWT; import processing.core.PApplet; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.io.File; + /** * Used by Base for platform-specific tweaking, for instance finding the @@ -206,24 +201,7 @@ public void setInterfaceZoom() throws Exception { public void saveLanguage(String languageCode) { } - /** - * This function should throw an exception or return a value. - * Do not return null. - */ - public File getSettingsFolder() throws Exception { - File override = Base.getSettingsOverride(); - if (override != null) { - return override; - } - - // If no subclass has a behavior, default to making a - // ".processing" directory in the user's home directory. - File home = new File(System.getProperty("user.home")); - return new File(home, ".processing"); - } - - - /** + /** * @return if not overridden, a folder named "sketchbook" in user.home. * @throws Exception so that subclasses can throw a fit */ diff --git a/app/src/processing/app/platform/LinuxPlatform.java b/app/src/processing/app/platform/LinuxPlatform.java index 3426144cae..ddfb4f3c43 100644 --- a/app/src/processing/app/platform/LinuxPlatform.java +++ b/app/src/processing/app/platform/LinuxPlatform.java @@ -22,16 +22,13 @@ package processing.app.platform; -import java.io.File; -import java.awt.Desktop; -import java.awt.Toolkit; - import processing.app.Base; -import processing.app.Messages; import processing.app.Preferences; import processing.core.PApplet; import javax.swing.*; +import java.awt.*; +import java.io.File; public class LinuxPlatform extends DefaultPlatform { @@ -90,40 +87,7 @@ static public String getHomeDir(String user) throws Exception { } - @Override - public File getSettingsFolder() throws Exception { - File override = Base.getSettingsOverride(); - if (override != null) { - return override; - } - - // https://github.com/processing/processing4/issues/203 - // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html - - File configHome = null; - - // Check to see if the user has set a different location for their config - String configHomeEnv = System.getenv("XDG_CONFIG_HOME"); - if (configHomeEnv != null && !configHomeEnv.isBlank()) { - configHome = new File(configHomeEnv); - if (!configHome.exists()) { - Messages.err("XDG_CONFIG_HOME is set to " + configHomeEnv + " but does not exist."); - configHome = null; // don't use non-existent folder - } - } - String snapUserCommon = System.getenv("SNAP_USER_COMMON"); - if (snapUserCommon != null && !snapUserCommon.isBlank()) { - configHome = new File(snapUserCommon); - } - // If not set properly, use the default - if (configHome == null) { - configHome = new File(getHomeDir(), ".config"); - } - return new File(configHome, "processing"); - } - - - @Override + @Override public File getDefaultSketchbookFolder() throws Exception { return new File(getHomeDir(), "sketchbook"); } diff --git a/app/src/processing/app/platform/MacPlatform.java b/app/src/processing/app/platform/MacPlatform.java index f26c8f2c66..59f016b17f 100644 --- a/app/src/processing/app/platform/MacPlatform.java +++ b/app/src/processing/app/platform/MacPlatform.java @@ -22,23 +22,20 @@ package processing.app.platform; +import processing.app.Base; +import processing.app.Messages; +import processing.app.ui.About; +import processing.core.PApplet; +import processing.data.StringList; + +import javax.swing.*; import java.awt.*; -import java.awt.desktop.AppReopenedEvent; import java.awt.desktop.AppReopenedListener; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; -import javax.swing.JMenu; -import javax.swing.JMenuBar; - -import processing.app.Base; -import processing.app.Messages; -import processing.app.ui.About; -import processing.core.PApplet; -import processing.data.StringList; - /** * Platform handler for macOS. @@ -112,16 +109,7 @@ public void initBase(Base base) { } - public File getSettingsFolder() throws Exception { - File override = Base.getSettingsOverride(); - if (override != null) { - return override; - } - return new File(getLibraryFolder(), "Processing"); - } - - - public File getDefaultSketchbookFolder() throws Exception { + public File getDefaultSketchbookFolder() throws Exception { return new File(getDocumentsFolder(), "Processing"); } @@ -144,19 +132,6 @@ public void openURL(String url) throws Exception { // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . - // TODO I suspect this won't work much longer, since access to the user's - // home directory seems verboten on more recent macOS versions [fry 191008] - // However, anecdotally it seems that just using the name works, - // and the localization is handled transparently. [fry 220116] - // https://github.com/processing/processing4/issues/9 - protected String getLibraryFolder() throws FileNotFoundException { - File folder = new File(System.getProperty("user.home"), "Library"); - if (!folder.exists()) { - throw new FileNotFoundException("Folder missing: " + folder); - } - return folder.getAbsolutePath(); - } - // TODO See above, and https://github.com/processing/processing4/issues/9 protected String getDocumentsFolder() throws FileNotFoundException { diff --git a/app/src/processing/app/platform/WindowsPlatform.java b/app/src/processing/app/platform/WindowsPlatform.java index b74a1674c3..3ec8941a98 100644 --- a/app/src/processing/app/platform/WindowsPlatform.java +++ b/app/src/processing/app/platform/WindowsPlatform.java @@ -22,20 +22,22 @@ package processing.app.platform; -import java.awt.*; -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; - import com.sun.jna.Library; import com.sun.jna.Native; -import com.sun.jna.platform.win32.*; - -import processing.app.*; +import com.sun.jna.platform.win32.GDI32; +import com.sun.jna.platform.win32.Shell32Util; +import com.sun.jna.platform.win32.ShlObj; +import com.sun.jna.platform.win32.WinDef; +import processing.app.Base; +import processing.app.Messages; +import processing.app.Preferences; import processing.app.platform.WindowsRegistry.REGISTRY_ROOT_KEY; - import processing.core.PApplet; +import java.awt.*; +import java.io.File; +import java.io.UnsupportedEncodingException; + // With the changes to include .pyde files for 3.4, this class is // a bit of a mess. Registering a single extension has moved to @@ -351,54 +353,6 @@ protected void checkPath() { } - // looking for Documents and Settings/blah/Application Data/Processing - public File getSettingsFolder() throws Exception { - File override = Base.getSettingsOverride(); - if (override != null) { - return override; - } - - try { - String appDataRoaming = getAppDataPath(); - if (appDataRoaming != null) { - File settingsFolder = new File(appDataRoaming, APP_NAME); - if (settingsFolder.exists() || settingsFolder.mkdirs()) { - return settingsFolder; - } - } - - String appDataLocal = getLocalAppDataPath(); - if (appDataLocal != null) { - File settingsFolder = new File(appDataLocal, APP_NAME); - if (settingsFolder.exists() || settingsFolder.mkdirs()) { - return settingsFolder; - } - } - - if (appDataRoaming == null && appDataLocal == null) { - throw new IOException("Could not get the AppData folder"); - } - - // https://github.com/processing/processing/issues/3838 - throw new IOException("Permissions error: make sure that " + - appDataRoaming + " or " + appDataLocal + - " is writable."); - - } catch (UnsatisfiedLinkError ule) { - String path = new File("lib").getCanonicalPath(); - - String msg = Util.containsNonASCII(path) ? - """ - Please move Processing to a location with only - ASCII characters in the path and try again. - https://github.com/processing/processing/issues/3543 - """ : - "Could not find JNA support files, please reinstall Processing."; - Messages.showError("Windows JNA Problem", msg, ule); - return null; // unreachable - } - } - /* What's happening internally with JNA https://github.com/java-native-access/jna/blob/master/contrib/platform/src/com/sun/jna/platform/win32/Shell32.java @@ -413,19 +367,7 @@ public File getSettingsFolder() throws Exception { */ - /** Get the Users\name\AppData\Roaming path to write settings files. */ - static private String getAppDataPath() { - return Shell32Util.getSpecialFolderPath(ShlObj.CSIDL_APPDATA, true); - } - - - /** Get the Users\name\AppData\Local path as a settings fallback. */ - static private String getLocalAppDataPath() { - return Shell32Util.getSpecialFolderPath(ShlObj.CSIDL_LOCAL_APPDATA, true); - } - - - /** Get the Documents and Settings\name\My Documents\Processing folder. */ + /** Get the Documents and Settings\name\My Documents\Processing folder. */ public File getDefaultSketchbookFolder() throws Exception { String documentsPath = getDocumentsPath(); if (documentsPath != null) { diff --git a/app/src/processing/app/ui/theme/Locale.kt b/app/src/processing/app/ui/theme/Locale.kt index 254c0946c1..9165c6c1bd 100644 --- a/app/src/processing/app/ui/theme/Locale.kt +++ b/app/src/processing/app/ui/theme/Locale.kt @@ -3,11 +3,10 @@ package processing.app.ui.theme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf -import processing.app.LocalPreferences import processing.app.Messages -import processing.app.Platform import processing.app.PlatformStart import processing.app.watchFile +import processing.utils.Settings import java.io.File import java.io.InputStream import java.util.* @@ -34,7 +33,7 @@ val LocalLocale = compositionLocalOf { Locale() } fun LocaleProvider(content: @Composable () -> Unit) { PlatformStart() - val settingsFolder = Platform.getSettingsFolder() + val settingsFolder = Settings.getFolder() val languageFile = File(settingsFolder, "language.txt") watchFile(languageFile) diff --git a/app/utils/src/main/java/processing/utils/Platform.java b/app/utils/src/main/java/processing/utils/Platform.java new file mode 100644 index 0000000000..497613e51a --- /dev/null +++ b/app/utils/src/main/java/processing/utils/Platform.java @@ -0,0 +1,26 @@ +package processing.utils; + +public class Platform { + /** + * returns true if Processing is running on a Mac OS X machine. + */ + static public boolean isMacOS() { + return System.getProperty("os.name").contains("Mac"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + + /** + * returns true if running on windows. + */ + static public boolean isWindows() { + return System.getProperty("os.name").contains("Windows"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + + /** + * true if running on linux. + */ + static public boolean isLinux() { + return System.getProperty("os.name").contains("Linux"); //$NON-NLS-1$ //$NON-NLS-2$ + } +} diff --git a/app/utils/src/main/java/processing/utils/Settings.java b/app/utils/src/main/java/processing/utils/Settings.java new file mode 100644 index 0000000000..c3ecfc0417 --- /dev/null +++ b/app/utils/src/main/java/processing/utils/Settings.java @@ -0,0 +1,114 @@ +package processing.utils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; + +public class Settings { + public static File getFolder() throws SettingsFolderException { + try { + var folder = getFolderForPlatform(); + if (!folder.exists() && !folder.mkdirs()) { + throw new SettingsFolderException(SettingsFolderException.Type.COULD_NOT_CREATE_FOLDER, folder.getAbsolutePath()); + } + return folder; + } catch (RuntimeException e) { + throw new SettingsFolderException(SettingsFolderException.Type.UNKNOWN); + } + } + + private static File getFolderForPlatform() throws SettingsFolderException { + // TODO: Detect override file, + + if (Platform.isWindows()) { + var options = new String[]{ + "APPDATA", + "LOCALAPPDATA" + }; + for (String option : options) { + var folder = new File(System.getenv(option), "Processing"); + if (!folder.exists() && !folder.mkdirs()) { + continue; + } + return folder; + } + throw new SettingsFolderException(SettingsFolderException.Type.WINDOWS_APPDATA_NOT_FOUND); + } + if (Platform.isMacOS()) { + var folder = new File(System.getProperty("user.home"), "Library"); + if (!folder.exists()) { + throw new SettingsFolderException(SettingsFolderException.Type.MACOS_LIBRARY_FOLDER_NOT_FOUND); + } + return new File(folder, "Processing"); + } + if (Platform.isLinux()) { + var options = new String[]{ + "SNAP_USER_COMMON", + "XDG_CONFIG_HOME" + }; + for (String option : options) { + var configHomeEnv = System.getenv(option); + if (configHomeEnv == null || configHomeEnv.isBlank()) { + continue; + } + var parentFolder = new File(configHomeEnv); + if (!parentFolder.exists()) { + throw new SettingsFolderException(SettingsFolderException.Type.LINUX_CONFIG_FOLDER_NOT_FOUND); + } + var folder = new File(parentFolder, "processing"); + if (!folder.exists() && !folder.mkdirs()) { + continue; + } + return folder; + } + var subfolder = ".config/processing"; + var isSudo = System.getenv("SUDO_USER"); + if (isSudo == null || isSudo.isEmpty()) { + return new File(System.getProperty("user.home") + subfolder); + } + // If user is SUDO_USER, try to get their home directory + try { + var process = Runtime.getRuntime().exec( + new String[]{ + "/bin/sh", "-c", "echo ~" + isSudo + } + ); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + return new File(reader.readLine() + subfolder); + } + } catch (Exception e) { + throw new SettingsFolderException(SettingsFolderException.Type.LINUX_SUDO_USER_ERROR); + } + } + + // If all else fails, use ~/.processing + return new File(System.getProperty("user.home"), ".processing"); + } + + + public static class SettingsFolderException extends Exception { + public enum Type { + COULD_NOT_CREATE_FOLDER, + WINDOWS_APPDATA_NOT_FOUND, + MACOS_LIBRARY_FOLDER_NOT_FOUND, + LINUX_CONFIG_FOLDER_NOT_FOUND, + LINUX_SUDO_USER_ERROR, + UNKNOWN + } + + private final Type type; + + public SettingsFolderException(Type type) { + this.type = type; + } + + public SettingsFolderException(Type type, String message) { + super(message); + this.type = type; + } + + public Type getType() { + return type; + } + } +} From f472518f134a15d5ce3b1196ddc0ff6dc244633c Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Wed, 19 Nov 2025 18:04:25 +0100 Subject: [PATCH 2/4] Fixed issue with missing / --- app/utils/src/main/java/processing/utils/Settings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/src/main/java/processing/utils/Settings.java b/app/utils/src/main/java/processing/utils/Settings.java index c3ecfc0417..f1ec4f7b5a 100644 --- a/app/utils/src/main/java/processing/utils/Settings.java +++ b/app/utils/src/main/java/processing/utils/Settings.java @@ -61,7 +61,7 @@ private static File getFolderForPlatform() throws SettingsFolderException { } return folder; } - var subfolder = ".config/processing"; + var subfolder = "/.config/processing"; var isSudo = System.getenv("SUDO_USER"); if (isSudo == null || isSudo.isEmpty()) { return new File(System.getProperty("user.home") + subfolder); From 15588f3ad4bc2efd962de467e33d9e4509639c29 Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 20 Nov 2025 10:48:14 +0100 Subject: [PATCH 3/4] Added both options for overrides - Added the previous settings override in base again - Added a system property to override the settings folder within tests --- app/src/processing/app/Base.java | 4 ++ .../main/java/processing/utils/Settings.java | 5 ++- app/utils/src/test/java/SettingsTest.java | 40 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 app/utils/src/test/java/SettingsTest.java diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index 7b1b6891f3..4a527cd6f3 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -2216,6 +2216,10 @@ static public InputStream getLibStream(String filename) throws IOException { * @deprecated use processing.utils.Settings.getFolder() instead, this method will invoke AWT */ static public File getSettingsFolder() { + var override = getSettingsOverride(); + if (override != null) { + return override; + } try { return processing.utils.Settings.getFolder(); } catch (processing.utils.Settings.SettingsFolderException e) { diff --git a/app/utils/src/main/java/processing/utils/Settings.java b/app/utils/src/main/java/processing/utils/Settings.java index f1ec4f7b5a..e4e7aa4a4f 100644 --- a/app/utils/src/main/java/processing/utils/Settings.java +++ b/app/utils/src/main/java/processing/utils/Settings.java @@ -18,7 +18,10 @@ public static File getFolder() throws SettingsFolderException { } private static File getFolderForPlatform() throws SettingsFolderException { - // TODO: Detect override file, + var settingsOverride = System.getProperty("processing.settings.folder"); + if (settingsOverride != null && !settingsOverride.isEmpty()) { + return new File(settingsOverride); + } if (Platform.isWindows()) { var options = new String[]{ diff --git a/app/utils/src/test/java/SettingsTest.java b/app/utils/src/test/java/SettingsTest.java new file mode 100644 index 0000000000..c03fb6732d --- /dev/null +++ b/app/utils/src/test/java/SettingsTest.java @@ -0,0 +1,40 @@ +import org.junit.jupiter.api.Test; +import processing.utils.Settings; + +import java.io.IOException; +import java.nio.file.Files; + +public class SettingsTest { + + /** + * Requesting the settings folder should create it if it doesn't exist + */ + @Test + public void testSettingsFolder() { + try { + var folder = Settings.getFolder(); + assert (folder.exists()); + } catch (Settings.SettingsFolderException e) { + assert (false); + } + } + + /** + * Overriding the settings folder via system property should work + */ + @Test + public void testOverrideFolder() throws IOException { + var settings = Files.createTempDirectory("settings_test"); + System.setProperty("processing.settings.folder", settings.toString()); + + try { + var folder = Settings.getFolder(); + assert (folder.toPath().toString().equals(settings.toString())); + } catch (Settings.SettingsFolderException e) { + assert (false); + } finally { + System.clearProperty("processing.settings.folder"); + Files.deleteIfExists(settings); + } + } +} From 1c27d78a18af9400d610e8bbb702051eaaf5b31a Mon Sep 17 00:00:00 2001 From: Stef Tervelde Date: Thu, 27 Nov 2025 15:05:40 +0100 Subject: [PATCH 4/4] Add support for portable settings detection Introduces logic to detect a preferences.txt file in the same folder as the running executable or jar. If found, settings are loaded from this location, improving portability and allowing users to override default settings without modifying system directories. --- .../main/java/processing/utils/Settings.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/utils/src/main/java/processing/utils/Settings.java b/app/utils/src/main/java/processing/utils/Settings.java index e4e7aa4a4f..7ad34b9b76 100644 --- a/app/utils/src/main/java/processing/utils/Settings.java +++ b/app/utils/src/main/java/processing/utils/Settings.java @@ -3,6 +3,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; +import java.util.Optional; public class Settings { public static File getFolder() throws SettingsFolderException { @@ -23,6 +24,11 @@ private static File getFolderForPlatform() throws SettingsFolderException { return new File(settingsOverride); } + var portableSettings = FindPortableSettings(); + if (portableSettings.isPresent()) { + return portableSettings.get(); + } + if (Platform.isWindows()) { var options = new String[]{ "APPDATA", @@ -88,6 +94,32 @@ private static File getFolderForPlatform() throws SettingsFolderException { return new File(System.getProperty("user.home"), ".processing"); } + /** + * find a preferences.txt file in the same folder as the running jar/executable + * + * @return Optional File pointing to preferences.txt if found, empty otherwise + */ + private static Optional FindPortableSettings() { + var command = ProcessHandle.current().info().command(); + if (command.isEmpty()) return Optional.empty(); + + var path = command.get(); + path = path.replaceAll("/[^/]+$", ""); + + if (Platform.isMacOS()) { + // On macOS, the executable is inside the .app bundle, so we need to go up to above the .app folder + path = path.replaceAll("/[^/]+\\.app/.*$", ""); + } + var file = new File(path, "preferences.txt"); + if (System.getenv().containsKey("DEBUG")) + System.out.println("Looking for portable settings at: " + file.getAbsolutePath()); + + if (!file.exists()) { + return Optional.empty(); + } + return Optional.of(new File(path)); + + } public static class SettingsFolderException extends Exception { public enum Type {