Recently I have created two very simple, yet very useful components called AttributesEditableView and AttributesEditor. AttributesEditableView is designed to just show arbitrary number of attributes, while the AttributesEditor makes it possible to edit attributes.
So, say we have three attributes that can take values 'Y', 'N' and '?' (where by '?' we mean "unspecified"). The view looks something like [[Y][N][?]]
:
In the editor, user can use arrow-keys to jump left-right between attributes, and by pressing the SPACE key toggle the value of particular (currently selected) attribute.
(I guess those of you who are familiar with JTable
and its peculiarities already understand when am I getting to right now).
It all works well in a form, but when I want to make a TableCellEditor
out of this AttributesEditor, I get stuck, because JTable
either "eats" all events, or there is something that I am doing wrong so my AttributesEditor simply does not get events... :(
My logic is the following: how come DefaultCellEditor
when the editor component is JTextField
gets those events (left-key or right-key presses for example), while my cell-editor does not?
I know I am not giving all the classes here, but I believe the two classes below should give those who have already encountered this problem enough information to be able to tell me how to make this cell editor work as expected?
UPDATE: I managed to fix the problem by refactoring the AttributesEditor class. Now it uses key-bindings instead of the KeyListener
and everything works as expected. Lesson learned...
You can see the AttributesEditableView, AttributesEditor and AttributesCellEditor in action on the following screen shot (I also have the renderer, but could not bother to put it on, it looks like AttributesEditableView anyway):
In the screenshot above, first attribute may have 'Y', 'N' and '?' values, second one 'A', 'B', 'C', and 'D', and the third attribute may have 'T' and 'F' as values.
Here are the classes:
AttributesCellEditor
/**
* Authors: Dejan Lekic , http://dejan.lekic.org
* License: MIT
*/
public class AttributesCellEditor extends AbstractCellEditor
implements TableCellEditor, TreeCellEditor {
private AttributesEditor attributesEditor;
private AttributesColumnModel attrColModel;
private AttributesModel model;
private int clickCountToStart = 2;
DefaultCellEditor dfe;
public AttributesCellEditor(AttributesColumnModel argModel) {
super();
attrColModel = argModel;
model = new AttributesModel(attrColModel.getAllowedValues());
model.setDefaultValues(attrColModel.getDefaultValues());
attributesEditor = new AttributesEditor(model);
attributesEditor.setBorder(new BevelBorder(BevelBorder.LOWERED));
}
// ::::: CellEditor method implementations :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
@Override
public Object getCellEditorValue() {
System.out.println("getCellEditorValue()");
// before we return the value, we should also modify the attributes column model and set the
// selected attribute index to the index of what we last edited.
attrColModel.setSelectedAttributeIndex(model.getSelectedAttributeIndex());
return model.getAttributes();
}
// ::::: TableCellEditor method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row,
int column) {
String val = (value == null) ? "" : value.toString();
model.setAttributes(val);
model.setSelectedAttributeIndex(attrColModel.getSelectedAttributeIndex());
attributesEditor.setModel(model);
return attributesEditor;
}
// ::::: TreeCellEditor method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// TODO: implement these
@Override
public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected,
boolean expanded, boolean leaf, int row) {
//To change body of generated methods, choose Tools | Templates.
throw new UnsupportedOperationException("Not supported yet.");
}
// ::::: AbstractCellEditor method overrides ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
@Override
public boolean isCellEditable(EventObject e) {
if (e instanceof MouseEvent) {
return ((MouseEvent) e).getClickCount() >= clickCountToStart;
}
return true;
}
/**
* {@inheritDoc}
* @return
*/
@Override
public boolean stopCellEditing() {
boolean ret = super.stopCellEditing();
return ret;
}
} // AttributesCellEditor class
AttributesEditor
/**
* Authors: Dejan Lekic , http://dejan.lekic.org
* License: MIT
*/
public class AttributesEditor
extends AttributesEditableView
implements PropertyChangeListener, FocusListener, KeyListener {
public AttributesEditor() {
super();
editorComponent = true;
setFocusable(true);
setBackground(Color.white);
// Let's copy the border from the TextField
Border b = (Border) UIManager.getLookAndFeelDefaults().get("TextField.border");
setBorder(b);
// Let's copy the insets from the TextField
Insets insets = (Insets) UIManager.getLookAndFeelDefaults().get("TextField.margin");
getInsets().set(insets.top, insets.left, insets.bottom, insets.right);
addKeyListener(this);
// listeners...
addFocusListener(this);
}
public AttributesEditor(AttributesModel argModel) {
this();
setOpaque(true);
setFocusable(true);
setBackground(Color.white);
repaint();
setModel(argModel);
}
// :::: PropertyChangeListener method implementations :::::::::::::::::::::::::::::::::::::::::::::::::
@Override
public void propertyChange(PropertyChangeEvent evt) {
String str = (String) evt.getNewValue();
System.out.println("CALL: AttributesEditor.propertyChange() : " + str);
updateView();
}
// :::: FocusListener method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
@Override
public void focusGained(FocusEvent e) {
int sel = model.getSelectedAttributeIndex();
if (sel < 0) {
model.setSelectedAttributeIndex(0);
}
sel = model.getSelectedAttributeIndex();
labels[sel].setBackground(model.getSelectedBackgroundColor());
}
@Override
public void focusLost(FocusEvent e) {
model.setSelectedAttributeIndex(-1);
updateView();
}
// :::: KeyListener method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
@Override
public void keyTyped(KeyEvent e) {
// Weird thing is, arrow keys do not trigger keyTyped() to be called,
// so we have to use keyPressed here, or keyReleased
}
/**
* Weird thing is, arrow keys do not trigger keyTyped() to be called, so we have to use keyPressed/keyReleased here.
* @param e
*/
@Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
switch (key) {
case KeyEvent.VK_UP:
model.previous(model.getSelectedAttributeIndex());
break;
case KeyEvent.VK_DOWN:
case KeyEvent.VK_SPACE:
model.next(model.getSelectedAttributeIndex());
break;
case KeyEvent.VK_LEFT:
model.previousAttribute();
updateView();
break;
case KeyEvent.VK_RIGHT:
model.nextAttribute();
updateView();
break;
default:
int idx = model.getSelectedAttributeIndex();
char chr = Character.toUpperCase(e.getKeyChar());
if (model.isValid(idx, chr)) {
model.setValue(idx, chr);
}
} // switch
} // keyPressed() method
@Override
public void keyReleased(KeyEvent e) {
// nothing
}
} // AttributesEditor class
AttributesEditableView
/**
* Authors: Dejan Lekic , http://dejan.lekic.org
* License: MIT
*/
public class AttributesEditableView
extends JComponent
implements PropertyChangeListener, MouseListener {
protected AttributesModel model;
protected JLabel[] labels;
protected boolean editorComponent;
protected boolean inTable;
public AttributesEditableView() {
super();
FlowLayout fl = new FlowLayout(FlowLayout.LEFT);
fl.setVgap(0);
setLayout(fl);
editorComponent = false;
inTable = false;
}
public AttributesEditableView(AttributesModel argModel) {
this();
setModel(argModel);
}
// :::: JComponent method overrides :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
/**
* I had to do this because it is a common problem with JComponent subclasses.
* More about it: http://docs.oracle.com/javase/tutorial/uiswing/painting/problems.html
*
* @param g
*/
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(getForeground());
}
// :::: PropertyChangeListener mthod implementations ::::::::::::::::::::::::::::::::::::::::::::::::::
@Override
public void propertyChange(PropertyChangeEvent evt) {
String str = (String) evt.getNewValue();
updateView();
}
// :::: <Interface> method implementations ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
@Override
public void mouseClicked(MouseEvent e) {
// labels have names in form of "attributelbl-3", we need to extract the ID from the name
String num = e.getComponent().getName().split("-")[1];
int idx = Integer.parseInt(num);
if (editorComponent) {
model.setSelectedAttributeIndex(idx);
}
model.next(idx);
} // mouseClicked() method
@Override
public void mousePressed(MouseEvent e) {
// do nothing
}
@Override
public void mouseReleased(MouseEvent e) {
// do nothing
}
@Override
public void mouseEntered(MouseEvent e) {
// labels have names in form of "attributelbl-3", we need to extract the ID from the name
String num = e.getComponent().getName().split("-")[1];
int idx = Integer.parseInt(num);
model.setSelectedAttributeIndex(idx);
updateView();
}
@Override
public void mouseExited(MouseEvent e) {
// labels have names in form of "attributelbl-3", we need to extract the ID from the name
String num = e.getComponent().getName().split("-")[1];
int idx = Integer.parseInt(num);
if (!editorComponent) {
model.setSelectedAttributeIndex(-1);
}
updateView();
}
/**
* Use this method to forcefully highlight specific attribute.
* @param argIndex
*/
public final void highlight(int argIndex) {
for (int i = 0; i < model.getNumberOfAttributes(); i++) {
if (i == argIndex) {
labels[i].setBackground(model.getSelectedBackgroundColor());
} else {
labels[i].setBackground(model.getBackgroundColor());
}
} // for
} // highlight() method
/**
* Extremely important method. Here we set the model, and generate JLabel objects for the attributes.
* We also set the values, and initialise the listeners.
* @param argModel
*/
public final void setModel(AttributesModel argModel) {
if (model != null) {
model.removePropertyChangeListener(this);
}
labels = null;
removeAll();
labels = new JLabel[argModel.getNumberOfAttributes()];
int w = 0;
int h = 0;
for (int i = 0; i < argModel.getNumberOfAttributes(); i++) {
String txt = "" + argModel.getValue(i);
System.out.println(txt);
JLabel lbl = new JLabel(txt);
labels[i] = lbl;
lbl.setName("attributelbl-" + i); // very important, because we will use the name to get the idx
lbl.setHorizontalAlignment(SwingConstants.CENTER);
lbl.setOpaque(true);
lbl.setBackground(argModel.getBackgroundColor());
if (isInTable()) {
lbl.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, Color.GRAY));
}
int nh = lbl.getPreferredSize().height;
int nw = nh; // make the width to be equal to the height
lbl.setPreferredSize(new Dimension(nw, nh));
lbl.addMouseListener(this);
h = Math.max(h, lbl.getPreferredSize().height);
w = w + lbl.getPreferredSize().width;
add(lbl);
} // for
Dimension ps = new Dimension(w, h);
model = argModel;
model.addPropertyChangeListener(this);
} // setModel() method
public final AttributesModel getModel() {
return model;
}
public boolean isInTable() {
return inTable;
}
public void setInTable(boolean inTable) {
this.inTable = inTable;
}
/**
* Updates the view
*/
protected void updateView() {
String attrs = model.getAttributes();
for (int i = 0; i < model.getNumberOfAttributes(); i++) {
labels[i].setText("" + model.getValue(i));
if (model.getSelectedAttributeIndex() == i) {
labels[i].setBackground(model.getSelectedBackgroundColor());
} else {
labels[i].setBackground(model.getBackgroundColor());
}
}
}
} // AttributesEditableView class