When you open a bean or select Properties... from its context menu in the Able Agent Editor, the customizer class in the BeanInfo file is instantiated and passed a reference to the bean. Generally a customizer must be a subclass of the awt Component class. In the Able environment, which is based on the Swing GUI library, we support customizers that extend JPanel, JDialog, and JFrame.
Simple beans will typically extend JPanel or one of the provided customizers such as AbleObjectCustomizer or AbleDefaultAgentCustomizer. More complex beans may require complex dialogs or even full-blown editor windows with menu bars and icon palettes and extend JDialog or JFrame.The type of customizer you provide depends on the data content of your bean, and the amount of inherited information you wish to surface.
All customizers must implement the AbleCustomizer interface which defines the methods used to pass the bean reference. The customizer uses that reference to get the bean data necessary to display the properties panel. It then reads the panel and uses that reference to set the data on the bean. There is also an optional AbleCustomizerPanel interface which defines methods used to determine the file to display when the Help button is pressed. The customary name of a bean customizer file is the term Customizer suffixed to the bean class name.
The simplest customizer would display only the member variables associated with your bean and open an appropriate help panel when the help button is pressed. A simple implementation is to extend a JPanel and implement the AbleCustomizer and AbleCustomizerPanel interfaces. The SimpleAbleBeanCustomizer is an example of such a customizer. Though it extends AbleObject, none of the member variables inherited from AbleObject can be displayed or changed. When instantiated in the Agent Editor, the SimpleAbleBeanCustomizer is added to a dialog that includes OK, Cancel, and Help buttons. It is described in detail later.
A more complex customizer would allow view and edit of inherited properties. The SimpleAbleBeanCustomizer, for instance, provides no means to edit the bean name, which is a data value of AbleObject. Though a name field could be added to the panel, since SimpleAbleBean extends AbleObject its customizer could extend the AbleObjectCustomizer and surface all the displayable values it inherits from AbleObject. With this style customizer you provide a panel which is inserted into a set of tabbed panels. The inherited properties such as the bean name are displayed on one or more other panels. Your customizer specifies the tab location for your panel, and you can provide as many panels as you wish to organize, display and edit your bean's data. The SimpleAbleAgentCustomizer extends the AbleDefaultAgentCustomizer and is an example of this style. Note that the tabs labelled General and Connections are provided by the AbleObjectCustomizer, and the Functions tab by the AbleDefaultAgentCustomizer.
With a customizer that includes property tab panels, its possible that there are interactions between the panels. For instance, an action such as pressing a button on one tab may change a value on another tab. Thus there is a need for panels to listen for property changes and refresh their views, and possibly refresh all the panels from the bean data. The AbleFileWatcherCustomizer is an example of this style customizer. It includes a button that calls the bean init method which changes the bean state, a property it inherits from AbleObject. The AbleFileWatcherCustomizer is described in detail later.
You can also create your own unique customizers. In Able for example the Filter Editor, Fuzzy Rule Editor, and Boolean Rule Editor each provide their own customizers that extend JFrame. These appear to be applications rather than a simple property editor. Any customizer that is an instance of JFrame, JDialog, or JPanel can be opened in the Agent Editor.
The simplest of customizers displays its member data, saves changes, and displays help. The next section describes the implementation of the SimpleAbleBeanCustomizer which extends JPanel. Note that the customizer provides only the label, text field, and reset button on the panel shown:
Member Variables
Member data consists of a reference to the bean object itself so that it can be updated when OK is pressed, the text field to contain the color value, and a change support variable used by methods required by the Customizer interface.
private SimpleAbleBean myBean = null; private JTextField myTextField = null; private PropertyChangeSupport myChgSupport = null;Constructors
A default constructor creates and positions the screen components.
public SimpleAbleBeanCustomizer() { super(); initializePanel(); }private void initializePanel() { this.setLayout(new BorderLayout(10,10)); this.setPreferredSize(new Dimension(200, 150)); JPanel upperPanel = new JPanel(); upperPanel.add(new JLabel("Color:")); myTextField = new JTextField(10); upperPanel.add(myTextField); this.add(upperPanel, BorderLayout.NORTH); JButton resetButton = new JButton("Reset"); resetButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ResetButton_actionPerformed(e); }}); resetButton.setToolTipText("Reset the simple bean color"); this.add(resetButton, BorderLayout.SOUTH); }Main
A main method is not necessary but is useful during development. In this case it illustrates how the customizer panel is created and called by the Able Agent Editor.
static public void main(String args[]){ try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); SimpleAbleBean bean = new SimpleAbleBean(); SimpleAbleBeanCustomizer customizer = new SimpleAbleBeanCustomizer(); customizer.setObject(bean); new AbleCustomizerDialog(new JFrame(), customizer, bean); } catch (Exception e) { JOptionPane.showMessageDialog( null, e.toString(), "SimpleAbleBeanCustomizer()", JOptionPane.ERROR_MESSAGE); } }AbleCustomizer Implementation
Before the panel is displayed, the data must be retrieved from the target bean and passed to the screen components that will display it. Now that we have a reference to the bean, we also create a change support object should other programs wish to register with the customizer. The setObject method performs this function. If the bean is a remote object, this method would need to throw a RemoteException.
public void setObject(Object theObject) { if (myChgSupport == null) myChgSupport = new PropertyChangeSupport(theObject); myBean = (SimpleAbleBean) theObject; myTextField.setText(myBean.getSimpleBeanColor()); }When the OK button is pressed, data must be retrieved from the screen components and passed back into the target bean. The updateObject method performs this function. Since beans in general can be remote objects, we throw RemoteExceptions to signal invalid panel input.
public void updateObject() throws RemoteException { try { String color = myTextField.getText().trim(); if (color.length() == 0) throw new RemoteException("Blank input provided on Simple Bean panel."); myBean.setSimpleBeanColor(color); } catch (NullPointerException e) { throw new RemoteException("Error on Simple Bean panel.\n" + e.toString()); } }We provide methods in the unlikely event that other programs wish to register or deregister with the customizer.
public void addPropertyChangeListener(PropertyChangeListener l) { myChgSupport.addPropertyChangeListener(l); } public void removePropertyChangeListener(PropertyChangeListener l) { myChgSupport.removePropertyChangeListener(l); }AbleCustomizerPanel Implementation
Two methods are required by the AbleCustomizerPanel interface to specify the html source file to open when the Help button is pressed.
public String getHelpFileName() { return AbleHelp.helpFilesPath + "examples/ablebean/doc-files/SimpleAbleBeanHelp.html" ; } public URL getHelpContext() { return Able.HomeUrl; }
In the AbleFileWatcher customizer, we extend the AbleObjectCustomizer, and illustrate tab-to-tab interaction. Since the AbleFileWatcher bean extends AbleObject, the customizer extends AbleObjectCustomizer. Since there is more member data there are more panel components in this bean. Thus we elect to implement the panel as a separate class called AbleFileWatcherPanel.java. The most interesting aspect of this customizer is the interaction between panels. When the Initialize button is pressed, bean values shown on the General tab change and their values must be updated. Our customizer and panel provides the File Watcher tab as shown:
Since the customizer extends AbleObjectCustomizer, it inherits the following capabilities:
- setObject and updateObject methods for data members displayed on the General and Connections tabs.
- An implementation that allows other programs to register and deregister for property changes.
- A working directory value retains the value of the last directory browsed. In this case we set it with the Browse... button so that it remembers the last directory browsed.
- A property change manager that allows individual panels to register a property of interest and a method to be called when that property changes. This allows a panel to refresh itself or an individual data field when bean data changes.
- Methods to enable/disable notification, which are used when a bean method is called that will change many properties. The panel can then force the customizer to update all the tabs instead of relying on property changes which may refresh the same fields several times.
- A window adapter that removes property change listeners when the window closes.
Since all the panel components are now in a separate class, the customizer does little but provide the setObject and updateObject methods.
AbleFileWatcherCustomizer Member Variables
We keep a reference to the panel which is used in the setObject and updateObject methods.
private AbleFileWatcherPanel myBeanPanel = null;AbleFileWatcherCustomizer Constructors
The default constructor provides a new JFrame as shown:
public AbleFileWatcherCustomizer() {this(new JFrame(), "File Watcher Customizer") ;}The Agent Editor passes in its frame when it constructs a customizer:
public AbleFileWatcherCustomizer(Frame theFrame, String theTitle) { super(theFrame, theTitle, false); // non-modal setCustomizer(this); myBeanPanel = new AbleFileWatcherPanel(this); insertTab("File Watcher", myBeanPanel, 1); }We provide a reference to the base customizer so that it can update all the data on all the tabs - equivalent to pressing OK but without closing the customizer. The constructor then creates the panel and gives it a reference to the customizer as well so that the panel can register for property changes with the base customizer. It then inserts the panel into the list of tabbed panels at position 1, after the General tab located at position 0.
AbleFileWatcherCustomizer Main
A main method is not necessary but is useful during development. In this case it illustrates how the customizer panel is created and called by the Able Agent Editor.
static public void main(String args[]){ try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); AbleFileWatcherCustomizer c = new AbleFileWatcherCustomizer(); c.setObject(new AbleFileWatcher()); c.setVisible(true); } catch (Exception e) { e.printStackTrace(); JOptionPane.showMessageDialog(null, e.toString(), "AbleFileWatcherCustomizer()", JOptionPane.ERROR_MESSAGE); } }AbleCustomizer Implementation
Since this customizer extends AbleObjectCustomizer, we override the setObject and updateObject methods to work with the components on the File Watcher tab. We must call the base object methods to set and update the other tabs.
public void setObject(Object theBean) { super.setObject(theBean); try { myBeanPanel.getDataFromBean((AbleFileWatcher) theBean); } catch (RemoteException e) { JOptionPane.showMessageDialog(this, e.toString(), "AbleFileWatcherCustomizer Error: setObject()", JOptionPane.ERROR_MESSAGE); } }public void updateObject() throws RemoteException { super.updateObject(); myBeanPanel.setDataOnBean(); }
AbleFileWatcherPanel Member Variables
The AbleFileWatcherPanel is responsible for displaying the File Watcher tab and updating the bean with data from that tab. It keeps a reference to the bean and to the customizer so it can register for property changes and get or set the latest working directory. It includes panel components for each bean member variable to display data that can change or be edited such as radio buttons and text fields. It also has a label for file size since that changes depending on the file specified.
private AbleFileWatcher myFileWatcher = null; private AbleFileWatcherCustomizer myCustomizer = null; private String myFileName = null; private JTextField myFileNameTextField = null; private JRadioButton myConditionModifiedRadioButton = null; private JRadioButton myConditionDeletedRadioButton = null; private JRadioButton myConditionThresholdRadioButton = null; private JTextField myThresholdTextField = null; private JLabel myFileSizeLabel = null; private JRadioButton myActionNoneRadioButton = null; private JRadioButton myActionAlertRadioButton = null; private JRadioButton myActionExecuteRadioButton = null; private JTextField myCommandTextField = null; private JRadioButton myActionNotifyRadioButton = null; private JButton myInitializeButton = null;AbleFileWatcherPanel Constructors
We require a reference to the customizer so we allow only a constructor that takes it as a parameter. We create and position all the panel components with typical Swing techniques in the initializePanel method so they are not displayed here.
private AbleFileWatcherPanel() {}public AbleFileWatcherPanel(AbleFileWatcherCustomizer theCustomizer) { super(); myCustomizer = theCustomizer; initializePanel(); addPropertyListeners(); }In addPropertyListeners, we arbitrarily decide to refresh the panel should another process change the bean's AbleEventProcessingEnabled value. This would mean that processing options expected to be set in the bean's initialize method have been set to another value. We might wish to implement a Reset button which could be enabled anytime properties differ from the values set in the init method.
private void addPropertyListeners() { myCustomizer.addPropertyChangeMethod(AbleProperty.AbleEventProcessingEnabled, this, "refreshPanel"); }We could also register for AbleProperty.UpdateProperties if there were data on our panel that could be changed either directly or as a side-effect of an action taken on another panel. This event is fired when the customizer's updateProperties method is called, typically done to force all the screen data to be set in the bean before another bean method is called. The listener is removed by the AbleObjectCustomizer when its frame is closed.
AbleFileWatcherPanel get and set data
The methods called from the customizer's setObject and updateObject pass data between the screen and the bean. These are the getDataFromBean and setDataOnBean methods.
AbleCustomizerPanel Implementation
Since the panel information is separated from the customizer, the panel provides the implementation for the required AbleCustomizerPanel interfaces as shown:
public String getHelpFileName() { return AbleHelp.helpFilesPath + "examples/ablebean/doc-files/AbleFileWatcherHelp.html" ; }public URL getHelpContext() { return Able.HomeUrl; }
When the Initialize button on the tab is pressed, the InitializeButton_actionPerformed method executes.
private void InitializeButton_actionPerformed(ActionEvent e) { try { myCustomizer.updateProperties(); myCustomizer.setIgnoreAnyPropertyChanges(true); myFileWatcher.init() ; myCustomizer.setIgnoreAnyPropertyChanges(false); myCustomizer.setObject(myFileWatcher); // refresh from underlying object } catch (RemoteException exc) { JOptionPane.showMessageDialog( this, exc.toString(), "AbleFileWatcherPanel Error: InitializeButton()", JOptionPane.ERROR_MESSAGE); } }First, it calls updateProperties which causes all the tab panels to be read. This method will temporarily remove all property listeners on the bean and fire a single AbleProperty.UpdateProperties event.
Next we turn off all property change notifications temporarily, and issue the init on the bean. This updates AbleObject data which is displayed on the General tab as it includes this logic:
setTimerEventProcessingEnabled(true); setAbleEventProcessingEnabled(Able.ProcessingDisabled_PostingDisabled); startEnabledEventProcessing();Now, to refresh the data displayed on the general tab, execute the setObject customizer which causes all the customizers setObject methods to be run which repopulates all the tabs. Also, re-enable the property change notifications.
As an alternate implementation, we could require the screens to register for property changes for each field of interest. While this is feasible in this case, implementing this design for every field is onerous since it requires accessor methods for each member variable. We have implemented it for the fields on the General tab, and registered for changes with the following logic:
private void addPropertyListeners() { myCustomizer.addPropertyChangeMethod(AbleProperty.State, this, "setStateTextField"); myCustomizer.addPropertyChangeMethod(AbleProperty.Name, this, "setNameTextField"); myCustomizer.addPropertyChangeMethod(AbleProperty.Comment, this, "setCommentTextArea"); myCustomizer.addPropertyChangeMethod(AbleProperty.DataFlowEnabled, this, "setDataFlowEnabledCheckBox");myCustomizer.addPropertyChangeMethod(AbleProperty.TimerEventProcessingEnabled, this, "setTimerEventProcessingEnabledCheckBox"); myCustomizer.addPropertyChangeMethod(AbleProperty.SleepTime, this, "setSleepTimeTextField"); myCustomizer.addPropertyChangeMethod(AbleProperty.AbleEventProcessingEnabled, this, "setAbleEventProcessingCheckBoxes"); }The alternate implementation for the action of the Initialize button would be as follows:
private void InitializeButton_actionPerformed(ActionEvent e) { try { myCustomizer.updateProperties(); myFileWatcher.init() ; } catch (RemoteException exc) { JOptionPane.showMessageDialog( this, exc.toString(), "AbleFileWatcherPanel Error: InitializeButton()", JOptionPane.ERROR_MESSAGE); } }In the alternate implementation, we would not register for AbleEventProcessingEnabled property changes in the addPropertyListeners method in AbleFileWatcherPanel unless there were updates to the panel implemented. In this case the setButtonStates method could be called though it would need to be given public scope.
We believe the alternate implementation to be preferable for remote objects since it will result in fewer accesses of the remote object. Since local objects are the focus of Able at this release level, the overhead and time to produce a separate method to update each field on the panel provides little payback. We have implemented accessor methods ourselves only for the fields on the General tab at this time.