JTextArea-Dialog as JTable-CellEditor misses first typed character
Asked Answered
J

1

1

We need a CellEditor for a JTable for editing a large multiline-text. We tried using a popup visually extending the TableCell, which was overlapping the cells to the right and bottom. This led to various problems if the cell was in the right bottom corner, near the screen-boundaries, etc.

Then we decided to use a modal JDialog for editing the cell-value. So the users could move the dialog around, and we could persist its size and position.

Now the problems started ;-)

We are not able to "forward" the first typed character to the Dialog. There are many examples on stack overflow, where this problem is solved for custom CellEditors which are displayed directly in the Table(Cell), for example here: Losing first character in JTable panel based cell editor

The following SSCCE (from camickrs answer: https://mcmap.net/q/541821/-jtable-design-to-synchronize-with-back-end-data-structure) shows that the first keystroke in the second TableColumn gets lost most of the time.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Frame;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.DefaultCellEditor;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;

/**
 * Example taken from this answer: https://mcmap.net/q/541821/-jtable-design-to-synchronize-with-back-end-data-structure
 * 
 * @author camickr
 */
public class TablePopupEditor extends DefaultCellEditor
{
  private PopupDialog popup;
  private String      currentText = "";
  private JButton     editorComponent1;

  public TablePopupEditor()
  {
    super( new JTextField() );

    setClickCountToStart( 2 );

    //  Use a JButton as the editor component
    editorComponent1 = new JButton();
    editorComponent1.setBackground( Color.white );
    editorComponent1.setBorderPainted( false );
    editorComponent1.setContentAreaFilled( false );

    //  Set up the dialog where we do the actual editing
    popup = new PopupDialog();
  }

  @Override
  public Object getCellEditorValue()
  {
    return currentText;
  }

  @Override
  public Component getTableCellEditorComponent( JTable table, Object value, boolean isSelected, int row, int column )
  {

    SwingUtilities.invokeLater( new Runnable()
    {
      @Override
      public void run()
      {
        System.out.println( "run" );
        popup.setText( currentText );
        Point p = editorComponent1.getLocationOnScreen();
        popup.setLocation( p.x, p.y + editorComponent1.getSize().height );
        popup.setVisible( true );
        fireEditingStopped();
      }
    } );

    currentText = value.toString();
    editorComponent1.setText( currentText );
    return editorComponent1;
  }

  /*
  *   Simple dialog containing the actual editing component
  */
  class PopupDialog extends JDialog implements ActionListener
  {
    private JTextArea textArea;

    public PopupDialog()
    {
      super( (Frame) null, "Change Description", true );

      textArea = new JTextArea( 5, 20 );
      textArea.setLineWrap( true );
      textArea.setWrapStyleWord( true );
      KeyStroke keyStroke = KeyStroke.getKeyStroke( "ENTER" );
      textArea.getInputMap().put( keyStroke, "none" );
      JScrollPane scrollPane = new JScrollPane( textArea );
      getContentPane().add( scrollPane );

      JButton cancel = new JButton( "Cancel" );
      cancel.addActionListener( this );
      JButton ok = new JButton( "Ok" );
      ok.setPreferredSize( cancel.getPreferredSize() );
      ok.addActionListener( this );

      JPanel buttons = new JPanel();
      buttons.add( ok );
      buttons.add( cancel );
      getContentPane().add( buttons, BorderLayout.SOUTH );
      pack();

      getRootPane().setDefaultButton( ok );
    }

    public void setText( String text )
    {
      textArea.setText( text );
    }

    /*
    *   Save the changed text before hiding the popup
    */
    @Override
    public void actionPerformed( ActionEvent e )
    {
      if ( "Ok".equals( e.getActionCommand() ) )
      {
        currentText = textArea.getText();
      }

      textArea.requestFocusInWindow();
      setVisible( false );
    }
  }

  public static void main( String[] args )
  {
    String[] columnNames = { "Item", "Description" };
    Object[][] data =
        { { "Item 1", "Description of Item 1" }, { "Item 2", "Description of Item 2" }, { "Item 3", "Description of Item 3" } };

    JTable table = new JTable( data, columnNames );
    table.getColumnModel().getColumn( 1 ).setPreferredWidth( 300 );
    table.setPreferredScrollableViewportSize( table.getPreferredSize() );
    JScrollPane scrollPane = new JScrollPane( table );

    // Use the popup editor on the second column
    TablePopupEditor popupEditor = new TablePopupEditor();
    table.getColumnModel().getColumn( 1 ).setCellEditor( popupEditor );

    JFrame frame = new JFrame( "Popup Editor Test" );
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
    frame.getContentPane().add( scrollPane );
    frame.pack();
    frame.setLocationRelativeTo( null );
    frame.setVisible( true );
  }
}

Is there a reliable way to catch the first character?

Juan answered 9/5, 2018 at 15:15 Comment(0)
H
1

I can't even figure out how the character ever gets appended to the text area. I tried about 200 times invoking the editor and it only appeared once. So there is obviously some timing issue. Random issues like this are usually a sign of code not executing on the EDT.

In any case I came up with a work around:

public Component getTableCellEditorComponent(
    JTable table, Object value, boolean isSelected, int row, int column)
{
    AWTEvent event = EventQueue.getCurrentEvent();

    SwingUtilities.invokeLater(new Runnable()
    {
        public void run()
        {
            String append = "";

            if (event.getID() == KeyEvent.KEY_PRESSED)
            {
                KeyEvent ke = (KeyEvent)event;
                String keyText = ke.getKeyText(ke.getKeyCode());

                if (keyText.length() == 1)
                    append += ke.getKeyChar();
            }

            popup.setText(currentText + append);
            //popup.setLocationRelativeTo( editorComponent );
            Point p = editorComponent.getLocationOnScreen();
            popup.setLocation(p.x, p.y + editorComponent.getSize().height);
            popup.show();
            fireEditingStopped();
        }
    });

    currentText = value.toString();
    editorComponent.setText( currentText );
    return editorComponent;
}

The above code saves the event that was used to invoke the editor. So when the popup is displayed it can check for a key event and get the character that was pressed.

Hamish answered 9/5, 2018 at 16:31 Comment(1)
The problem with this solution is, that the first given character sometimes appears twice, since as already stated inside of the question, the first keypress sometimes goes through. In case of such situation we manually append the character and also get it thorugh the keypress.Pectoral

© 2022 - 2024 — McMap. All rights reserved.