I have defined a class InfoAction implements java.swing.Action
that needs to retrieve some information from its calling context in order to execute itself. For this I have defined an interface for providing those information:
public interface InfoProvider {
Info getInfo();
}
The actionPerformed
in the InfoAction class looks like
public void actionPerformed(ActionEvent e) {
Info info=e.getSource().getInfo();
// ... treat Info ...
}
Then I want to create various Swing objects using these actions, every Swing object providing its own context to the action.
InfoAction infoAction = MyApp.getInstance().getAction(MyApp.RENAME_ACTION); // Retrieving one action
JButton button = new JButton(infoAction); // TODO: inject the InfoProvider interface
JMenuItem menuItem = new JMenuItem(infoAction); // TODO: inject the InfoProvider interface
I'm stuck at how to add to those swing objects the InfoProvider interface.
Naively I tried to write
InfoAction infoAction = MyApp.getInstance().getAction(MyApp.RENAME_ACTION);
JButton button = new JButton(infoAction) implements InfoProvider {
Info getInfo() {
return MyGui.this.getInfo();
}
};
JMenuItem menuItem = new JMenuItem(infoAction) implements InfoProvider {
Info getInfo() {
return MyGui.this.getInfo();
}
};
But this is not a valid code.
I'd like to avoid creating sub-classes of all the Swing pieces such as
private class InfoButton extends JButton implements InfoProvider {...}
, private class InfoMenuItem extends JMenuItem implements InfoProvider {...}
.
So I wonder what are the other options to do this while keeping a clean code ?
Would working with reflection a good option ? Getting rid of the interface and testing if the actionEvent.getSource() has a getInfo() method ?
PS: I'm working with Java8
CodePudding user response:
You don’t need to specialize every component supporting actions (or timer or other sources of action events). Instead, create one specialized Action
implementation to mediate between the component/event source and the actual action. E.g.
public class InfoProviderAction implements Action, InfoProvider {
final PropertyChangeSupport listeners = new PropertyChangeSupport(this);
final PropertyChangeListener relay = ev -> listeners.firePropertyChange(
ev.getPropertyName(), ev.getOldValue(), ev.getNewValue());
final Action target;
final InfoProvider actualProvider;
public InfoProviderAction(Action target, InfoProvider actualProvider) {
this.target = target;
this.actualProvider = actualProvider;
}
@Override
public void actionPerformed(ActionEvent e) {
target.actionPerformed(new ActionEvent(
this, e.getID(), e.getActionCommand(), e.getWhen(), e.getModifiers()));
}
@Override
public Info getInfo() {
return actualProvider.getInfo();
}
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
boolean hadListeners = listeners.hasListeners(null);
listeners.addPropertyChangeListener(listener);
if(!hadListeners) target.addPropertyChangeListener(relay);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
boolean hadListeners = listeners.hasListeners(null);
listeners.removePropertyChangeListener(listener);
if(hadListeners && !listeners.hasListeners(null))
target.removePropertyChangeListener(relay);
}
@Override
public boolean isEnabled() {
return target.isEnabled();
}
@Override
public void setEnabled(boolean b) {
target.setEnabled(b);
}
@Override
public Object getValue(String key) {
return target.getValue(key);
}
@Override
public void putValue(String key, Object value) {
target.putValue(key, value);
}
}
Then, your MyGui
class can use it like
InfoAction infoAction = MyApp.getInstance().getAction(MyApp.RENAME_ACTION);
infoAction = new InfoProviderAction(infoAction, this::getInfo);// inject the InfoProvider
JButton button = new JButton(infoAction);
JMenuItem menuItem = new JMenuItem(infoAction);
The component will see the InfoProviderAction
as its action, which will exhibit the same properties and behavior as the wrapped action. Whereas the actual action will see the InfoProviderAction
as the even source, implementing the InfoProvider
interface as expected.
Problems could arise if the action expects the source to be a component. But actions should not assume this, as it would cause problems with Timer
or global key bindings as event sources.
CodePudding user response:
There is something you can use with Swing:
When you build your object, you can configure it:
class ComponentFactory {
public static <T extends JComponent> T configure(T component) {
// the MyGui.this won't compile here, but it is only to explain
// what you can do with your current code.
component.putClientProperty(InfoProvider.class, MyGui.this);
}
}
And build object as in:
ComponentFactory.configure(new JButton(infoAction));
Or create a static method for it in ComponentFactory
:
ComponentFactory.createButton(infoAction);
In your action, you can then read the property:
public void actionPerformed(ActionEvent e) {
Object src = e.getSource();
if (src instanceof JComponent) {
JComponent c = (JComponent) src;
Info info = (Info) c.getClientProperty(InfoProvider.class);
...
}
}
Notes:
Since it is Swing, there is a some chance it requires to be Serializable
- which I don't think is a problem in 2022.
And that the doc for putClientProperty
says:
The clientProperty dictionary is not intended to support large scale extensions to JComponent nor should be it considered an alternative to subclassing when designing a new component.
It should be fine for one property, otherwise you would have to use subclassing and implements InfoProvider for each such component.