Skip to content

Commit a36f1de

Browse files
authored
Add named signature-setting presets (+ XDG-aware config location) (#360)
1 parent 7d7d6b9 commit a36f1de

26 files changed

Lines changed: 2623 additions & 277 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ target/
2222
jsignpdf/conf/
2323
dependency-reduced-pom.xml
2424
guide.md
25+
distribution/demo/service-agreeement_signed.pdf

design-doc/3.0.0-presets-feature.md

Lines changed: 258 additions & 0 deletions
Large diffs are not rendered by default.

distribution/doc/release-notes/3.0.0.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ packaging.
2222
installers and the Flatpak bundle ship their own Java 21 runtime, so
2323
they are unaffected; users of the platform-independent
2424
`jsignpdf-<VERSION>.zip` now need a Java 21 (or newer) JRE on `PATH`.
25+
- **Named signature-setting presets** (#360) — save an arbitrary number
26+
of signing configurations under custom names and switch between them
27+
from a toolbar combo or the new **Presets** menu (save as new, rename,
28+
overwrite, delete). If _Store passwords_ is enabled when a preset is
29+
saved, the preset also captures the signing passwords encrypted with
30+
the same machine-local seed used for the main config; the encrypted
31+
values are silently ignored when a preset file is loaded on a
32+
different user account, and disabling _Store passwords_ before saving
33+
omits the password keys entirely (recommended before sharing a preset
34+
file).
35+
- **Configuration has moved** to a platform-appropriate XDG-aware
36+
location (`$XDG_CONFIG_HOME/jsignpdf/` on Linux, `%APPDATA%\JSignPdf\`
37+
on Windows, `~/Library/Application Support/JSignPdf/` on macOS);
38+
existing `~/.JSignPdf` settings are migrated automatically on first
39+
run. Override with `JSIGNPDF_CONFIG_DIR` for portable installs.
2540

2641
## Other changes
2742

jsignpdf/src/main/java/net/sf/jsignpdf/BasicSignerOptions.java

Lines changed: 212 additions & 150 deletions
Large diffs are not rendered by default.

jsignpdf/src/main/java/net/sf/jsignpdf/SignPdfForm.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import net.sf.jsignpdf.utils.KeyStoreUtils;
6161
import net.sf.jsignpdf.utils.PKCS11Utils;
6262
import net.sf.jsignpdf.utils.PropertyProvider;
63+
import net.sf.jsignpdf.utils.PropertyStoreFactory;
6364

6465
import org.apache.commons.lang3.StringUtils;
6566

@@ -74,7 +75,7 @@ public class SignPdfForm extends javax.swing.JFrame implements SignResultListene
7475

7576
private SignerFileChooser fileChooser = new SignerFileChooser();
7677

77-
protected final PropertyProvider props = PropertyProvider.getInstance();
78+
protected final PropertyProvider props = PropertyStoreFactory.getInstance().mainConfig();
7879

7980
private boolean autoclose = false;
8081
private BasicSignerOptions options;
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package net.sf.jsignpdf.fx.preset;
2+
3+
import static net.sf.jsignpdf.Constants.RES;
4+
5+
import java.text.DateFormat;
6+
import java.text.MessageFormat;
7+
import java.time.Instant;
8+
import java.util.Optional;
9+
10+
import javafx.beans.property.ReadOnlyObjectWrapper;
11+
import javafx.beans.property.ReadOnlyStringWrapper;
12+
import javafx.geometry.Insets;
13+
import javafx.scene.control.Alert;
14+
import javafx.scene.control.ButtonType;
15+
import javafx.scene.control.Dialog;
16+
import javafx.scene.control.Label;
17+
import javafx.scene.control.TableCell;
18+
import javafx.scene.control.TableColumn;
19+
import javafx.scene.control.TableView;
20+
import javafx.scene.control.TextInputDialog;
21+
import javafx.scene.layout.HBox;
22+
import javafx.stage.Stage;
23+
import javafx.util.Callback;
24+
import net.sf.jsignpdf.BasicSignerOptions;
25+
26+
/**
27+
* Dialog that lists all presets and offers per-row rename / overwrite / delete actions.
28+
*/
29+
public class ManagePresetsDialog extends Dialog<Void> {
30+
31+
private final PresetManager manager;
32+
private final BasicSignerOptions options;
33+
34+
public ManagePresetsDialog(PresetManager manager, BasicSignerOptions options, Stage owner) {
35+
this.manager = manager;
36+
this.options = options;
37+
38+
setTitle(RES.get("jfx.gui.preset.manage.title"));
39+
setHeaderText(null);
40+
if (owner != null) {
41+
initOwner(owner);
42+
}
43+
44+
TableView<Preset> table = buildTable();
45+
table.setPrefSize(620, 320);
46+
table.setPlaceholder(new Label(RES.get("jfx.gui.preset.manage.empty")));
47+
48+
HBox content = new HBox(table);
49+
content.setPadding(new Insets(10));
50+
getDialogPane().setContent(content);
51+
getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
52+
((javafx.scene.control.Button) getDialogPane().lookupButton(ButtonType.CLOSE))
53+
.setText(RES.get("jfx.gui.preset.manage.button.close"));
54+
setResultConverter(bt -> null);
55+
}
56+
57+
private TableView<Preset> buildTable() {
58+
TableView<Preset> table = new TableView<>(manager.getPresets());
59+
60+
TableColumn<Preset, String> nameCol = new TableColumn<>(RES.get("jfx.gui.preset.manage.column.name"));
61+
nameCol.setCellValueFactory(cd -> new ReadOnlyStringWrapper(cd.getValue().getDisplayName()));
62+
nameCol.setPrefWidth(220);
63+
64+
TableColumn<Preset, Instant> createdCol = new TableColumn<>(RES.get("jfx.gui.preset.manage.column.created"));
65+
createdCol.setCellValueFactory(cd -> new ReadOnlyObjectWrapper<>(cd.getValue().getCreatedAt()));
66+
createdCol.setCellFactory(col -> new TableCell<>() {
67+
private final DateFormat fmt = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
68+
69+
@Override
70+
protected void updateItem(Instant item, boolean empty) {
71+
super.updateItem(item, empty);
72+
setText((empty || item == null) ? "" : fmt.format(java.util.Date.from(item)));
73+
}
74+
});
75+
createdCol.setPrefWidth(150);
76+
77+
TableColumn<Preset, Void> actionsCol = new TableColumn<>(RES.get("jfx.gui.preset.manage.column.actions"));
78+
actionsCol.setCellFactory(buildActionsCellFactory());
79+
actionsCol.setPrefWidth(230);
80+
actionsCol.setSortable(false);
81+
actionsCol.setReorderable(false);
82+
83+
table.getColumns().add(nameCol);
84+
table.getColumns().add(createdCol);
85+
table.getColumns().add(actionsCol);
86+
return table;
87+
}
88+
89+
private Callback<TableColumn<Preset, Void>, TableCell<Preset, Void>> buildActionsCellFactory() {
90+
return column -> new TableCell<>() {
91+
private final javafx.scene.control.Button renameBtn =
92+
new javafx.scene.control.Button(RES.get("jfx.gui.preset.manage.button.rename"));
93+
private final javafx.scene.control.Button overwriteBtn =
94+
new javafx.scene.control.Button(RES.get("jfx.gui.preset.manage.button.overwrite"));
95+
private final javafx.scene.control.Button deleteBtn =
96+
new javafx.scene.control.Button(RES.get("jfx.gui.preset.manage.button.delete"));
97+
private final HBox box = new HBox(6, renameBtn, overwriteBtn, deleteBtn);
98+
99+
{
100+
renameBtn.setOnAction(e -> onRename(getTableRow().getItem()));
101+
overwriteBtn.setOnAction(e -> onOverwrite(getTableRow().getItem()));
102+
deleteBtn.setOnAction(e -> onDelete(getTableRow().getItem()));
103+
}
104+
105+
@Override
106+
protected void updateItem(Void item, boolean empty) {
107+
super.updateItem(item, empty);
108+
setGraphic((empty || getTableRow() == null || getTableRow().getItem() == null) ? null : box);
109+
}
110+
};
111+
}
112+
113+
private void onRename(Preset preset) {
114+
if (preset == null) {
115+
return;
116+
}
117+
TextInputDialog dlg = new TextInputDialog(preset.getDisplayName());
118+
dlg.setTitle(RES.get("jfx.gui.preset.dialog.rename.title"));
119+
dlg.setHeaderText(RES.get("jfx.gui.preset.dialog.rename.header"));
120+
dlg.setContentText(RES.get("jfx.gui.preset.dialog.rename.prompt"));
121+
dlg.initOwner(getDialogPane().getScene().getWindow());
122+
123+
while (true) {
124+
Optional<String> result = dlg.showAndWait();
125+
if (result.isEmpty()) {
126+
return;
127+
}
128+
String name = result.get();
129+
PresetValidation.Result validation = PresetValidation.validate(name,
130+
n -> manager.hasDisplayName(n, preset));
131+
if (validation != PresetValidation.Result.OK) {
132+
showAlert(Alert.AlertType.ERROR,
133+
RES.get("jfx.gui.preset.dialog.rename.title"),
134+
validationMessage(validation));
135+
dlg.getEditor().setText(PresetValidation.trim(name));
136+
continue;
137+
}
138+
manager.rename(preset, PresetValidation.trim(name));
139+
return;
140+
}
141+
}
142+
143+
private void onOverwrite(Preset preset) {
144+
if (preset == null) {
145+
return;
146+
}
147+
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
148+
confirm.setTitle(RES.get("jfx.gui.preset.dialog.overwrite.title"));
149+
confirm.setHeaderText(null);
150+
confirm.setContentText(MessageFormat.format(
151+
RES.get("jfx.gui.preset.dialog.overwrite.confirm"), preset.getDisplayName()));
152+
confirm.initOwner(getDialogPane().getScene().getWindow());
153+
Optional<ButtonType> result = confirm.showAndWait();
154+
if (result.isPresent() && result.get() == ButtonType.OK) {
155+
manager.overwrite(preset, options);
156+
}
157+
}
158+
159+
private void onDelete(Preset preset) {
160+
if (preset == null) {
161+
return;
162+
}
163+
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
164+
confirm.setTitle(RES.get("jfx.gui.preset.dialog.delete.title"));
165+
confirm.setHeaderText(null);
166+
confirm.setContentText(MessageFormat.format(
167+
RES.get("jfx.gui.preset.dialog.delete.confirm"), preset.getDisplayName()));
168+
confirm.initOwner(getDialogPane().getScene().getWindow());
169+
Optional<ButtonType> result = confirm.showAndWait();
170+
if (result.isPresent() && result.get() == ButtonType.OK) {
171+
manager.delete(preset);
172+
}
173+
}
174+
175+
private String validationMessage(PresetValidation.Result result) {
176+
switch (result) {
177+
case EMPTY: return RES.get("jfx.gui.preset.validation.empty");
178+
case ILLEGAL_CHAR: return RES.get("jfx.gui.preset.validation.illegalChar");
179+
case TOO_LONG: return RES.get("jfx.gui.preset.validation.tooLong");
180+
case DUPLICATE: return RES.get("jfx.gui.preset.validation.duplicate");
181+
default: return "";
182+
}
183+
}
184+
185+
private void showAlert(Alert.AlertType type, String title, String message) {
186+
Alert alert = new Alert(type);
187+
alert.setTitle(title);
188+
alert.setHeaderText(null);
189+
alert.setContentText(message);
190+
alert.initOwner(getDialogPane().getScene().getWindow());
191+
alert.showAndWait();
192+
}
193+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package net.sf.jsignpdf.fx.preset;
2+
3+
import java.time.Instant;
4+
import java.util.Objects;
5+
6+
/**
7+
* A named signature-setting preset. Identity is the backing filename; {@code displayName} is the user-visible label stored
8+
* inside the preset file as {@code preset.displayName}. {@code createdAt} is optional and informational.
9+
*/
10+
public final class Preset {
11+
12+
private final String filename;
13+
private final String displayName;
14+
private final Instant createdAt;
15+
16+
public Preset(String filename, String displayName, Instant createdAt) {
17+
this.filename = Objects.requireNonNull(filename, "filename");
18+
this.displayName = Objects.requireNonNull(displayName, "displayName");
19+
this.createdAt = createdAt;
20+
}
21+
22+
public String getFilename() {
23+
return filename;
24+
}
25+
26+
public String getDisplayName() {
27+
return displayName;
28+
}
29+
30+
public Instant getCreatedAt() {
31+
return createdAt;
32+
}
33+
34+
/**
35+
* Returns a copy with a new display name. Filename and createdAt are preserved.
36+
*/
37+
public Preset withDisplayName(String newDisplayName) {
38+
return new Preset(filename, newDisplayName, createdAt);
39+
}
40+
41+
@Override
42+
public String toString() {
43+
return displayName;
44+
}
45+
46+
@Override
47+
public boolean equals(Object o) {
48+
if (this == o) {
49+
return true;
50+
}
51+
if (!(o instanceof Preset)) {
52+
return false;
53+
}
54+
Preset other = (Preset) o;
55+
return filename.equals(other.filename);
56+
}
57+
58+
@Override
59+
public int hashCode() {
60+
return filename.hashCode();
61+
}
62+
}

0 commit comments

Comments
 (0)