Firing handler event from different widget in GWT - java

I got two ListBoxes, assume customerListBox and timeListBox, and Label resultLabel. When timeListBox's value is changed, it fires its ValueChangeHandler (let's call it recalculateValueHandler) which recalculates result and put it into resultLabel. I need this to work vice versa - when customerListBox's value is changed I need to recalculate new result for same time value but different customer.
So I'd need something like customerListBox.onValueChange(fire(recalculateValueHandler)), hope you do understand. Is there anything that could work for me this way? I try to avoid duplicating pretty much same code into both handlers.

You just need three things
declare all elements at top level so they are accessible across all methods
create a method that recalculates the value, let's call it recalculateValue
add ChangeHandlers for both ListBoxes (ListBox don't have ValueChangeHandler) that will call recalculateValue method.
Working example:
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.RootPanel;
public class Test implements EntryPoint {
// elements declared at top level are accessible across all methods
private ListBox customerListBox;
private ListBox timeListBox;
private Label resultLabel;
#Override
public void onModuleLoad() {
// add some data
customerListBox = new ListBox();
for(int i = 0; i < 10; i++)
customerListBox.addItem("Customer " + (i + 1));
// add some data
timeListBox = new ListBox();
for(int i = 0; i < 10; i++)
timeListBox.addItem("Time " + (i + 1));
resultLabel = new Label();
// recalculateValue when customerListBox changes
customerListBox.addChangeHandler(new ChangeHandler() {
#Override
public void onChange(ChangeEvent event) {
recalculateValue();
}
});
// recalculateValue when timeListBox changes
timeListBox.addChangeHandler(new ChangeHandler() {
#Override
public void onChange(ChangeEvent event) {
recalculateValue();
}
});
// initial result (optional)
recalculateValue();
// show elements
RootPanel.get().clear();
RootPanel.get().add(customerListBox);
RootPanel.get().add(timeListBox);
RootPanel.get().add(resultLabel);
}
private void recalculateValue() {
// use values from ListBoxes
resultLabel.setText(customerListBox.getSelectedValue() + " / " + timeListBox.getSelectedValue());
}
}
Notice that both handlers ares identical, so you can create only one handler and use it for both ListBoxes like that:
ChangeHandler handler = new ChangeHandler() {
#Override
public void onChange(ChangeEvent event) {
recalculateValue();
}
};
// recalculateValue when customerListBox changes
customerListBox.addChangeHandler(handler);
// recalculateValue when timeListBox changes
timeListBox.addChangeHandler(handler);

Related

Is there a way to change a Java property without firing a value changed event to it's listeners?

What I'm trying to do
I'm looking for a way to change a property, without a call to the listeners's changed method.
More specifically I'm trying to implement an undo/redo functionality. The way I've implemented it is as following, in an example with a BooleanProperty and a JavaFX CheckBox.
The selectedProperty of the CheckBox is changed by a mouse click.
A BooleanProperty (actually a JavaFX SimpleBooleanProperty) is changed because it is bound bidirectionally to the selectedProperty
The ChangeListener of the BooleanProperty registers this and adds a Command on the application's undoStack. The Command stores the property, the old and the new value.
The user clicks the undo button
Via the button the application takes that last Command from the stack and calls it's undo() method.
The undo() method changes the BooleanProperty back.
The ChangeListener registers this change again and creates a new Command
An endless cycle is created
My Hacky Solution
The way I did it is by passing the ChangeListener to the Command object. Then the undo() method first removes the ChangeListener, changes the BooleanProperty and then adds the ChangeListener again.
It feels wrong and hacky to pass the ChangeListener to the Command (in my actual implementation in the 3. step there are actually a few more classes between the ChangeListener and the Command which now all need to know about the ChangeListener)
My Question
Is this really the way to do it? Isn't there a way to change the property in step 6 and just tell it to not inform it's listeners? Or at least to get it's listeners?
There's no supported way of bypassing listeners, as you describe. You just need to build this logic into your undo/redo mechanism. The idea is basically to set a flag if you are performing an undo/redo, and not add the change to your stack if so.
Here's a very simple example: note this is not production quality - for example typing in a text control will add to the stack for every character change (keeping copies of the current text at each change). In real code, you should coalesce these changes together.
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
public class UndoManager {
private boolean performingUndoRedo = false ;
private Deque<Command<?>> undoStack = new LinkedList<>();
private Deque<Command<?>> redoStack = new LinkedList<>();
private Map<Property<?>, ChangeListener<?>> listeners = new HashMap<>();
public <T> void register(Property<T> property) {
// don't register properties multiple times:
if (listeners.containsKey(property)) {
return ;
}
// FIXME: should coalesce (some) changes on the same property, so, e.g. typing in a text
// control does not result in a separate command for each character
ChangeListener<? super T> listener = (obs, oldValue, newValue) -> {
if (! performingUndoRedo) {
Command<T> cmd = new Command<>(property, oldValue, newValue) ;
undoStack.addFirst(cmd);
}
};
property.addListener(listener);
listeners.put(property, listener);
}
public <T> void unregister(Property<T> property) {
listeners.remove(property);
}
public void undo() {
if (undoStack.isEmpty()) {
return ;
}
Command<?> command = undoStack.pop();
performingUndoRedo = true ;
command.undo();
redoStack.addFirst(command);
performingUndoRedo = false ;
}
public void redo() {
if (redoStack.isEmpty()) {
return ;
}
Command<?> command = redoStack.pop();
performingUndoRedo = true ;
command.redo();
undoStack.addFirst(command);
performingUndoRedo = false ;
}
private static class Command<T> {
private final Property<T> property ;
private final T oldValue ;
private final T newValue ;
public Command(Property<T> property, T oldValue, T newValue) {
super();
this.property = property;
this.oldValue = oldValue;
this.newValue = newValue;
}
private void undo() {
property.setValue(oldValue);
}
private void redo() {
property.setValue(newValue);
}
#Override
public String toString() {
return "property: "+property+", from: "+oldValue+", to: "+newValue ;
}
}
}
And here's a quick test harness:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class UndoExample extends Application {
#Override
public void start(Stage stage) throws Exception {
ComboBox<Color> textColor = new ComboBox<Color>();
textColor.getItems().addAll(Color.BLACK, Color.RED, Color.DARKGREEN, Color.BLUE);
textColor.setValue(Color.BLACK);
textColor.setCellFactory(lv -> new ColorCell());
textColor.setButtonCell(new ColorCell());
CheckBox italic = new CheckBox("Italic");
TextArea text = new TextArea();
updateStyle(text, textColor.getValue(), italic.isSelected());
ChangeListener<Object> listener = (obs, oldValue, newValue) ->
updateStyle(text, textColor.getValue(), italic.isSelected());
textColor.valueProperty().addListener(listener);
italic.selectedProperty().addListener(listener);
UndoManager undoMgr = new UndoManager();
undoMgr.register(textColor.valueProperty());
undoMgr.register(italic.selectedProperty());
undoMgr.register(text.textProperty());
Button undo = new Button("Undo");
Button redo = new Button("Redo");
undo.setOnAction(e -> undoMgr.undo());
redo.setOnAction(e -> undoMgr.redo());
HBox controls = new HBox(textColor, italic, undo, redo);
controls.setSpacing(5);
BorderPane root = new BorderPane(text);
root.setTop(controls);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
private void updateStyle(TextArea text, Color textColor, boolean italic) {
StringBuilder style = new StringBuilder()
.append("-fx-text-fill: ")
.append(hexString(textColor))
.append(";")
.append("-fx-font: ");
if (italic) {
style.append("italic ");
}
style.append("13pt sans-serif ;");
text.setStyle(style.toString());
}
private String hexString(Color color) {
int r = (int) (color.getRed() * 255) ;
int g = (int) (color.getGreen() * 255) ;
int b = (int) (color.getBlue() * 255) ;
return String.format("#%02x%02x%02x", r, g, b);
}
private static class ColorCell extends ListCell<Color> {
private Rectangle rect = new Rectangle(25, 25);
#Override
protected void updateItem(Color color, boolean empty) {
super.updateItem(color, empty);
if (empty || color==null) {
setGraphic(null);
} else {
rect.setFill(color);
setGraphic(rect);
}
}
}
public static void main(String[] args) {
Application.launch(args);
}
}
There is pretty much not a possibility to do this without "hacks"!
However, there is also a shorter solution, via using reflection:
/**
* Set the value of property without firing any change event.
* The value of property will be set via reflection.
* This property must be "Base" property such as {#link DoublePropertyBase}.
*
* #param property | Property to set!
* #param newValue | New value of property.
*/
public static <T> void setPropertyWithoutFiringEvent(Property<T> property, T newValue)
{
Class<?> cls = property.getClass();
while (cls != null) //While until helper variable is found
{
try
{
Field fieldH = cls.getDeclaredField("helper"), fieldV = cls.getDeclaredField("valid");
fieldH.setAccessible(true);
fieldV.setAccessible(true);
Object helper = fieldH.get(property), valid = fieldV.getBoolean(property); //Temporary values
fieldH.set(property, null); //Disabling ExpressionHelper by setting it on null;
property.setValue(newValue);
fieldH.set(property, helper); //Setting helper back!
fieldV.set(property, valid); //Important
return;
}
catch (Exception e)
{
cls = cls.getSuperclass(); //If not found go to super class of property next time!
}
}
System.err.println("Property " + property + " cant be set because variable \"helper\" was not found!");
}
This function temporarily disables ExpressionHelper what is an object responsible for firing change events, and then it will change the value of property and enable ExpressionHelper back! This will cause that one change will not be notified!
If the reflection is not friendly solution for you, then just use the solution above however this one is far shorter and simpler.

Creating Custom Spinner Model

I'm trying to create a custom JSpinner that represents a sequence of numbers of power 2. Like, 1-2-4-8-16 and so on. I have to do that with extending AbstractSpinnerModel and changing its implemented methods (getNextValue etc.). The problem is, the arrow keys on my custom JSpinner don't work. Nothing changes when I click one of them. I need to show previous and next values of current value. ( Previous value = 4 Current Value = 8 Next Value = 16 ).
Here is my code :
import java.awt.*;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class Hw5SpinnerModel extends JFrame {
public static void main(String[] args) {
Hw5SpinnerModel frame = new Hw5SpinnerModel();
}
public Hw5SpinnerModel() {
setTitle("Hw5SpinnerModel");
setSize(350, 300);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final JSpinner jspn = new JSpinner(new CustomSpinnerModel());
final JLabel jlbl = new JLabel("");
add(jspn, BorderLayout.NORTH);
add(jlbl, BorderLayout.CENTER);
jspn.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
jlbl.setText(jspn.getPreviousValue() + jspn.getValue() + jspn.getNextValue());
}
});
//jlbl.setText(jspn.getNextValue() + "");
setVisible(true);
}
public class CustomSpinnerModel extends AbstractSpinnerModel {
Integer i = 1;
#Override
public Object getValue() {
return i;
}
#Override
public void setValue(Object value) {
i = (Integer) value;
}
#Override
public Object getNextValue() {
return 2 * i;
}
#Override
public Object getPreviousValue() {
return i / 2;
}
}
}
Any help will be appreciated.
jSpinner1.setModel(new SpinnerNumberModel(1, null, null, 1) {
#Override
public Object getNextValue() {
Object nextValue = super.getValue();
int x = Integer.valueOf(nextValue.toString())*2;
//Object o = x;
return x;
}
});
It works 100%
I had this problem this week, and here's the answer to your question: The JSpinner operates through the use of ChangeListeners, and those ChangeListeners need to be activated when the model's value changes.
You may have had difficulty finding this (as I did), because you thought that getNextValue and getPreviousValue, in your custom class, were returning bad values. They aren't; but the JSpinner just doesn't use them directly. It feeds them straight back to the SpinnerModel through setValue.
(Except if getNextValue or getPreviousValue return null, which is a special case. Then the JSpinner does nothing.)
So, in code, your setValue function needs a fireStateChanged call after the change to i:
#Override
public void setValue(Object value) {
i = (Integer) value;
fireStateChanged ();
}
You might think that the JSpinner shouldn't operate this way, and should take the value from getNextValue and getPreviousValue to display immediately, instead of this indirect process. But the JSpinner has to call setValue in any case, and setValue has to notify the ChangeListeners in any case (because a single model can be passed around to many places in your program).
For instance, if you had two JSpinners in different places, with the same SpinnerModel, then changes to the model need to be reflected on both JSpinners, but only one JSpinner gets to call getNextValue or getPreviousValue at one time.

