Jon Swanson
Jon Swanson

Reputation: 324

Combining 'Excel-like' behavior and updating the model when focus leaves my JTable

I had a request for JTable functionality that would allow data entry more like in Excel. If the user types in a cell, the previous value is replaced by the new value and one can navigate between rows and columns using arrow keys.

I found several solutions to this problem and implemented one.

I was then given an additional requirement- if the user clicks outside the table, the cell that data was being entered into is updated (editing stops).

I suspect I am just missing something, but my attempts to implement both requirements have failed.

  1. I have a custom cell editor that listens for focusLost ans stops cell editing. This works if I double click in the table and then click outside the table. But if I just typed a number in a cell, this method doesn't run.

  2. tableMe.setSurrendersFocusOnKeystroke(true); This a) results in my losing the first keystroke and b) puts me in the editor, so I can;t used the right and left arrow keys to move out the cell.

  3. I added a FocusListener on the table. Then, I can just type a number in a cell and navigate to another cell, but I can never get into the editor. A double-click immediately stops the edit. I thought I could find some difference between when I was 'really' editing (have a cursor in the editor window) and when I have just typed a number in the cell. What I have tried so far (isEditing() always comes up true, hasFocus() always comes up false) does not work.

Can anyone point me a solution for this? My attempts are below. I commented out the false starts, but they are there so you can see what I tried.

import java.io.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;
import java.text.NumberFormat;
import javax.swing.text.JTextComponent;
import javax.swing.border.*;
import javax.swing.BorderFactory;
import javax.swing.border.Border;

class ExcelTableTest extends JFrame
{
    public static final Color activeDark  = new Color(230, 185, 184);
    public static final Color activeLight = new Color(244, 233, 233);

    private MyJTable      tableMe;
    private MyTableModel  modelMe;
    private JTextField    textMe;
    private JScrollPane   scrollTable;

    private String[]   tableHeaders = { "Column 1", "Column 2" };
    private Object[][] tableData    = new Object[ tableHeaders.length ][ 4 ];

    ExcelTableTest()
    {
        setLayout( new BorderLayout() );
        modelMe  = new MyTableModel(tableData, tableHeaders );
        tableMe  = new MyJTable(modelMe);
        tableMe.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        TableColumn column = null;
        for (int i = 0; i < tableHeaders.length; i++)
        {
           column = tableMe.getColumnModel().getColumn(i);
           column.setCellRenderer( new MyCellRenderer()   );
           column.setCellEditor(   new NumberCellEditor() );
        }
        tableMe.setRowSelectionAllowed(false);
//      tableMe.addFocusListener( new MyFocusListener() );
//      tableMe.setSurrendersFocusOnKeystroke(true);

//      modelMe.addTableModelListener(new MyTableModelListener());
        scrollTable = new JScrollPane(tableMe);
        scrollTable.setVerticalScrollBarPolicy(
            ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
        scrollTable.setHorizontalScrollBarPolicy(
            ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        scrollTable.setPreferredSize(new Dimension(500,150));

        textMe = new JTextField( 6 );

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        setSize(500,210);
        add(BorderLayout.CENTER, scrollTable );
        add(BorderLayout.SOUTH,  textMe );
        setVisible(true);
    }

    class MyJTable extends JTable
    {
       MyJTable( TableModel model)
       {
          super(model);
       }
//
// Trying to get cells selected when editing starts
       @Override
       public Component prepareEditor(TableCellEditor editor,
                                         int row, int column)
       {
          Component editComponent = super.prepareEditor(editor, row, column);
          if (editComponent instanceof JTextComponent)
          {
             System.out.println( "In prepare editor" );
             ((JTextComponent) editComponent).selectAll();
//           System.out.println( "Requesting Focus" );
//           System.out.println( 
//              ((JTextComponent) editComponent).isFocusable());
//           System.out.println( 
//              ((JTextComponent) editComponent).requestFocusInWindow());
          }
          return editComponent;
       }    
//
// This also works, but end result is the same- when using this, the editor
// does not have focus, so I cannot update the cell just by switching focus
// to another part of the GUI.
//     @Override
//     public boolean editCellAt( int row, int col, EventObject e )
//     {
//        boolean result = super.editCellAt( row, col, e );
//
//        Component editComponent = getEditorComponent();
//        if (  editComponent == null || 
//            !(editComponent instanceof JTextComponent) )
//           return result;
//        if (e instanceof KeyEvent)
//           ((JTextComponent) editComponent).selectAll();
//
//        return result;
//     }
//
    }

    class MyFocusListener implements FocusListener
    {
       @Override
       public void focusGained(FocusEvent e)
       {
          System.out.println("Table has focus");
       }

       @Override
       public void focusLost(FocusEvent e)
       {
          System.out.println("Table lost focus");
          MyJTable        table       = (MyJTable) e.getSource();
          int[]           columns     = table.getSelectedColumns();
//
// I will not be allowing multiple column selection
//
// This is still a problem, as the table IS editing, even though the editor
// does not seem to have focus. This is true whether I am in the case where
// I really have the editor going (in which case the lostFocus of the editor
// nicely cleans things up) or the editor is not really going, and then the
// lostFocus of the editor never triggers.
//        if ( columns.length > 0 && ! table.isEditing() )
          if ( columns.length > 0 )
          {
             TableColumn       tableColumn = table.getColumnModel()
                                                  .getColumn(columns[0]);
             DefaultCellEditor cellEditor  = (DefaultCellEditor)
                                              tableColumn.getCellEditor();
// Does not have focus even when I double click to edit the cell
             if (cellEditor != null && ! cellEditor.getComponent().hasFocus() );
             {
                System.out.println( "Editor focus: " + 
                                     cellEditor.getComponent().hasFocus() );
                if (!cellEditor.stopCellEditing())
                {
                   cellEditor.cancelCellEditing();
                }
             }
          }
       }
    }

    class MyTableModel extends DefaultTableModel
    {

       public MyTableModel(Object rowData[][], Object columnNames[]) {
           super(rowData, columnNames);
       }

       @Override
       public Class getColumnClass(int col) {
          return Double.class;
       }

       @Override
       public boolean isCellEditable(int row, int col)
       {
          return true;
       }
    }

    class MyCellRenderer extends DefaultTableCellRenderer
    {
        public Component getTableCellRendererComponent(
          JTable table, Object value, boolean selected, boolean focus,
          int row, int col) {

          Component renderComponent = super.getTableCellRendererComponent(
             table, value, selected, focus, row, col);
//
// This is called constantly 
//        System.out.println( "In renderer" );

          if ((row % 2) == 0) {
             renderComponent.setBackground(activeDark);
          } else {
             renderComponent.setBackground(activeLight);
          }

          NumberFormat nf = NumberFormat.getInstance();
          nf.setMinimumFractionDigits(0);

          if (value != null)
             System.out.println( "Value: " + value + "; " + value.getClass() );
          super.setText((value == null) ? "" : nf.format(value));
          super.setHorizontalAlignment( SwingConstants.RIGHT );

          return renderComponent;
       }
    }

    class NumberCellEditor extends DefaultCellEditor
    {
       private Double minimum = 0.0;
       private Double value   = null;
       private final  JTextField textField;
       private final  NumberFormat nf = NumberFormat.getInstance();

       public NumberCellEditor()
       {
          super(new JTextField());
          textField = (JTextField) getComponent();
          nf.setMinimumFractionDigits(0);
          textField.setHorizontalAlignment(JTextField.RIGHT);
          textField.addFocusListener(new FocusListener()
          {
             @Override
             public void focusGained(FocusEvent e)
             {
                System.out.println( "In editor" );
                if (value != null)
                   textField.setText(nf.format(value.doubleValue()));
                else
                   textField.setText("");
                textField.setCaretPosition(0);
             }

             @Override
             public void focusLost(FocusEvent e)
             {
                System.out.println( "Lost focus editing cell" );
                if (!stopCellEditing()) cancelCellEditing();
             }
           });
        }

        public boolean stopCellEditing()
        {
          String s = (String)super.getCellEditorValue();
          System.out.println( "Stop Cell editing: " + value );
          if ("".equals(s))
          {
             value = null;
             super.stopCellEditing();
          }
          try
          {
              value = nf.parse(s).doubleValue();
          }
          catch (Exception e)
          {
             textField.setBorder(new LineBorder(Color.red));
             return false;
          }
          return super.stopCellEditing();
       }

       public Component getTableCellEditorComponent(JTable table, Object value,
                                                    boolean isSelected,
                                                    int row, int col)
       {
          this.value = (Double) value;
          textField.setBorder(new LineBorder(Color.black));
          return super.getTableCellEditorComponent(
                          table, value, isSelected, row, col);
       }

       public Object getCellEditorValue()
       {
          value = (Double) value;
          return value;
       }
    }

    public static void main( String[] args )
    {
        ExcelTableTest excelTest = new ExcelTableTest();
    }
}

Here is the simple solution described below. It only requires one line-

        tableMe.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);

If I start typing in a cell, then mouse click in the cell, I go into the editor and lose what was being typed. If I click anywhere else, what was being typed is saved and the editor is stopped.

What I had tried was to define a flag-

