Skip to content

Commit 19f19b2

Browse files
committed
Improve input validation and visual feedback
1 parent 938a4b6 commit 19f19b2

File tree

5 files changed

+166
-5
lines changed

5 files changed

+166
-5
lines changed

pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@
112112

113113
<jdatepicker.version>1.3.2</jdatepicker.version>
114114
<object-inspector.version>0.1</object-inspector.version>
115+
116+
<scijava-common.version>2.100.0</scijava-common.version>
115117
</properties>
116118

117119
<repositories>

src/main/java/org/scijava/ui/swing/SwingDialog.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@
3737
import java.awt.event.WindowAdapter;
3838
import java.awt.event.WindowEvent;
3939

40+
import javax.swing.JButton;
4041
import javax.swing.JDialog;
4142
import javax.swing.JOptionPane;
4243
import javax.swing.JPanel;
4344
import javax.swing.JScrollPane;
45+
import javax.swing.UIManager;
4446
import javax.swing.WindowConstants;
4547
import javax.swing.border.Border;
4648
import javax.swing.border.EmptyBorder;
@@ -292,6 +294,17 @@ public boolean isVisible() {
292294
return optionPane.isVisible();
293295
}
294296

297+
/**
298+
* Gets the OK button of this dialog.
299+
*
300+
* @return the OK button, or {@code null} if not found (e.g., because the
301+
* dialog does not have an OK button).
302+
*/
303+
public JButton getOkButton() {
304+
final String okText = UIManager.getString("OptionPane.okButtonText");
305+
return findButton(buttons, okText);
306+
}
307+
295308
// -- Helper methods --
296309

297310
private void rebuildPane(final boolean scrollBars) {
@@ -339,6 +352,20 @@ private void rebuildPane(final boolean scrollBars) {
339352
}
340353
}
341354

355+
/** Recursively searches a component tree for a {@link JButton} by label. */
356+
private JButton findButton(final Component c, final String text) {
357+
if (c instanceof JButton && text.equals(((JButton) c).getText())) {
358+
return (JButton) c;
359+
}
360+
if (c instanceof Container) {
361+
for (final Component child : ((Container) c).getComponents()) {
362+
final JButton found = findButton(child, text);
363+
if (found != null) return found;
364+
}
365+
}
366+
return null;
367+
}
368+
342369
/**
343370
* Makes the given component grab the keyboard focus whenever the window gains
344371
* the focus.

src/main/java/org/scijava/ui/swing/widget/SwingInputHarvester.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@
2929

3030
package org.scijava.ui.swing.widget;
3131

32+
import java.util.ArrayList;
33+
import java.util.List;
34+
3235
import javax.swing.JOptionPane;
3336
import javax.swing.JPanel;
3437

38+
import org.scijava.module.ModuleCanceledException;
39+
import org.scijava.module.ModuleException;
40+
import org.scijava.module.ModuleItem;
3541
import org.scijava.module.Module;
3642
import org.scijava.module.process.PreprocessorPlugin;
3743
import org.scijava.plugin.Plugin;
@@ -40,6 +46,7 @@
4046
import org.scijava.ui.swing.SwingUI;
4147
import org.scijava.widget.InputHarvester;
4248
import org.scijava.widget.InputPanel;
49+
import org.scijava.widget.InputWidget;
4350

4451
/**
4552
* SwingInputHarvester is an {@link InputHarvester} that collects input
@@ -60,6 +67,53 @@ public SwingInputPanel createInputPanel() {
6067
return new SwingInputPanel();
6168
}
6269

70+
@Override
71+
public void harvest(final Module module) throws ModuleException {
72+
final InputPanel<JPanel, JPanel> inputPanel = createInputPanel();
73+
buildPanel(inputPanel, module);
74+
if (!inputPanel.hasWidgets()) return;
75+
76+
// Validate all inputs now so the dialog opens with accurate styling and
77+
// the OK button already disabled for any initially invalid values.
78+
for (final ModuleItem<?> item : module.getInfo().inputs()) {
79+
final InputWidget<?, ?> w = inputPanel.getWidget(item.getName());
80+
if (w != null) w.get().updateValidation();
81+
}
82+
inputPanel.refresh();
83+
84+
while (true) {
85+
// Show the dialog; bail out immediately if canceled.
86+
if (!harvestInputs(inputPanel, module)) throw new ModuleCanceledException();
87+
88+
// Validate all unresolved inputs and collect any error messages.
89+
final List<String> errors = new ArrayList<>();
90+
for (final ModuleItem<?> item : module.getInfo().inputs()) {
91+
if (module.isInputResolved(item.getName())) continue;
92+
final String message = item.validateMessage(module);
93+
if (message != null && !message.isEmpty()) {
94+
// Use the same label logic as DefaultWidgetModel.getWidgetLabel().
95+
String label = item.getLabel();
96+
if (label == null || label.isEmpty()) {
97+
final String name = item.getName();
98+
label = name.substring(0, 1).toUpperCase() + name.substring(1);
99+
}
100+
errors.add(label + ": " + message);
101+
}
102+
}
103+
104+
if (errors.isEmpty()) break; // all inputs valid; proceed
105+
106+
// Show a modal error dialog, then re-open the harvester dialog.
107+
JOptionPane.showMessageDialog(
108+
null,
109+
String.join("\n", errors),
110+
"Invalid Input",
111+
JOptionPane.ERROR_MESSAGE);
112+
}
113+
114+
processResults(inputPanel, module);
115+
}
116+
63117
@Override
64118
public boolean harvestInputs(final InputPanel<JPanel, JPanel> inputPanel,
65119
final Module module)
@@ -83,8 +137,21 @@ public boolean harvestInputs(final InputPanel<JPanel, JPanel> inputPanel,
83137
new SwingDialog(pane, optionType, messageType, doScrollBars);
84138
dialog.setTitle(title);
85139
dialog.setModal(modal);
140+
141+
// Wire the OK button to the panel so validation can enable/disable it.
142+
if (inputPanel instanceof SwingInputPanel) {
143+
final SwingInputPanel swingPanel = (SwingInputPanel) inputPanel;
144+
swingPanel.setOkButton(dialog.getOkButton());
145+
swingPanel.updateOkButton();
146+
}
147+
86148
final int rval = dialog.show();
87149

150+
// Detach the OK button once the dialog closes.
151+
if (inputPanel instanceof SwingInputPanel) {
152+
((SwingInputPanel) inputPanel).setOkButton(null);
153+
}
154+
88155
// verify return value of dialog
89156
return rval == JOptionPane.OK_OPTION;
90157
}

src/main/java/org/scijava/ui/swing/widget/SwingInputPanel.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
package org.scijava.ui.swing.widget;
3131

32+
import javax.swing.JButton;
3233
import javax.swing.JLabel;
3334
import javax.swing.JPanel;
3435

@@ -48,6 +49,32 @@ public class SwingInputPanel extends AbstractInputPanel<JPanel, JPanel> {
4849

4950
private JPanel uiComponent;
5051

52+
/** OK button to enable/disable based on overall validation state, or null. */
53+
private JButton okButton;
54+
55+
// -- SwingInputPanel methods --
56+
57+
/**
58+
* Sets the OK button that should be enabled or disabled to reflect whether
59+
* all widget values are currently valid. Pass {@code null} to detach.
60+
*/
61+
void setOkButton(final JButton okButton) {
62+
this.okButton = okButton;
63+
}
64+
65+
/**
66+
* Enables the OK button if all widgets are valid, disables it otherwise.
67+
* Does nothing if no OK button has been set via {@link #setOkButton}.
68+
*/
69+
void updateOkButton() {
70+
if (okButton == null) return;
71+
final boolean allValid = widgets.values().stream().allMatch(w -> {
72+
final String msg = w.get().getValidationMessage();
73+
return msg == null || msg.isEmpty();
74+
});
75+
okButton.setEnabled(allValid);
76+
}
77+
5178
// -- InputPanel methods --
5279

