I have a table cell factory responsible for creating an editable cell in a JavaFX TableView.
I'm trying to implement some added functionality to the tableview so that when the user clicks outside the editable cell a commit is made (the edited text is saved, and not discarded as per the default tableview behavior.)
I added an textField.focusedProperty() event handler, where I commit the text from the text field. However, when one clicks outside the current cell cancelEdit() gets called and calling commitEdit(textField.getText()); has no effect.
I have come to realize that once cancelEdit() is called the TableCell.isEditing() returns false and so the commit will never happen.
How can I make so that when the user clicks outside the editable cell the text is committed?
After committing an setOnEditCommit() event handler will take care of the validation and database logic. I haven't included it here since it will most likely complicate things even further.
// EditingCell - for editing capability in a TableCell
public static class EditingCell extends TableCell<Person, String> {
private TextField textField;
public EditingCell() {
}
#Override public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setText(null);
setGraphic(textField);
textField.selectAll();
}
#Override public void cancelEdit() {
super.cancelEdit();
setText((String) getItem());
setGraphic(null);
}
#Override public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(textField.getText());
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (!newValue) {
commitEdit(textField.getText());
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
Since I could not find kuaw26's source code (dead link) I developed my own solution for java 8. I found out that the TextField in the code above never receives a keyReleased event for the esc-key, therefore his code does not work.
Unfortunately I needed to duplicate code from TextFieldTableCell and CellUtils and adapt it, since TextFieldTableCell uses a private TextField and CellUtils is package protected. This is probably not the best OO way.
Here is my solution:
// package yourLib;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.util.Callback;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
/**
* A class containing a {#link TableCell} implementation that draws a
* {#link TextField} node inside the cell. If the TextField is
* left, the value is commited.
*
*/
public class AcceptOnExitTableCell<S,T> extends TableCell<S,T> {
/***************************************************************************
* *
* Static cell factories *
* *
**************************************************************************/
/**
* Provides a {#link TextField} that allows editing of the cell content when
* the cell is double-clicked, or when
* {#link TableView#edit(int, javafx.scene.control.TableColumn)} is called.
* This method will only work on {#link TableColumn} instances which are of
* type String.
*
* #return A {#link Callback} that can be inserted into the
* {#link TableColumn#cellFactoryProperty() cell factory property} of a
* TableColumn, that enables textual editing of the content.
*/
public static <S> Callback<TableColumn<S,String>, TableCell<S,String>> forTableColumn() {
return forTableColumn(new DefaultStringConverter());
}
/**
* Provides a {#link TextField} that allows editing of the cell content when
* the cell is double-clicked, or when
* {#link TableView#edit(int, javafx.scene.control.TableColumn) } is called.
* This method will work on any {#link TableColumn} instance, regardless of
* its generic type. However, to enable this, a {#link StringConverter} must
* be provided that will convert the given String (from what the user typed
* in) into an instance of type T. This item will then be passed along to the
* {#link TableColumn#onEditCommitProperty()} callback.
*
* #param converter A {#link StringConverter} that can convert the given String
* (from what the user typed in) into an instance of type T.
* #return A {#link Callback} that can be inserted into the
* {#link TableColumn#cellFactoryProperty() cell factory property} of a
* TableColumn, that enables textual editing of the content.
*/
public static <S,T> Callback<TableColumn<S,T>, TableCell<S,T>> forTableColumn(
final StringConverter<T> converter) {
return list -> new AcceptOnExitTableCell<S,T>(converter);
}
/***************************************************************************
* *
* Fields *
* *
**************************************************************************/
private TextField textField;
private boolean escapePressed=false;
private TablePosition<S, ?> tablePos=null;
/***************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates a default TextFieldTableCell with a null converter. Without a
* {#link StringConverter} specified, this cell will not be able to accept
* input from the TextField (as it will not know how to convert this back
* to the domain object). It is therefore strongly encouraged to not use
* this constructor unless you intend to set the converter separately.
*/
public AcceptOnExitTableCell() {
this(null);
}
/**
* Creates a TextFieldTableCell that provides a {#link TextField} when put
* into editing mode that allows editing of the cell content. This method
* will work on any TableColumn instance, regardless of its generic type.
* However, to enable this, a {#link StringConverter} must be provided that
* will convert the given String (from what the user typed in) into an
* instance of type T. This item will then be passed along to the
* {#link TableColumn#onEditCommitProperty()} callback.
*
* #param converter A {#link StringConverter converter} that can convert
* the given String (from what the user typed in) into an instance of
* type T.
*/
public AcceptOnExitTableCell(StringConverter<T> converter) {
this.getStyleClass().add("text-field-table-cell");
setConverter(converter);
}
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
// --- converter
private ObjectProperty<StringConverter<T>> converter =
new SimpleObjectProperty<StringConverter<T>>(this, "converter");
/**
* The {#link StringConverter} property.
*/
public final ObjectProperty<StringConverter<T>> converterProperty() {
return converter;
}
/**
* Sets the {#link StringConverter} to be used in this cell.
*/
public final void setConverter(StringConverter<T> value) {
converterProperty().set(value);
}
/**
* Returns the {#link StringConverter} used in this cell.
*/
public final StringConverter<T> getConverter() {
return converterProperty().get();
}
/***************************************************************************
* *
* Public API *
* *
**************************************************************************/
/** {#inheritDoc} */
#Override public void startEdit() {
if (! isEditable()
|| ! getTableView().isEditable()
|| ! getTableColumn().isEditable()) {
return;
}
super.startEdit();
if (isEditing()) {
if (textField == null) {
textField = getTextField();
}
escapePressed=false;
startEdit(textField);
final TableView<S> table = getTableView();
tablePos=table.getEditingCell();
}
}
/** {#inheritDoc} */
#Override public void commitEdit(T newValue) {
if (! isEditing())
return;
final TableView<S> table = getTableView();
if (table != null) {
// Inform the TableView of the edit being ready to be committed.
CellEditEvent editEvent = new CellEditEvent(
table,
tablePos,
TableColumn.editCommitEvent(),
newValue
);
Event.fireEvent(getTableColumn(), editEvent);
}
// we need to setEditing(false):
super.cancelEdit(); // this fires an invalid EditCancelEvent.
// update the item within this cell, so that it represents the new value
updateItem(newValue, false);
if (table != null) {
// reset the editing cell on the TableView
table.edit(-1, null);
// request focus back onto the table, only if the current focus
// owner has the table as a parent (otherwise the user might have
// clicked out of the table entirely and given focus to something else.
// It would be rude of us to request it back again.
// requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
}
}
/** {#inheritDoc} */
#Override public void cancelEdit() {
if(escapePressed) {
// this is a cancel event after escape key
super.cancelEdit();
setText(getItemText()); // restore the original text in the view
}
else {
// this is not a cancel event after escape key
// we interpret it as commit.
String newText=textField.getText(); // get the new text from the view
this.commitEdit(getConverter().fromString(newText)); // commit the new text to the model
}
setGraphic(null); // stop editing with TextField
}
/** {#inheritDoc} */
#Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
updateItem();
}
/***************************************************************************
* *
* // djw code taken and adapted from package protected CellUtils. *
* *
**************************************************************************/
private TextField getTextField() {
final TextField textField = new TextField(getItemText());
// Use onAction here rather than onKeyReleased (with check for Enter),
// as otherwise we encounter RT-34685
textField.setOnAction(event -> {
if (converter == null) {
throw new IllegalStateException(
"Attempting to convert text input into Object, but provided "
+ "StringConverter is null. Be sure to set a StringConverter "
+ "in your cell factory.");
}
this.commitEdit(getConverter().fromString(textField.getText()));
event.consume();
});
textField.setOnKeyPressed(t -> { if (t.getCode() == KeyCode.ESCAPE) escapePressed = true; else escapePressed = false; });
textField.setOnKeyReleased(t -> {
if (t.getCode() == KeyCode.ESCAPE) {
// djw the code may depend on java version / expose incompatibilities:
throw new IllegalArgumentException("did not expect esc key releases here.");
}
});
return textField;
}
private String getItemText() {
return getConverter() == null ?
getItem() == null ? "" : getItem().toString() :
getConverter().toString(getItem());
}
private void updateItem() {
if (isEmpty()) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getItemText());
}
setText(null);
setGraphic(textField);
} else {
setText(getItemText());
setGraphic(null);
}
}
}
private void startEdit(final TextField textField) {
if (textField != null) {
textField.setText(getItemText());
}
setText(null);
setGraphic(textField);
textField.selectAll();
// requesting focus so that key input can immediately go into the
// TextField (see RT-28132)
textField.requestFocus();
}
}
You could do it by overriding the method commitEdit as next:
#Override
public void commitEdit(T item) {
// This block is necessary to support commit on losing focus, because
// the baked-in mechanism sets our editing state to false before we can
// intercept the loss of focus. The default commitEdit(...) method
// simply bails if we are not editing...
if (!isEditing() && !item.equals(getItem())) {
TableView<S> table = getTableView();
if (table != null) {
TableColumn<S, T> column = getTableColumn();
CellEditEvent<S, T> event = new CellEditEvent<>(
table, new TablePosition<S,T>(table, getIndex(), column),
TableColumn.editCommitEvent(), item
);
Event.fireEvent(column, event);
}
}
super.commitEdit(item);
}
This workaround comes from https://gist.github.com/james-d/be5bbd6255a4640a5357#file-editcell-java-L109
Here's how I did it - I binded the textField's text property with the text property of the cell (bidirectional).
class EditingCell<S, T> extends TableCell<S, T> {
private final TextField mTextField;
public EditingCell() {
super();
mTextField = new TextField();
mTextField.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if( event.getCode().equals(KeyCode.ENTER) )
commitEdit((T)mTextField.getText());
}
});
mTextField.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if( !newValue )
commitEdit((T)mTextField.getText());
}
});
mTextField.textProperty().bindBidirectional(textProperty());
}
#Override
public void startEdit() {
super.startEdit();
setGraphic(mTextField);
}
#Override
public void cancelEdit() {
super.cancelEdit();
setGraphic(null);
}
#Override
public void updateItem(final T item, final boolean empty) {
super.updateItem(item, empty);
if( empty ) {
setText(null);
setGraphic(null);
}
else {
if( item == null ) {
setGraphic(null);
}
else {
if( isEditing() ) {
setGraphic(mTextField);
setText((String)getItem());
}
else {
setGraphic(null);
setText((String)getItem());
}
}
}
}
}
I created my own workaround (but for JavaFX 2). Main idea - transform cancelEdit() to commitEdit(). With possible validation of committed text via validator.
/** Validator. */
public interface TextColumnValidator<T> {
boolean valid(T rowVal, String newVal);
}
/**
* Special table text field cell that commit its content on focus lost.
*/
public class TextFieldTableCellEx<S> extends TextFieldTableCell<S, String> {
/** */
private final TextColumnValidator<S> validator;
/** */
private boolean cancelling;
/** */
private boolean hardCancel;
/** */
private String curTxt = "";
/** Create cell factory. */
public static <S> Callback<TableColumn<S, String>, TableCell<S, String>>
cellFactory(final TextColumnValidator<S> validator) {
return new Callback<TableColumn<S, String>, TableCell<S, String>>() {
#Override public TableCell<S, String> call(TableColumn<S, String> col) {
return new TextFieldTableCellEx<>(validator);
}
};
}
/**
* Text field cell constructor.
*
* #param validator Input text validator.
*/
private TextFieldTableCellEx(TextColumnValidator<S> validator) {
this.validator = validator;
}
/** {#inheritDoc} */
#Override public void startEdit() {
super.startEdit();
curTxt = "";
hardCancel = false;
Node g = getGraphic();
if (g != null) {
final TextField tf = (TextField)g;
tf.textProperty().addListener(new ChangeListener<String>() {
#Override public void changed(ObservableValue<? extends String> val, String oldVal, String newVal) {
curTxt = newVal;
}
});
tf.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override public void handle(KeyEvent evt) {
if (KeyCode.ENTER == evt.getCode())
cancelEdit();
else if (KeyCode.ESCAPE == evt.getCode()) {
hardCancel = true;
cancelEdit();
}
}
});
// Special hack for editable TextFieldTableCell.
// Cancel edit when focus lost from text field, but do not cancel if focus lost to VirtualFlow.
tf.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override public void changed(ObservableValue<? extends Boolean> val, Boolean oldVal, Boolean newVal) {
Node fo = getScene().getFocusOwner();
if (!newVal) {
if (fo instanceof VirtualFlow) {
if (fo.getParent().getParent() != getTableView())
cancelEdit();
}
else
cancelEdit();
}
}
});
Platform.runLater(new Runnable() {
#Override public void run() {
tf.requestFocus();
}
});
}
}
/** {#inheritDoc} */
#Override public void cancelEdit() {
if (cancelling)
super.cancelEdit();
else
try {
cancelling = true;
if (hardCancel || curTxt.trim().isEmpty())
super.cancelEdit();
else if (validator.valid(getTableView().getSelectionModel().getSelectedItem(), curTxt))
commitEdit(curTxt);
else
super.cancelEdit();
}
finally {
cancelling = false;
}
}
}
Update: this code was written as a part of Apache Ignite Schema Import GUI Utility.
See full version of TableCell code: https://github.com/apache/ignite/blob/ignite-1.9/modules/schema-import/src/main/java/org/apache/ignite/schema/ui/Controls.java
Also you could build this utility (it is a very simple utility with 2 screens) and play with it under Java7/javaFx2 & Java8/JavaFx8.
I tested - it works under both of them.
Related
I am creating a window with JavaFX, using an object oriented approach which will allow different kinds of windows to be created with the same basic framework. These windows will display html content.
I have run into a concurrency problem when trying to add an observer.
Window:
public Window(String linkName, String fName, String wName, int width, int height, GuiObserver o) throws IOException {
wApp = new WindowApp();
wApp.setObserver(o);
new Thread(new Runnable() {
public void run() {
wApp.launch(WindowApp.class,linkName,fName,wName,""+width,""+height);
/*try {
wApp.launch(WindowApp.class,linkName,fName,wName,""+width,""+height);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
}
}).start();
}
This creates the window and sets the observer, but puts the launch into a new thread. I can confirm that the observer is added, but the observer cannot be notified. Here is the WindowApp class:
/**
* Instantiate the class with an html file
* THIS MUST NOT TAKE ANY PARAMETERS
*/
public WindowApp() throws IOException {}
#Override
public void start(Stage s) throws Exception {
this.stage = s;
//get the parameters
List<String> params = getParameters().getRaw();
String linkName = params.get(0);
updateHtmlFromFile(new File(params.get(1)));
windowName = params.get(2);
windowWidth = Integer.parseInt(params.get(3));
windowHeight = Integer.parseInt(params.get(4));
//create the window
stage.setWidth(windowWidth);
stage.setHeight(windowHeight);
Scene scene = new Scene(new Group());
browser = new WebView();
webEngine = browser.getEngine();
webEngine.setJavaScriptEnabled(true);
//add the link which will bind Java and Javascript functionality
Class jC = Class.forName(linkName);
Constructor jCon = jC.getConstructor(browser.getClass(),webEngine.getClass(),stage.getClass());
javascriptLink = (JavascriptLink) jCon.newInstance(browser,webEngine,stage);
javascriptLink.setObserver(gObserver);
//javascriptLink = new JavascriptLink(browser,webEngine,stage);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(browser);
webEngine.getLoadWorker().stateProperty()
.addListener(new ChangeListener<State>() {
#Override
public void changed(ObservableValue ov, State oldState, State newState) {
if (newState == Worker.State.SUCCEEDED) {
stage.setTitle(windowName);
}
}
});
webEngine.load(htmlUrl);
scene.setRoot(scrollPane);
stage.setScene(scene);
stage.show();
}
/**
* Add an observer to the javascript Link
* #param o
*/
public void setObserver(GuiObserver o) {
gObserver = o;
}
This object also has a "Javascript link" which interacts with the HTML document after its launch:
package gui;
import java.io.FileNotFoundException;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class MainWindowJSLink extends JavascriptLink {
private GuiObserver gObserver;
/**
* This will be called by WindowApp when appropriate, and does not need to be called by a person
* #param b
* #param we
* #param st
* #throws FileNotFoundException
*/
public MainWindowJSLink(WebView b, WebEngine we, Stage st) throws FileNotFoundException {
super(b, we, st);
}
#Override
public void eventTriggered(String eventName) {
System.out.println("Event triggered -> "+eventName);
switch (eventName) {
case "codeEditorContentsChanged":
handleCodeEditorContentsChanged();
break;
}
//notify observers if necessary
this.triggerObserver(eventName, null);
}
/**
* Handle the event that the contents of the code editor have changed
* This will probably setup a timer to wait a few seconds until the user has stopped typing,
* then send the contents to the syntax highlighter and get the result back
*/
private void handleCodeEditorContentsChanged() {
//System.out.println(getJSCodeEditorContents());
}
/**
* Get the raw contents of the code editor (unedited and not stripped of html entities)
* #return
*/
public String getJSCodeEditorContents() {
String contents = (String) super.webEngine.executeScript("getCodeEditorContents()");
return contents;
}
/**
* Set the contents of the code editor
* #param contents String
* NOTE:
*/
public void setJSCodeEditorContents(String contents) {
super.webEngine.executeScript("setCodeEditorContents(`"+contents+"`)");
}
//observer observable stuff
#Override
public void setObserver(GuiObserver o) {
gObserver = o;
gObserver.setJS(this);
System.out.println("MainWindow -> observer set");
}
#Override
public void triggerObserver(String s, Object o) {
gObserver.trigger(s,o);
}
#Override
/**
* Handle information sent from the
*/
public void send(String s, Object o) {
//handle information sent to this object
}
}
Unfortunately, the gObserver.trigger does not execute. Interestingly enough, if I put a System.exit(0) before that trigger is called, the program exits. However, if I put it after it is called (or in the corresponding method), it is not.
Can anyone offer any advice on how to fix this concurrency issue? If you need more information, please let me know, and I will provide it.
class RuleComboBox extends JComboBox {
public RuleComboBox() {
super();
this.setModel(new javax.swing.DefaultComboBoxModel(new String[]{"abc", "defg"}));
((JLabel) this.getRenderer()).setHorizontalAlignment(SwingConstants.CENTER);
}
}
The getRenderer() line aligns the text to centre.
When I useruleComboBox1.setEnabled(false) and ruleComboBox1.setEditable(true), the text aligns back to the left which I don't want. How can I stop this?
I should explain that I'm using setEditable(true) to keep the appearance of the text within the ComboBox when I disable it.
The editor for a JComboBox must implement the ComboBoxEditor interface. The default implementation extends from BasicComboBoxEditor which returns a JTextField as the editor. A JTextField doesn't support the concept of displaying text centered.
So you can implement your own ComboBoxEditor. I would suggest you can just use the BasicComboBoxEditor and change the code to create a JTextPane instead of a JTextField. Then when you create the text pane you can use:
SimpleAttributeSet center = new SimpleAttributeSet();
StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);
StyledDocument doc = textPane.getStyledDocument();
doc.setParagraphAttributes(0, doc.getLength(), center, false);
which will cause the code to be centered.
Note: this will not be a straight forward conversion. A JTextField invokes an ActionListener when the Enter key is pressed. A JTextPane doesn't support this functionality (the default is to insert a new line) so you will need to replicate this functionality for the JTextPane. That is you will need to use Key Bindings to handle the Enter key. So you will need to wrap the ActionListener in a custom Action and then bind the Enter key to the text pane.
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.border.Border;
import java.awt.Component;
import java.awt.event.*;
import java.lang.reflect.Method;
//import sun.reflect.misc.MethodUtil;
/**
* A custom editor for editable combo boxes. The editor is implemented as a JTextPane.
*
*/
public class TextPaneComboBoxEditor implements ComboBoxEditor {
protected JTextPane editor;
private Object oldValue;
public TextPaneComboBoxEditor() {
editor = createEditorComponent();
}
public Component getEditorComponent() {
return editor;
}
/**
* Creates the internal editor component. Override this to provide
* a custom implementation.
*
* #return a new editor component
* #since 1.6
*/
protected JTextPane createEditorComponent() {
JTextPane editor = new BorderlessTextPane("",9);
editor.setBorder(null);
SimpleAttributeSet center = new SimpleAttributeSet();
StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);
StyledDocument doc = editor.getStyledDocument();
doc.setParagraphAttributes(0, doc.getLength(), center, false);
return editor;
}
/**
* Sets the item that should be edited.
*
* #param anObject the displayed value of the editor
*/
public void setItem(Object anObject) {
String text;
if ( anObject != null ) {
text = anObject.toString();
if (text == null) {
text = "";
}
oldValue = anObject;
} else {
text = "";
}
// workaround for 4530952
if (! text.equals(editor.getText())) {
editor.setText(text);
}
}
public Object getItem() {
Object newValue = editor.getText();
// This code only works for Strings. The default implementation would
// use reflection to create Object of whatever class was stored in the
// ComboBoxModel. You will need to fix the reflection code if you want
// to support other types of data in the model
/*
if (oldValue != null && !(oldValue instanceof String)) {
// The original value is not a string. Should return the value in it's
// original type.
if (newValue.equals(oldValue.toString())) {
return oldValue;
} else {
// Must take the value from the editor and get the value and cast it to the new type.
Class<?> cls = oldValue.getClass();
try {
Method method = MethodUtil.getMethod(cls, "valueOf", new Class[]{String.class});
newValue = MethodUtil.invoke(method, oldValue, new Object[] { editor.getText()});
} catch (Exception ex) {
// Fail silently and return the newValue (a String object)
}
}
}
*/
return newValue;
}
public void selectAll() {
editor.selectAll();
editor.requestFocus();
}
public void addActionListener(ActionListener l) {
// editor.addActionListener(l);
Action enter = new WrappedActionListener(l);
editor.getActionMap().put("insert-break", enter);
}
public void removeActionListener(ActionListener l) {
// editor.removeActionListener(l);
}
static class BorderlessTextPane extends JTextPane {
public BorderlessTextPane(String value,int n) {
// super(value,n);
setText(value);
}
// workaround for 4530952
public void setText(String s) {
if (getText().equals(s)) {
return;
}
super.setText(s);
}
public void setBorder(Border b) {
if (!(b instanceof UIResource)) {
super.setBorder(b);
}
}
}
/**
* A subclass of TextPaneComboBoxEditor that implements UIResource.
* TextPaneComboBoxEditor doesn't implement UIResource
* directly so that applications can safely override the
* cellRenderer property with TextPaneListCellRenderer subclasses.
* <p>
* <strong>Warning:</strong>
* Serialized objects of this class will not be compatible with
* future Swing releases. The current serialization support is
* appropriate for short term storage or RMI between applications running
* the same version of Swing. As of 1.4, support for long term storage
* of all JavaBeans™
* has been added to the <code>java.beans</code> package.
* Please see {#link java.beans.XMLEncoder}.
*/
public static class UIResource extends TextPaneComboBoxEditor
implements javax.swing.plaf.UIResource {
}
static class WrappedActionListener extends AbstractAction
{
private ActionListener listener;
public WrappedActionListener(ActionListener listener)
{
this.listener = listener;
}
#Override
public void actionPerformed(ActionEvent e)
{
listener.actionPerformed(e);
}
}
}
All you need in your current code is:
comboBox.setEditor( new TextPaneComboBoxEditor() );
I have a jtable with a custom editor and renderer applied to a column to turn the column's contents into a Button, however once i press on any of the buttons, the table loses focus, and the only interactive object in the panel is the button that was just clicked on, which can be clicked on repeatedly.
Here is the button renderer code:
public class ButtonRenderer implements TableCellRenderer {
private Border dsd_originalBorder;
private int in_mnemonic;
private Border dsd_focusBorder;
private JButton dsd_renderButton;
/**
* empty constructor
*/
public ButtonRenderer() {
dsd_renderButton = new JButton();
}
public Component getTableCellRendererComponent(JTable dsd_table, Object dsd_value,
boolean bo_isSelected, boolean bo_hasFocus, int in_row, int in_column) {
if (!WorkplaceConstants.STR_INACTIVE.equals(dsd_table.getValueAt(in_row, dsd_table
.getColumn(Main.hm_language.get(Language.STATUS))
.getModelIndex())))
return new JLabel("");
if (bo_isSelected) {
dsd_renderButton.setForeground(dsd_table.getSelectionForeground());
dsd_renderButton.setBackground(dsd_table.getSelectionBackground());
} else {
dsd_renderButton.setForeground(dsd_table.getForeground());
dsd_renderButton.setBackground(UIManager.getColor("Button.background"));
}
if (bo_hasFocus) {
dsd_renderButton.setBorder(dsd_focusBorder);
} else {
dsd_renderButton.setBorder(dsd_originalBorder);
}
// renderButton.setText( (value == null) ? "" : value.toString() );
if (dsd_value == null) {
dsd_renderButton.setText("");
dsd_renderButton.setIcon(null);
} else if (dsd_value instanceof Icon) {
dsd_renderButton.setText("");
dsd_renderButton.setIcon((Icon) dsd_value);
} else {
dsd_renderButton.setText(dsd_value.toString());
dsd_renderButton.setIcon(null);
}
return dsd_renderButton;
}
/**
* returns the mnemonic to activate the button when the cell has focus
*
* #return the mnemonic
*/
public int m_getMnemonic() {
return in_mnemonic;
}
/**
* The mnemonic to activate the button when the cell has focus
*
* #param p_in_mnemonic
* the mnemonic
*/
public void m_setMnemonic(int p_in_mnemonic) {
this.in_mnemonic = p_in_mnemonic;
dsd_renderButton.setMnemonic(p_in_mnemonic);
}
/**
* Get foreground color of the button when the cell has focus
*
* #return the foreground color
*/
public Border m_getFocusBorder() {
return dsd_focusBorder;
}
/**
* The foreground color of the button when the cell has focus
*
* #param dsd_focusBorder
* the foreground color
*/
public void m_setFocusBorder(Border dsd_focusBorder) {
this.dsd_focusBorder = dsd_focusBorder;
}
Here is the button Editor code:
public class ButtonEditor extends AbstractCellEditor implements TableCellEditor, ActionListener, MouseListener{
private JTable dsd_table;
private int in_mnemonic;
private Border dsd_originalBorder;
private Border dsd_focusBorder;
private JButton dsd_editButton;
private Object dsd_editorValue;
private boolean bo_isButtonColumnEditor;
/**
* Constructor
* #param dsdp_table the table to which the editor is going to be applied.
* #param action the action which is to be executed when the button is clicked
*/
public ButtonEditor(JTable dsdp_table) {
this.dsd_table = dsdp_table;
dsd_editButton = new JButton();
dsd_editButton.setFocusPainted( false );
dsd_editButton.addActionListener( this );
dsd_originalBorder = dsd_editButton.getBorder();
m_setFocusBorder( new LineBorder(Color.BLUE) );
dsdp_table.addMouseListener( this );
}
public Object getCellEditorValue() {
return dsd_editorValue;
}
/**
* returns the mnemonic to activate the button when the cell has focus
*
* #return the mnemonic
*/
public int m_getMnemonic()
{
return in_mnemonic;
}
/**
* The mnemonic to activate the button when the cell has focus
*
* #param in_mnemonic the mnemonic
*/
public void m_setMnemonic(int in_mnemonic)
{
this.in_mnemonic = in_mnemonic;
dsd_editButton.setMnemonic(in_mnemonic);
}
/**
* Get foreground color of the button when the cell has focus
*
* #return the foreground color
*/
public Border m_getFocusBorder() {
return dsd_focusBorder;
}
/**
* The foreground color of the button when the cell has focus
*
* #param dsdp_focusBorder
* the foreground color
*/
public void m_setFocusBorder(Border dsdp_focusBorder) {
this.dsd_focusBorder = dsdp_focusBorder;
dsd_editButton.setBorder(dsdp_focusBorder);
}
public void actionPerformed(ActionEvent arg0) {
int in_modelRow = dsd_table.convertRowIndexToModel( dsd_table.getEditingRow() );
fireEditingStopped();
// Here i start a custom thread to run in the background.
}
public void mouseClicked(MouseEvent arg0) {
}
public void mouseEntered(MouseEvent arg0) {
}
public void mouseExited(MouseEvent arg0) {
}
public void mousePressed(MouseEvent arg0) {
if (dsd_table.isEditing() && dsd_table.getCellEditor() == this)
bo_isButtonColumnEditor = true;
}
public void mouseReleased(MouseEvent arg0) {
if (bo_isButtonColumnEditor && dsd_table.isEditing())
dsd_table.getCellEditor().stopCellEditing();
bo_isButtonColumnEditor = false;
}
public void addCellEditorListener(CellEditorListener arg0) {
}
public void cancelCellEditing() {
}
public void removeCellEditorListener(CellEditorListener arg0) {
}
public boolean shouldSelectCell(EventObject arg0) {
return false;
}
public boolean stopCellEditing() {
return false;
}
public Component getTableCellEditorComponent(JTable dsd_table, Object dsd_value,
boolean bo_isSelected, int in_row, int in_column) {
if (!WorkplaceConstants.STR_INACTIVE.equals(dsd_table.getValueAt(in_row, dsd_table
.getColumn(Main.hm_language.get(Language.STATUS))
.getModelIndex())))
return new JLabel("");
if (dsd_value == null)
{
dsd_editButton.setText( "" );
dsd_editButton.setIcon( null );
}
else if (dsd_value instanceof Icon)
{
dsd_editButton.setText( "" );
dsd_editButton.setIcon( (Icon)dsd_value );
}
else
{
dsd_editButton.setText( dsd_value.toString() );
dsd_editButton.setIcon( null );
}
bo_isButtonColumnEditor = true;
this.dsd_editorValue = dsd_value;
dsd_editButton.setBorder(dsd_originalBorder);
return dsd_editButton;
}
public boolean isCellEditable(EventObject e){
return true;
}
public Border m_getoriginalBorder() {
return dsd_originalBorder;
}
public void m_set_originalBorder(Border dsd_originalBorder) {
this.dsd_originalBorder = dsd_originalBorder;
}
}
Here is how i am assigning the editor and renderer to the table
public static void m_setButtonColumnConfiguration(JTable table) {
ButtonEditor dsd_btn_edit = new ButtonEditor(table);
dsd_btn_edit.m_setMnemonic(KeyEvent.VK_D);
table.getColumn(/*i get the identifier for the column here*/).setCellEditor(dsd_btn_edit);
ButtonRenderer dsd_btn_rend = new ButtonRenderer();
dsd_btn_rend.m_setMnemonic(KeyEvent.VK_D);
table.getColumn(/*i get the identifier for the column here*/)
.setCellRenderer(dsd_btn_rend);
}
Button, however once i press on any of the buttons, the table loses focus
Yes, anytime you click on a component it gets focus, so this makes sense.
and the only interactive object in the panel is the button that was just clicked on,
Not sure I understand this statement. You can click on the table again and it will regain focus. You can then navigate around the table.
Maybe you are just suggesting the button renderer/editor isn't working the way you expect. We can't test this because you didn't post a SSCCE.
Anyway, check out Table Button Column for another example of a button renderer/editor that responds to mouse clicks or key board events.
I have to build a complex GUI in JAVA with Swing (for the moment I have near 80 classes).
The graphic partv of the application is split as follows: a first series of tabs (eg "Management", "Administration", "Configuration"), then a second level (for example, "User", "Group", "Game"). For now I'm two grade levels (one for each level of tabs). The next level is a JPanel that manages a business object (my whole GUI is built around my business model), at this level there are 2 type of JPanel: who manages simple objects (eg, "User", "Category" , "Game", "Level") and those which manages objects "composite primary key" (eg "User_Game" which represent the form of a double-entry table for each game level for all users).
My second level of tabs can contain multiple JPanel.
When my JPanel manages a single object is composed of a JTable and two buttons (Add and Remove) on which I put events, if not it is a simple JTable. When I have foreign keys (eg "Group" for "User" and "Category" to "Game" or "Level" to "User_Game") it is a JComboBox that takes its information directly from JTableModel. When it comes to managing a JTable object to "composite primary key" the columns and rows also directly dependent models (eg "Game" and "User" "User_Game").
Each has its own JTable model that deals with the persistence layer (Hibernate for information) and other TableModel.
To manage changes (such as adding, modifying or deleting a "User") I use the code below:
import java.awt.event.*;
import javax.swing.*;
import java.beans.*;
/*
* This class listens for changes made to the data in the table via the
* TableCellEditor. When editing is started, the value of the cell is saved
* When editing is stopped the new value is saved. When the oold and new
* values are different, then the provided Action is invoked.
*
* The source of the Action is a TableCellListener instance.
*/
public class TabCellListener implements PropertyChangeListener, Runnable
{
private JTable table;
private Action action;
private int row;
private int column;
private Object oldValue;
private Object newValue;
/**
* Create a TableCellListener.
*
* #param table the table to be monitored for data changes
* #param action the Action to invoke when cell data is changed
*/
public TabCellListener(JTable table, Action action)
{
this.table = table;
this.action = action;
this.table.addPropertyChangeListener( this );
this.table.getModel().addTableModelListener(new ModelListenerTableGui(this.table, this.action));
}
/**
* Create a TableCellListener with a copy of all the data relevant to
* the change of data for a given cell.
*
* #param row the row of the changed cell
* #param column the column of the changed cell
* #param oldValue the old data of the changed cell
* #param newValue the new data of the changed cell
*/
private CellListenerTableGui(JTable table, int row, int column, Object oldValue, Object newValue)
{
this.table = table;
this.row = row;
this.column = column;
this.oldValue = oldValue;
this.newValue = newValue;
}
/**
* Get the column that was last edited
*
* #return the column that was edited
*/
public int getColumn()
{
return column;
}
/**
* Get the new value in the cell
*
* #return the new value in the cell
*/
public Object getNewValue()
{
return newValue;
}
/**
* Get the old value of the cell
*
* #return the old value of the cell
*/
public Object getOldValue()
{
return oldValue;
}
/**
* Get the row that was last edited
*
* #return the row that was edited
*/
public int getRow()
{
return row;
}
/**
* Get the table of the cell that was changed
*
* #return the table of the cell that was changed
*/
public JTable getTable()
{
return table;
}
//
// Implement the PropertyChangeListener interface
//
#Override
public void propertyChange(PropertyChangeEvent e)
{
// A cell has started/stopped editing
if ("tableCellEditor".equals(e.getPropertyName()))
{
if (table.isEditing())
processEditingStarted();
else
processEditingStopped();
}
}
/*
* Save information of the cell about to be edited
*/
private void processEditingStarted()
{
// The invokeLater is necessary because the editing row and editing
// column of the table have not been set when the "tableCellEditor"
// PropertyChangeEvent is fired.
// This results in the "run" method being invoked
SwingUtilities.invokeLater( this );
}
/*
* See above.
*/
#Override
public void run()
{
row = table.convertRowIndexToModel( table.getEditingRow() );
column = table.convertColumnIndexToModel( table.getEditingColumn() );
oldValue = table.getModel().getValueAt(row, column);
newValue = null;
}
/*
* Update the Cell history when necessary
*/
private void processEditingStopped()
{
newValue = table.getModel().getValueAt(row, column);
// The data has changed, invoke the supplied Action
if ((newValue == null && oldValue != null) || (newValue != null && !newValue.equals(oldValue)))
{
// Make a copy of the data in case another cell starts editing
// while processing this change
CellListenerTableGui tcl = new CellListenerTableGui(
getTable(), getRow(), getColumn(), getOldValue(), getNewValue());
ActionEvent event = new ActionEvent(
tcl,
ActionEvent.ACTION_PERFORMED,
"");
action.actionPerformed(event);
}
}
}
And the following action:
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import javax.swing.Action;
public class UpdateTableListener<N> extends AbstractTableListener implements Action
{
protected boolean enabled;
public UpdateTableListener(AbstractTableGui<N> obs)
{
super(obs);
this.enabled = true;
}
#Override
public void actionPerformed(ActionEvent e)
{
if (null != e && e.getSource() instanceof CellListenerTableGui)
{
TabCellListener tcl = (TabCellListener)e.getSource();
this.obs.getModel().setValueAt(tcl.getNewValue(), tcl.getRow(), tcl.getColumn());
int sel = this.obs.getModel().getNextRequiredColumn(tcl.getRow());
if (sel == -1)
this.obs.getModel().save(tcl.getRow());
}
}
#Override
public void addPropertyChangeListener(PropertyChangeListener arg0)
{
}
#Override
public Object getValue(String arg0)
{
return null;
}
#Override
public boolean isEnabled()
{
return this.enabled;
}
#Override
public void putValue(String arg0, Object arg1)
{
}
#Override
public void removePropertyChangeListener(PropertyChangeListener arg0)
{
}
#Override
public void setEnabled(boolean arg0)
{
this.enabled = arg0;
}
}
This code works well, data are well persisted.
Then I add this code to refresh dependent components:
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import javax.swing.Action;
public class ChangeTableListener implements Action
{
protected AbstractTableGui table;
public ChangeTableListener(AbstractTableGui table)
{
this.table = table;
}
#Override
public void actionPerformed(ActionEvent arg0)
{
this.table.getModel().fireTableDataChanged();
this.table.repaint();
}
#Override
public void addPropertyChangeListener(PropertyChangeListener arg0)
{
}
#Override
public Object getValue(String arg0)
{
return null;
}
#Override
public boolean isEnabled()
{
return false;
}
#Override
public void putValue(String arg0, Object arg1)
{
}
#Override
public void removePropertyChangeListener(PropertyChangeListener arg0)
{
}
#Override
public void setEnabled(boolean arg0)
{
}
}
My TableModel.fireTableDataChanged rebuild JTable content (calle super.fireTableDataChanged and fireTableStructureChanged), JTable.repaint reset the Renderers, and it works for Combobox (forein keys) and it update well title on double-entry tables, but it can't add or delete columns or rows on double-entry tables.
Moreover I see more high latency if there is the slightest change.
My question is simple: how do you manage inter-dependent components?
For your help,
In advance,
Thanks.
Edit :
Here an example of TableCellEditor.
import javax.swing.DefaultCellEditor;
import javax.swing.JTextField;
public class TextColumnEditor extends DefaultCellEditor
{
public TextColumnEditor()
{
super(new JTextField());
}
public boolean stopCellEditing()
{
Object v = this.getCellEditorValue();
if(v == null || v.toString().length() == 0)
{
this.fireEditingCanceled();
return false;
}
return super.stopCellEditing();
}
}
An example of TableModel :
import java.util.ArrayList;
public class GroupModelTable extends AbstractModelTable<Groups>
{
protected GroupsService service;
public GroupModelTable(AbstractTableGui<Groups> content)
{
super(content, new ArrayList<String>(), new ArrayList<Groups>());
this.headers.add("Group");
this.content.setModel(this);
this.service = new GroupsService();
this.setLines(this.service.search(new Groups()));
}
public Object getValueAt(int rowIndex, int columnIndex)
{
switch (columnIndex)
{
case 0:
return this.lines.get(rowIndex).getTitle();
default:
return "";
}
}
public void setValueAt(Object aVal, int rowIndex, int columnIndex)
{
switch (columnIndex)
{
case 0:
this.lines.get(rowIndex).setTitle(aVal.toString());
break;
default:
break;
}
}
#Override
public Groups getModel(int line, int column)
{
return null;
}
#Override
public Groups getModel(int line)
{
return this.lines.get(line);
}
public boolean isCellEditable(int row, int column)
{
return true;
}
#Override
public GroupModelTableGui newLine()
{
this.lines.add(new Groups());
return this;
}
#Override
public int getNextRequiredColumn(int row)
{
Groups g = this.getModel(row);
if (g != null && g.getTitle() != null && g.getTitle().length() > 0)
return -1;
return 0;
}
#Override
public void save(int row)
{
Groups g = this.getModel(row);
if (g != null)
{
try
{
if (g.getId() == null)
this.service.create(g);
else
this.service.update(g);
}
catch (Exception e)
{
}
}
}
#Override
public void removeRow(int row)
{
Groups g = this.getModel(row);
if (g != null)
{
try
{
if (g.getId() != null)
this.service.delete(g);
super.removeRow(row);
}
catch (Exception e)
{
}
}
}
}
An example of Table :
public class GroupTable extends AbstractTable<Groups>
{
public GroupTable()
{
new GroupModelTableGui(this);
new CellListenerTableGui(this.getContent(), new UpdateTableListenerGui<Groups>(this));
this.getContent().getColumnModel().getColumn(0).setCellEditor(new TextColumnEditorGui());
}
}
I hope it will help you to understand :/
Your TabCellListener is unfamiliar to me. Your TableCellEditor should not interact with the TableModel directly. It should implement getTableCellEditorComponent() and getCellEditorValue(), as shown in this example. When the editor concludes, the model will have the new value. Your TableModel should handle persistence. More than one view may listen to a single TableModel via TableModelListener.
Addendum: Your CellEditor, TextColumnEditor, probably shouldn't invoke fireEditingCanceled(). Pressing Escape should be sufficient to revert the edit, as shown in this example. You might also look at the related tutorial section and example.
In windows it is possible to show a grayed out JCheckbox, to show that the collection of data which it represents not all items have the same value.
Is this even possible with a JCheckBox?
How do i go about this?
(Hoping there's a way to not override it)
Thanks
JIDE Common Layer has a TristateCheckBox.
It's possible with some of work.
I have this code from some years ago. Is based in some examples I found in internet, but I cannot find any reference to the original creator, so I apologize
import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.ActionMapUIResource;
import java.awt.event.*;
/**
* Maintenance tip - There were some tricks to getting this code
* working:
*
* 1. You have to overwite addMouseListener() to do nothing
* 2. You have to add a mouse event on mousePressed by calling
* super.addMouseListener()
* 3. You have to replace the UIActionMap for the keyboard event
* "pressed" with your own one.
* 4. You have to remove the UIActionMap for the keyboard event
* "released".
* 5. You have to grab focus when the next state is entered,
* otherwise clicking on the component won't get the focus.
* 6. You have to make a TristateDecorator as a button model that
* wraps the original button model and does state management.
*/
public class TristateCheckBox extends JCheckBox {
/** This is a type-safe enumerated type */
public static class State { private State() { } }
public static final State NOT_SELECTED = new State();
public static final State SELECTED = new State();
public static final State DONT_CARE = new State();
private final TristateDecorator model;
public TristateCheckBox(String text, Icon icon, State initial){
super(text, icon);
// Add a listener for when the mouse is pressed
super.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
grabFocus();
model.nextState();
}
});
// Reset the keyboard action map
ActionMap map = new ActionMapUIResource();
map.put("pressed", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
grabFocus();
model.nextState();
}
});
map.put("released", null);
SwingUtilities.replaceUIActionMap(this, map);
// set the model to the adapted model
model = new TristateDecorator(getModel());
setModel(model);
setState(initial);
}
public TristateCheckBox(String text, State initial) {
this(text, null, initial);
}
public TristateCheckBox(String text) {
this(text, DONT_CARE);
}
public TristateCheckBox() {
this(null);
}
/** No one may add mouse listeners, not even Swing! */
public void addMouseListener(MouseListener l) { }
/**
* Set the new state to either SELECTED, NOT_SELECTED or
* DONT_CARE. If state == null, it is treated as DONT_CARE.
*/
public void setState(State state) { model.setState(state); }
/** Return the current state, which is determined by the
* selection status of the model. */
public State getState() { return model.getState(); }
public void setSelected(boolean b) {
if (b) {
setState(SELECTED);
} else {
setState(NOT_SELECTED);
}
}
/**
* Exactly which Design Pattern is this? Is it an Adapter,
* a Proxy or a Decorator? In this case, my vote lies with the
* Decorator, because we are extending functionality and
* "decorating" the original model with a more powerful model.
*/
private class TristateDecorator implements ButtonModel {
private final ButtonModel other;
private TristateDecorator(ButtonModel other) {
this.other = other;
}
private void setState(State state) {
if (state == NOT_SELECTED) {
other.setArmed(false);
setPressed(false);
setSelected(false);
} else if (state == SELECTED) {
other.setArmed(false);
setPressed(false);
setSelected(true);
} else { // either "null" or DONT_CARE
other.setArmed(true);
setPressed(true);
setSelected(true);
}
}
/**
* The current state is embedded in the selection / armed
* state of the model.
*
* We return the SELECTED state when the checkbox is selected
* but not armed, DONT_CARE state when the checkbox is
* selected and armed (grey) and NOT_SELECTED when the
* checkbox is deselected.
*/
private State getState() {
if (isSelected() && !isArmed()) {
// normal black tick
return SELECTED;
} else if (isSelected() && isArmed()) {
// don't care grey tick
return DONT_CARE;
} else {
// normal deselected
return NOT_SELECTED;
}
}
/** We rotate between NOT_SELECTED, SELECTED and DONT_CARE.*/
private void nextState() {
State current = getState();
if (current == NOT_SELECTED) {
setState(SELECTED);
} else if (current == SELECTED) {
setState(DONT_CARE);
} else if (current == DONT_CARE) {
setState(NOT_SELECTED);
}
}
/** Filter: No one may change the armed status except us. */
public void setArmed(boolean b) {
}
/** We disable focusing on the component when it is not
* enabled. */
public void setEnabled(boolean b) {
setFocusable(b);
other.setEnabled(b);
}
/** All these methods simply delegate to the "other" model
* that is being decorated. */
public boolean isArmed() { return other.isArmed(); }
public boolean isSelected() { return other.isSelected(); }
public boolean isEnabled() { return other.isEnabled(); }
public boolean isPressed() { return other.isPressed(); }
public boolean isRollover() { return other.isRollover(); }
public void setSelected(boolean b) { other.setSelected(b); }
public void setPressed(boolean b) { other.setPressed(b); }
public void setRollover(boolean b) { other.setRollover(b); }
public void setMnemonic(int key) { other.setMnemonic(key); }
public int getMnemonic() { return other.getMnemonic(); }
public void setActionCommand(String s) {
other.setActionCommand(s);
}
public String getActionCommand() {
return other.getActionCommand();
}
public void setGroup(ButtonGroup group) {
other.setGroup(group);
}
public void addActionListener(ActionListener l) {
other.addActionListener(l);
}
public void removeActionListener(ActionListener l) {
other.removeActionListener(l);
}
public void addItemListener(ItemListener l) {
other.addItemListener(l);
}
public void removeItemListener(ItemListener l) {
other.removeItemListener(l);
}
public void addChangeListener(ChangeListener l) {
other.addChangeListener(l);
}
public void removeChangeListener(ChangeListener l) {
other.removeChangeListener(l);
}
public Object[] getSelectedObjects() {
return other.getSelectedObjects();
}
}
}
My colleague who this question came from thought of this;
Create a dummy JCheckBox which is disabled and selected. set the same size as the real one.
Create an Icon which' paint method actually paints the dummy JCheckbox.
Set the original JCheckBox' Icon to the one painting the dummy.
Remove the icon as soon as the JCheckBox is clicked.
++ No overridden JCheckBox
-- not a real tri-state Combo
I think he's satisfied.
Thanks for the help