   private boolean       reallyInEditor = true;

then override editCellAt rather than prepare editor

       @Override
       public boolean editCellAt( int row, int col, EventObject e )
       {
          boolean result = super.editCellAt( row, col, e );

          Component editComponent = getEditorComponent();
          if (  editComponent == null || 
              !(editComponent instanceof JTextComponent) )
             return result;
          if (e instanceof KeyEvent)
          {
             ((JTextComponent) editComponent).selectAll();
             reallyInEditor = false;
         }

          return result;
       }
    }

to set my flag to indicate the user entered the editor by typing rather than a double-click

and put a FocusListener on the table

        tableMe.addFocusListener( new MyFocusListener() );

    class MyFocusListener implements FocusListener
    {
       @Override
       public void focusGained(FocusEvent e)
       {
          System.out.println("Table has focus");
       }

       @Override
       public void focusLost(FocusEvent e)
       {
          System.out.println("Table lost focus");
          MyJTable        table       = (MyJTable) e.getSource();
          int[]           columns     = table.getSelectedColumns();
//
// I will not be allowing multiple column selection
          if ( columns.length > 0 )
          {
             TableColumn       tableColumn = table.getColumnModel()
                                                  .getColumn(columns[0]);
             DefaultCellEditor cellEditor  = (DefaultCellEditor)
                                              tableColumn.getCellEditor();
             if (cellEditor != null && !reallyInEditor)
             {
                System.out.println( "Editor focus: " + 
                                     cellEditor.getComponent().hasFocus() );
                if (!cellEditor.stopCellEditing())
                {
                   cellEditor.cancelCellEditing();
                }
                reallyInEditor = true;
             }
          }
       }
    }

the difference from all this extra work, is that if I start typing in a cell, then click in the cell, the cell is updated with the value I was entering and the editor is stopped.

Upvotes: 2

Views: 1324

Answers (1)

Vishal K
Vishal K

Reputation: 13066

Use the following line of code to achieve what you are looking for:

tableMe.putClientProperty("terminateEditOnFocusLost",Boolean.TRUE);

Upvotes: 4

Related Questions