ILazyContentProvider updating everything at each viewer.setItemCount()

Greetings dear Stackoverflowians,
A couple of months ago I was dealing with a ILazyTreeContentProvider, and finally fixed it as per Eclipse RCP - ILazyTreeContentProvider implementation is unexpectedly eager
But I am facing the exact same problem with a ILazyContentProvider, and despite having followed similar steps as with the tree, I am at a loss.
In this table I am adding around 1000 elements per second in the table, and triggering a refresh via setItemCount() on the viewer every 100 ms.
The window size is smaller than 100 rows, and hence the updateElement() method should not start from the first index every time I call setItemCount() on the viewer.
Unfortunately, though, it does. It updates from 0 till the last index, each time.
Here's the code:
package manyelementscontentprovider;
import java.util.List;
import java.util.Vector;
import org.eclipse.jface.viewers.ILazyContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
public class LargeDataSetTable {
private class MyContentProvider implements ILazyContentProvider {
private TableViewer viewer;
public List<MyEntity> elements;
private int lastIndex=0;
public MyContentProvider(TableViewer viewer) {
this.viewer = viewer;
}
public void dispose() {
}
#SuppressWarnings("unchecked")
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
this.elements = (List<MyEntity>) newInput;
}
#Override
public void updateElement(int index) {
System.out.println(index);
if (!viewer.isBusy())
viewer.replace(elements.get(index), index);
}
}
public static class MyEntity {
public int counter;
public MyEntity(int counter) {
this.counter = counter;
}
public String toString() {
return "Item " + this.counter;
}
}
List<MyEntity> model;
private int counter;
private Display display;
private TableViewer v;
public LargeDataSetTable(Shell shell, Display display) {
model = createModel();
this.display=display;
v= new TableViewer(shell, SWT.VIRTUAL);
v.setLabelProvider(new LabelProvider());
v.setContentProvider(new MyContentProvider(v));
v.setInput(null);
v.setUseHashlookup(true);
counter = 0;
v.setInput(model);
v.setItemCount(model.size());
v.getTable().setLinesVisible(true);
}
private void startSomeShit() {
final Runnable gooeiUpdate = new Runnable() {
#Override
public void run() {
long timeA = System.currentTimeMillis();
v.setItemCount(counter);
v.setSelection( new StructuredSelection( model.get(counter-1) ), true );
v.setSelection(null);
long timeB = System.currentTimeMillis();
System.out.println("Paint lasted:"+(timeB-timeA));
}
};
Runnable addThingsToModel = new Runnable() {
public void run() {
long currentTime=System.currentTimeMillis();
long howManyGotIn =0;
while (counter<4000000) {
for (int i = 0; i< 10; i++){
final MyEntity m = new MyEntity(counter);
model.add(m);
counter++;
}
if (System.currentTimeMillis()-currentTime>100) {
howManyGotIn=counter - howManyGotIn;
display.syncExec(gooeiUpdate);
currentTime=System.currentTimeMillis();
System.out.println("How many got in = "+howManyGotIn);
howManyGotIn=counter;
}
try {
Thread.sleep(0,25);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread th = new Thread(addThingsToModel);
th.start();
}
private List<MyEntity> createModel() {
List<MyEntity> list = new Vector<MyEntity>(4000000);
return list;
}
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setLayout(new FillLayout());
LargeDataSetTable viewerCica = new LargeDataSetTable(shell,display);
shell.open();
viewerCica.startSomeShit();
while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
display.dispose();
}
}
Any sort of suggestions, opinions and options are very appreciated. You guys rock!
The javadoc for
TableViewer.setSelection(ISelection selection, boolean reveal)
states the following:
Sets a new selection for this viewer and optionally makes it visible. The TableViewer implementation of this method is inefficient for the ILazyContentProvider as lookup is done by indices rather than elements and may require population of the entire table in worse case.
Use Table#setSelection(int[] indices) and Table#showSelection() if you wish to set selection more efficiently when using a ILazyContentProvider.
Therefore, you could write something like this:
v.getTable().setSelection(counter - 1);
v.getTable().showSelection();
Using this approach, the paint operation takes an average time of 10ms.
Here is some code snippet from the AbstractTableViewer#virtualSetSelectionToWidget(List list, boolean reveal), which is called, when you use v.setSelection(new StructuredSelection(model.get(counter - 1)), true);:
if (getContentProvider() instanceof ILazyContentProvider) {
ILazyContentProvider provider = (ILazyContentProvider) getContentProvider();
// Now go through it again until all is done or we are no longer
// virtual
// This may create all items so it is not a good
// idea in general.
// Use #setSelection (int [] indices,boolean reveal) instead
for (int i = 0; virtualElements.size() > 0 && i < doGetItemCount(); i++) {
provider.updateElement(i);
Item item = doGetItem(i);
if (virtualElements.contains(item.getData())) {
indices[count++] = i;
virtualElements.remove(item.getData());
if (firstItem == null) {
firstItem = item;
}
}
}
}
as you can see it always iterates over all elements (confessing, that It might not be the best idea), as per Eclipse 3.x. Tree viewer has different implementation (which is actually understandable, that there you actually have kind of visibility levels and in table you don't have those).
I think, that refreshing of elements in general could be handled without dependency on content provider, so that only visible elements are refreshed (at least on demand).

Filtering JList based on JTextField

I have a JTextField and a JList in my program. The JList contains the user's contacts. I'd like to filter the JList based on the text on the JTextField. For example, if I type in "Mike" it will only show contacts including "Mike". When the user clears the JTextField it would reset the filter.
I know I could do this manually by having two arrays. One for the original contacts and one for the filtered ones. When the user changes the value of the JTextField I would go trought the original list, update the temporary list and update the JList. I just wonder if there is some built in feature to avoid manual labour.
The best way to do things like that is to have a ListModel implementation that filters its contents.
I don't know of any default filtering ListModel implementations, but it should not be too hard to do.
Here's a quick and dirty solution just to give you an idea. You might want to add more bells and whistles to it.
package test;
import java.util.ArrayList;
import javax.swing.AbstractListModel;
import javax.swing.ListModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
public class FilteredListModel extends AbstractListModel {
public static interface Filter {
boolean accept(Object element);
}
private final ListModel _source;
private Filter _filter;
private final ArrayList<Integer> _indices = new ArrayList<Integer>();
public FilteredListModel(ListModel source) {
if (source == null)
throw new IllegalArgumentException("Source is null");
_source = source;
_source.addListDataListener(new ListDataListener() {
public void intervalRemoved(ListDataEvent e) {
doFilter();
}
public void intervalAdded(ListDataEvent e) {
doFilter();
}
public void contentsChanged(ListDataEvent e) {
doFilter();
}
});
}
public void setFilter(Filter f) {
_filter = f;
doFilter();
}
private void doFilter() {
_indices.clear();
Filter f = _filter;
if (f != null) {
int count = _source.getSize();
for (int i = 0; i < count; i++) {
Object element = _source.getElementAt(i);
if (f.accept(element)) {
_indices.add(i);
}
}
fireContentsChanged(this, 0, getSize() - 1);
}
}
public int getSize() {
return (_filter != null) ? _indices.size() : _source.getSize();
}
public Object getElementAt(int index) {
return (_filter != null) ? _source.getElementAt(_indices.get(index)) : _source.getElementAt(index);
}
}
In order to use it you need to set it to your JList and then call setFilter() as you need.
Here's an example:
ListModel source = new DefaultListModel(); // use a model of your choice here;
FilteredListModel filteredListModel = new FilteredListModel(source);
JList list = new JList(filteredListModel);
filteredListModel.setFilter(new FilteredListModel.Filter() {
public boolean accept(Object element) {
return false; // put your filtering logic here.
}
});
Once method setFilter() is invoked your JList on the screen is expected to change its contents accordingly.
Alternatively, you may want to implement an observer/observable pattern for your Filter, so you can re-filter the list without calling method setFilter(). You can experiment with that later. For the first iteration it's good enough as long as you call method setFilter every time user types something in your JTextField.
A simpler solution might be to use JTable, which does have a built-in ability to filter and sort (RowSorter). A single-column table is not too different from a list.
If you're okay with external libs, I would recommend Jide's QuickListFilterField/QuickTreeFilterField. With few lines of code, you could get a visually filterable JList/JTree, case sensitive/insensitive search, wildcard/regex matching etc ... Amazingly easy to use !

Using null assignment to control addition and removal of listeners

I have a case where a JComponent needs to have a listener added or removed depending on the state of other fields of a class. The listener should not be added more than once, and it can, of course, be removed only once. Is it a good practice to use a class field to store the listener and use the null value to control the action of registering/unregistering the listener with the Component.
The code I have in mind is something like this (code modified to make it clear that the JComponent is provided to the class):
public class MyClass {
private ActionListener fListener = null;
private JComponent fComponent;
public MyClass(JComponent component) {
fComponent = component; // for example, component = new JButton("Test");
}
public void setListener() {
if (fListener == null ) {
fListener = new MyListener();
fComponent.addActionListener(fListener);
}
}
public void removeListener() {
if (fListener != null) {
fComponent.removeActionListener(fListener);
fListener = null;
}
}
}
Do not instantiate and dispose listener object every time. Use getActionListeners() method to verify that listener is added or not.
public class MyClass {
private ActionListener fListener = new MyListener();
private JButton fComponent = new JButton("Test");
public MyClass() {
fComponent.addActionListener(fListener);
}
public void setListener() {
if (fComponent.getActionListeners().length == 0) {
fComponent.addActionListener(fListener);
}
}
public void removeListener() {
if (fComponent.getActionListeners().length !=0) {
fComponent.removeActionListener(fListener);
}
}
}
Method ActionListener[] getActionListeners() returns array of all the ActionListeners added to this JButton.
Is it absolutely necessary to continually add and remove the listener from the component? Can you just disable the component, or have a flag that you can use to determine if the action can be run?
Can you wrap the listener in another listener that you define? The enveloping listener can have a boolean switch you can flip to control delegation to the real listener.
If worse comes to worst and you absolutely have to remove and add the listener, you can do it as follows, with a twist on AVD's solution:
public void setListener() {
// java.util.List and java.util.Arrays
List<ActionListeners> listenerList = Arrays.asList(fComponent.getActionListeners());
if (!listenerList.contains(fListener) {
fComponent.addActionListener(fListener);
}
}
public void removeListener() {
List<ActionListeners> listenerList = Arrays.asList(fComponent.getActionListeners());
if (listenerList.contains(fListener) {
fComponent.removeActionListener(fListener);
}
}

Categories

Resources