1

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):

attributes-in-action

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
aterai
  • 9,658
  • 4
  • 35
  • 44
DejanLekic
  • 18,787
  • 4
  • 46
  • 77
  • 1
    Not sure if will entirely solve your problem but see [Key bindings vs. Key Listeners](http://stackoverflow.com/a/15290065/1795530). – dic19 Jul 21 '14 at 11:27
  • 1
    1. for why hells is there Focus & KeyListener for JLabel, 2. paintComponent inside CellRenderer is quite quite often called inside JViewport, 3. whats real goal, 4. (most important thing at the end) remove licence for code on public forums, is un_logical and contraproductive – mKorbel Jul 21 '14 at 11:28
  • 1
    use BoxLayout instead of FlowLayout (resize:-) – mKorbel Jul 21 '14 at 11:29
  • I had some problems with key bindings even with just the AttributesEditableView and AttributesEditor (on a simple form, not table), so I decided to use key listener... Maybe I should re-evaluate this decision. Thanks. – DejanLekic Jul 21 '14 at 11:29
  • 2
    maybe use combinations of (custom)JRadioButtons for renderer and JComboBox as editor – mKorbel Jul 21 '14 at 11:31
  • @mKorbel: I do not have resizing problems, attributes may even be of the fixed width as far as we are concerned. JLabels only have mouse listener added, so I could shade/unshade the one that is below the mouse pointer. About license, I do not want people MSGing me about license of the code. – DejanLekic Jul 21 '14 at 11:33
  • @mKorbel , custom JRadioButtons idea is not bad. I will think about it. – DejanLekic Jul 21 '14 at 11:34
  • 1
    I posted similair code (JRadioButtons&JComboBox)in SSCCE/MCVE form a few times here – mKorbel Jul 21 '14 at 11:44
  • For [example](http://stackoverflow.com/a/11179669/230513). – trashgod Jul 21 '14 at 13:09

0 Answers0