Status: proposal Target release: 3.0.0 Tracking: GitHub issue #252 (config location); no dedicated issue for presets (user-request driven) Scope: JavaFX GUI; CLI unchanged (but shares the config-location code path)
Today every signing setting lives in a single flat properties file at ~/.JSignPdf. There is only one implicit profile — "last used" — so anyone who routinely switches between several configurations has to re-enter them by hand each time.
Issue #252 (unrelated, but adjacent) asks that the config file move out of $HOME and into an XDG-compliant location. A preset feature needs a directory the app owns anyway, so both changes land together in 3.0.0.
Goals
- Let the user save an arbitrary number of named signature-setting presets and switch between them with one click.
- Store configuration in a platform-appropriate, XDG-respecting location on Linux;
%APPDATA%on Windows;~/Library/Application Supporton macOS. - Do not break existing installs: if users already have
~/.JSignPdf, their settings must survive the upgrade untouched. - Allow arbitrary display names (Unicode, diacritics, punctuation) without imposing filename-safety rules on users.
- Keep the data format as plain Java properties so a user can inspect and hand-edit preset files if they want.
Non-goals
- No export/import UI. (Users can copy
*.propertiesfiles around themselves; we don't need to ship a dialog for it.) - No cross-machine preset sharing as a first-class feature.
JSignEncryptoruses a machine-local seed, so encrypted passwords won't decrypt elsewhere. We accept this. - No split of config / state / cache across separate XDG dirs. Everything stays under one
jsignpdf/directory for now. - No CLI changes. The CLI will continue to load the same main config file (from its new location), but has no notion of presets.
The following were pinned by the project owner before this doc was written:
| Decision | Choice |
|---|---|
| Migration strategy | One-shot: if the new config dir exists, use it; otherwise fall back to ~/.JSignPdf, then seed the new dir from it on first write. |
| Preset display name | Stored as preset.displayName inside each preset file. |
| Preset filename | Always written as preset-<epochMillis>.properties. Reads accept any *.properties file in the presets dir. |
| Export/import UI | Out of scope for 3.0.0. |
| UI surface | Both a top-level Presets menu and an always-visible combo box in the main window. |
PropertyProvider |
Refactor away from the singleton where it makes sense. |
Sections 4–10 build on these choices.
Introduce ConfigLocationResolver (new class, net.sf.jsignpdf.utils). Its job is to resolve a single Path — the application's config directory — exactly once per JVM, and to hand out Paths for the main config file and the presets subdir.
Resolution order:
- Environment override:
JSIGNPDF_CONFIG_DIRenv var. If set (non-empty), use that path verbatim. For portable installs, tests, and power users. - Platform-native location — used both when reading (if it exists) and when creating a fresh dir:
- Linux / BSD / other Unix:
$XDG_CONFIG_HOME/jsignpdf/ifXDG_CONFIG_HOMEis set and non-empty, otherwise$HOME/.config/jsignpdf/(lowercase name per XDG convention). - Windows:
%APPDATA%\JSignPdf\, falling back to%USERPROFILE%\AppData\Roaming\JSignPdf\ifAPPDATAis unset. - macOS:
$HOME/Library/Application Support/JSignPdf/.
- Linux / BSD / other Unix:
- Legacy fallback:
$HOME/.JSignPdf(as a file, not a dir — see §6 for how it's handled).
OS detection via System.getProperty("os.name") with the usual toLowerCase() substring checks (win, mac, otherwise Unix).
If HOME / user.home is missing entirely (exotic headless envs), log a warning and refuse to persist anything rather than dumping files in the CWD. CLI can still sign a document; only persistence breaks.
After migration, everything is under one dir. Call it <cfg> for brevity.
<cfg>/
config.properties # former ~/.JSignPdf content
presets/
preset-1745270400123.properties
preset-1745270500456.properties
Main config (config.properties). Same keys and encoding as the existing ~/.JSignPdf. No schema change — the main config tracks "last used settings," not "last used preset." Preset selection is fire-and-forget (see §9.1), so nothing about it is persisted.
Preset file. A properties file with:
preset.displayName— required, UTF-8, the user-visible name. Can contain any character except control chars and/\(validated in the dialog).preset.createdAt— optional ISO-8601 timestamp, informational.- A subset of the keys from
BasicSignerOptions.storeOptions()— see §7 for the exact scope.
No preset.name or other identity field beyond displayName; the file itself is the identity.
On first ConfigLocationResolver call per JVM:
- Resolve target dir (§4, step 1 or 2).
- If target dir exists → done, it is authoritative.
- If target dir doesn't exist and
$HOME/.JSignPdfexists as a file:- Create target dir and its
presets/subdir. - Copy
$HOME/.JSignPdf→<cfg>/config.properties(byte-for-byte copy,Files.copy). - Leave the legacy file in place. Rationale: if the user downgrades to a pre-3.0.0 build, their old install still works.
- Log an INFO line:
Migrated legacy settings from ~/.JSignPdf to <cfg>/config.properties.
- Create target dir and its
- If target dir doesn't exist and no legacy file exists → create target dir empty, the app starts with defaults. (Fresh install.)
After migration, the legacy file is never read or written by the new code. We do not attempt to detect divergence if both exist (e.g. user ran an old version after a new one); the new dir always wins.
A preset captures the signing configuration, not the session state. Concretely:
Included (the bulk of BasicSignerOptions):
- Keystore:
ksType,ksFile,keyAlias,keyIndex(password intentionally excluded — see below). - Passwords:
ksPasswd,keyPasswd,pdfOwnerPwd,pdfUserPwd,tsaPasswd,tsaCertFilePwd. - Signature metadata:
signerName,reason,location,contact. - Visible signature:
visible,page,positionLLX/Y/URX/Y,bgImgScale,renderMode,l2Text,l4Text,l2TextFontSize,imgPath,bgImgPath,acro6Layers. - TSA: all
tsa.*keys. - PDF encryption and rights (all current keys).
- OCSP/CRL, proxy.
Excluded:
- Recent-file list, last-used input/output paths, window geometry — these aren't part of the signing configuration.
Current state: PropertyProvider is a singleton tied to the hardcoded $HOME/.JSignPdf path. It holds a Properties instance and exposes typed getters/setters (getAsInt, setPropNullSensitive, etc.).
Problem: presets are N independent property stores; a single-instance design doesn't fit. Also, path resolution belongs in ConfigLocationResolver, not here.
Refactor:
- Remove the
getInstance()singleton and the hardcodedPROPERTY_FILEconstant. Make the constructor public and accept aPath. loadDefault()/saveDefault()go away; callers useload()/save()which operate on the path handed to the constructor.- Add a thin factory on
PropertyStoreFactory(new class):mainConfig()→PropertyProviderbacked by<cfg>/config.properties.preset(String filename)→PropertyProviderbacked by<cfg>/presets/<filename>.newPreset()→PropertyProviderbacked by a freshpreset-<epochMillis>.properties(with collision-retry).
- All call sites currently using
PropertyProvider.getInstance()are rewritten to go throughPropertyStoreFactory.mainConfig()(dependency-inject the instance where practical, use the factory at composition roots otherwise).
The typed getters/setters (getAsInt, getAsBool, …) stay intact — that API is fine.
net.sf.jsignpdf.fx.preset.PresetManager. Lives in the JavaFX module; owns the list of presets as an ObservableList<Preset> so the combo box can bind to it directly.
Responsibilities:
- Scan
<cfg>/presets/for*.propertieson startup; buildPresetrecords (filename,displayName,createdAt). - Provide:
saveAsNew(BasicSignerOptions, String displayName),overwrite(Preset, BasicSignerOptions),rename(Preset, newName),delete(Preset),load(Preset).
No notion of a "currently active" preset is tracked. Loading is a one-way transfer (preset → live options); after load returns, the manager has no opinion about which preset the user is "on." That means no dirty tracking, no confirmation prompts on switch, no restore-on-launch.
saveAsNew always writes a fresh preset-<epochMillis>.properties (retry with -2, -3, … on collision within the same millisecond). overwrite updates the contents of an existing preset file in place, preserving its filename. rename rewrites only preset.displayName inside the existing file — no file move. delete is Files.delete.
Add two methods:
public void loadFromPreset(PropertyProvider store);
public void storeToPreset(PropertyProvider store);These reuse the existing per-field load/store logic. The existing loadOptions() / storeOptions() — which operate on the main config — stay as-is, except they now take a PropertyProvider argument instead of reaching for the singleton.
The UI has two surfaces, with a clear split of concerns:
- Toolbar combo — one-shot loader. Pick an entry, settings get populated, end of interaction.
- Presets menu — the two mutating actions (save new / manage). Rename, delete, and in-place overwrite live inside the Manage dialog.
The design is deliberately stateless: there is no "active preset" tracked anywhere — not in the UI, not in config, not in memory. Load is fire-and-forget.
The combo is placed in the existing main-window toolbar, alongside the Open / zoom / other controls.
[ ☰ File | Presets | Help ]
[ Toolbar: [Open] [Zoom-] [Zoom+] ... Preset: [ Load preset… ▾ ] ]
- Combo contents come straight from
PresetManager.getPresets()(ObservableList), sorted by display name. - Selecting a preset immediately loads it —
PresetManager.load(preset)runs on the selection-change event and callssigningVM.syncFromOptions(opts). No intermediate "Load" button, no confirmation prompt, no dirty-state detection. If the user has unsaved edits when they pick a preset, those edits are overwritten; this is the explicit tradeoff of fire-and-forget. - The combo is purely a launcher — it retains no "selected preset" state. After the load fires, the combo visually returns to its placeholder (
Load preset…). This reinforces that there is no current preset, only current settings. - When the presets list is empty, the combo is disabled and shows
No presets savedas its placeholder.
A new top-level menu between File and Help:
Presets
├─ Save current as new preset…
└─ Manage presets…
-
Save current as new preset… opens a dialog for the display name (validated per §9.3) and writes a new
preset-<epochMillis>.propertiesfile. -
Manage presets… opens a dialog listing all presets. Each row has three actions:
- Rename — inline edit of the display name, validated per §9.3.
- Overwrite with current settings — copies the live
BasicSignerOptionsinto the selected preset file (preserving filename and createdAt). Confirmation prompt first, since this is destructive to the preset's previous contents. - Delete — confirmation, then
Files.delete.
No export/import. No reordering.
There is no Load submenu — the toolbar combo is the canonical load affordance. There is also no "Save" item separate from "Save as new" — without an active-preset concept, there is nothing to "save over," so the only two save-path operations are "create new" (menu) and "overwrite this specific one" (Manage dialog).
Enforced in the Save / Save as / Rename dialogs, before PresetManager is called:
- Trim leading/trailing whitespace.
- Reject empty or whitespace-only.
- Reject control characters (
< 0x20,0x7F). - Length cap at 60 chars — UI sanity, not a filesystem limit.
- Reject if another preset already uses the same display name (case-insensitive).
None of this touches the filename — that remains preset-<epochMillis>.properties regardless.
New keys added to net.sf.jsignpdf.translations.messages (English as the source of truth):
jfx.gui.preset.label— "Preset:"jfx.gui.preset.placeholder— "Load preset…"jfx.gui.preset.placeholder.empty— "No presets saved"jfx.gui.menu.presets,jfx.gui.menu.presets.saveAsNew,.managejfx.gui.preset.dialog.saveAsNew.title,.saveAsNew.prompt,.rename.titlejfx.gui.preset.dialog.delete.confirm— "Delete preset '{0}'?"jfx.gui.preset.dialog.overwrite.confirm— "Overwrite preset '{0}' with current settings?"jfx.gui.preset.validation.empty,.illegalChar,.tooLong,.duplicatejfx.gui.preset.manage.title,.column.name,.column.created,.button.rename,.button.overwrite,.button.delete
Unit:
ConfigLocationResolverwith per-OS system-property override and env-var stubs: Linux with/withoutXDG_CONFIG_HOME, Windows with/withoutAPPDATA, macOS,JSIGNPDF_CONFIG_DIRoverride, missinguser.home.- Migration: legacy-present-no-new-dir → dir created and file copied, byte-for-byte; legacy-absent-new-dir-present → no-op; both-present → new dir wins, legacy untouched.
PresetManager: save → reload → display name round-trips; rename preserves filename andcreatedAt; overwrite preserves filename andcreatedAtbut updates the rest; delete removes file; scan ignores files that lackpreset.displayName.- Display-name validation: whitespace, control chars,
/,\, length cap, case-insensitive uniqueness. - Filename collision on
System.currentTimeMillis()within the same millisecond: retry with counter suffix (preset-<ts>-2.properties).
Integration:
- End-to-end: open app with a legacy
~/.JSignPdfin a temp home, verify dir created and content migrated. - Save preset, restart, verify the preset file is still present in the toolbar combo (no active-preset restore — the combo starts at placeholder).
- Preset file with only
preset.displayNameset (all-defaults preset) loads without error.
Manual / UI:
- Toolbar combo: selecting a preset loads it directly and returns to the
Load preset…placeholder. - Combo is disabled with
No presets savedwhen the presets dir is empty, and becomes enabled as soon as the first preset is saved. - "Save current as new preset…" creates a visible new entry in the combo immediately.
- Manage dialog: rename / overwrite / delete each reflect in the toolbar combo contents without needing a restart.
- Overwrite and Delete both prompt for confirmation.
- Export / import. Deferred. If requested, we'd add a file-chooser pair that copies
.propertiesfiles to/from the presets dir, plus an optional "strip passwords" checkbox. Not in this PR. - CLI preset selection. Deferred. If requested, add a
--preset <name>flag that loads a preset's keys before applying other CLI options. Out of scope for 3.0.0. - Per-preset log/state split. Strict XDG purists might want logs under
$XDG_STATE_HOME/jsignpdf/and caches under$XDG_CACHE_HOME/jsignpdf/. Not tackled here; the single<cfg>dir is enough for 3.0.0.