5380
@Override
@@ -71,6 +98,12 @@ public void addWidget(final InputWidget<?, JPanel> widget) {
7198
}
7299
}
73100

101+
@Override
102+
public void refresh() {
103+
super.refresh(); // refreshWidget() on each widget, which applies validation styling
104+
updateOkButton();
105+
}
106+
74107
@Override
75108
public Class<JPanel> getWidgetComponentType() {
76109
return JPanel.class;

src/main/java/org/scijava/ui/swing/widget/SwingInputWidget.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,15 @@
2929

3030
package org.scijava.ui.swing.widget;
3131

32-
import javax.swing.JComponent;
33-
import javax.swing.JPanel;
32+
import java.awt.Color;
33+
import java.util.ArrayList;
34+
import java.util.HashSet;
35+
import java.util.List;
36+
import java.util.Set;
37+
38+
import javax.swing.*;
39+
import javax.swing.border.BevelBorder;
40+
import javax.swing.border.Border;
3441

3542
import net.miginfocom.swing.MigLayout;
3643

@@ -49,6 +56,7 @@ public abstract class SwingInputWidget<T> extends
4956
{
5057

5158
private JPanel uiComponent;
59+
private Set<JComponent> tooltipped = new HashSet<>();
5260

5361
// -- WrapperPlugin methods --
5462

@@ -61,6 +69,14 @@ public void set(final WidgetModel model) {
6169
uiComponent.setLayout(layout);
6270
}
6371

72+
// -- InputWidget methods --
73+
74+
@Override
75+
public void refreshWidget() {
76+
super.refreshWidget();
77+
refreshValidation();
78+
}
79+
6480
// -- UIComponent methods --
6581

6682
@Override
@@ -84,9 +100,25 @@ protected UserInterface ui() {
84100

85101
/** Assigns the model's description as the given component's tool tip. */
86102
protected void setToolTip(final JComponent c) {
87-
final String desc = get().getItem().getDescription();
88-
if (desc == null || desc.isEmpty()) return;
89-
c.setToolTipText(desc);
103+
tooltipped.add(c);
104+
}
105+
106+
/**
107+
* Updates the widget's border and tooltip to reflect the current validation
108+
* state. A red border and tooltip showing the error message are applied when
109+
* the model's validation message is non-null; both are cleared when valid.
110+
* <p>
111+
* Must be called on the Swing EDT.
112+
* </p>
113+
*/
114+
protected void refreshValidation() {
115+
final String message = get().getValidationMessage();
116+
final boolean valid = message == null || message.isEmpty();
117+
final JPanel p = getComponent();
118+
final Color borderColor = valid ? p.getBackground() : Color.RED;
119+
final String tip = valid ? get().getItem().getDescription() : message;
120+
p.setBorder(BorderFactory.createLineBorder(borderColor, 1));
121+
tooltipped.forEach(c -> c.setToolTipText(tip));
90122
}
91123

92124
}

0 commit comments

Comments
 (0)