Skip to content

Make Preferences standalone in app:utils - #1563

Open
diyaayay wants to merge 8 commits into
processing:mainfrom
diyaayay:preferences-decouple
Open

Make Preferences standalone in app:utils#1563
diyaayay wants to merge 8 commits into
processing:mainfrom
diyaayay:preferences-decouple

Conversation

@diyaayay

@diyaayay diyaayay commented Aug 6, 2026

Copy link
Copy Markdown

Resolves #1104

Splits processing.app.Preferences into a standalone storage class (processing.utils.Preferences in app:utils) and an app-side facade, so preferences work in contexts without the PDE: the Gradle plugin, CLI tools, and the preprocessor, as motivated in #1104 and the standalone-CLI discussion in #1522.

Changes

  • New processing.utils.Preferences: the storage half of the old class. Bundled defaults from the classpath, reading/writing preferences.txt, platform-specific keys, and the 3.x sketchbook migration. JDK-only (no core, no AWT/Swing). Errors are reported by throwing IOException with the cause chained instead of showing dialogs, so callers decide how to react.
  • processing.app.Preferences becomes a facade: keeps fonts/colors, Messages dialogs, and the proxy/native-chooser side effects in their original startup order, delegating storage to the utils class. No call sites change (~246 across app and java).
  • defaults.txt moves into app:utils resources: the published utils jar now carries its own defaults and works standalone. All readers (including the Compose PreferencesProvider) load it via getResourceAsStream, so nothing else changes.
  • Platform.getName(): platform name for preference-key suffixes, mirroring PConstants.platformNames. The four strings are part of the preferences file format, so a small mirror avoids giving app:utils a core dependency.
  • Change listeners (from the issue's TODO list): addChangeListener / removeChangeListener on the utils class. Fires only on real changes from set()/unset(); bulk loading is silent; a throwing listener can't block the change or other listeners. No consumers wired yet. First candidates are PDE components that currently re-read on window close, and possibly converging with the Compose side's PreferencesEvents later.
  • Preprocessor shim now delegates to the same class: java:preprocessor's processing.app.Preferences used to re-read preferences.txt on every call with the platform charset and had no defaults, so a standalone preprocessor could crash on getInteger("editor.tabs.size") with no prefs file present. It now resolves from the bundled defaults. Happy to split this commit into a follow-up PR if preferred.

Design notes

  • Why the cyclic dependency from the earlier attempt is gone: the original blocker was that the settings-folder logic lived in :app (Base/Platform) while Preferences was moving to :app:utils, so each module needed the other. Since then, Refactoring SketchException to be available outside of app #1196 created app:utils and Rewrite of the Base.getSettingsFolder() and Platform.getSettingsFolder() #1335 moved settings-folder resolution into processing.utils.Settings, so the knowledge Preferences needs now lives below it and all dependency arrows point one way. This PR adds no new dependencies on top of that: the remaining app couplings are handled by throwing instead of dialogs, and by passing the settings folder in as an argument (loadUserPrefs(File)), with Settings.getFolder() and the processing.app.preferences.file system property as fallbacks for standalone use.
  • Staged init: the app needs to set its own defaults (run.window.bgcolor, CJK input-method support) between loading the bundled defaults and the user's file, so init is split into loadDefaults() / loadUserPrefs(File), with init(File) composing them for standalone consumers. This keeps the PDE's startup order exactly as on main.
  • Delegation instead of inheritance: static methods only shadow, they don't override. An extends relationship would let calls through the parent type silently skip the app's error dialogs. The facade delegates explicitly; the cost is fully-qualified names (Java can't alias same-named imports), which was preferred over renaming a class and churning every reference.
  • Intentional behavior changes (both make failures visible rather than silent): a corrupt/unreadable preferences.txt surfaces as an exception (the PDE shows the same error dialog as before), and the utils class won't run the sketchbook migration/save over a file it couldn't read.

Builds on the direction @AhmedMagedC explored in #1209, thank you for the groundwork! Rebuilt on today's main since that branch predates the app:utils module: delegation instead of static inheritance, chained IOExceptions instead of a custom exception, no core dependency, and defaults bundled in the utils jar.

Tests

  • 17 new tests in app:utils (PreferencesTest): default loading, file creation, and backwards compatibility (3.x sketchbook migration, backslash normalization, platform-suffix keys), plus save/load round trips, the preferences.file override, getInteger fallback, sorted save, and the listener contract.
  • All suites pass on the rebased branch: app:utils, app (incl. PreferencesKtTest, so the Compose provider still finds the moved defaults.txt), and java (incl. ParserTests through the preprocessor).
  • Smoke-tested the utils jar standalone (jshell, nothing else on the classpath): fresh folder → defaults resolve → set/save → new JVM reads the value back. This caught a first-save folder-creation bug only real standalone use could hit, fixed with a regression test.
  • Manual: PDE runs normally; preference changes persist across restart.

Checklist

  • Tests pass locally

The bundled defaults are loaded from the classpath, so shipping them
inside the app:utils jar lets the standalone Preferences (upcoming)
resolve its own defaults without the app being present. All existing
readers load the file via getResourceAsStream() and are unaffected.
storage half of app.Preferences: defaults from classpath, load/save,
platform-specific keys. throws instead of showing dialogs; init split
so the app can insert its own defaults between stages. follows processing#1209.
app keeps fonts/colors, dialogs, and proxy/native-chooser side effects
in their original startup order; loading and saving now go through the
standalone class, with the settings folder passed in from Base.
replaces the per-call file re-read (platform charset, no defaults) with
the standalone class, so the preprocessor resolves preferences from the
bundled defaults even without a preferences.txt.
set()/unset() notify registered listeners when a value actually
changes; loading stays silent. per the processing#1104 todo list.
covers defaults loading, first-run creation, save/load round trips,
the preferences.file override, platform-specific keys, backslash
normalization, getInteger fallback, sorted save, the 3.x sketchbook
migration, and the change listener contract. adds a package-private
reset() for test isolation.
found while smoke-testing the utils jar standalone: the PDE and the
unit tests both pre-create the settings folder, but a standalone
caller passing a new location would fail on the first save.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Making Preferences standalone

1 participant