JTable display cell previous/last selected/edited/clicked value/JComponent

I have a problem with the minesweeper I made,

The game board is a JTable containing custom gadgets in the cells,

The first game goes without problem, but I press the JButton starting a new game,

I randomly refill the JTable with new custom gadgets,

My problem is that the last cell I clicked in the JTable is still in the clicked state with the previous value and I cannot click or see the new custom gadget that ought to be there ...

The custom gadgets extends JLabel and use custom AbstractCellEditor and custom TableCellRenderer, but I think it is not related to my custom gadgets,

I work on OSX,

Any hint to my problem's origin?

Any solutions?

[703 byte] By [feynman33a] at [2007-10-3 10:39:00]
# 1
Sounds like the editor is still active. Try the following when you intially create the table:table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
camickra at 2007-7-15 6:03:03 > top of Java-index,Desktop,Core GUI APIs...
# 2

Thank you for you answer.

I tried and it partially solved the problem:

I can launch the game a second time by pressing the button,

but if I try to launch the game a third time by pressing the button again,

the problem comes back ...

Also I cannot solve the second game ... It may be linked

Still investigating ...

feynman33a at 2007-7-15 6:03:03 > top of Java-index,Desktop,Core GUI APIs...
# 3
The game solving problem was unrelated and caused by a variable not reinitialized when I prepared the second game board ...
feynman33a at 2007-7-15 6:03:03 > top of Java-index,Desktop,Core GUI APIs...
# 4

My code included not related classes I eliminated to use only standard java classes,

here is is the minimal code for my mine sweeper reproducing the problem on the third launch

any hint? :

import java.awt.Container;

import java.awt.Point;

import java.awt.event.ActionEvent;

import java.awt.event.MouseEvent;

import java.util.Random;

import javax.swing.AbstractAction;

import javax.swing.Action;

import javax.swing.JButton;

import javax.swing.JComponent;

import javax.swing.JFrame;

import javax.swing.JLabel;

import javax.swing.JPanel;

import javax.swing.JTable;

import javax.swing.event.TableModelListener;

import javax.swing.table.DefaultTableModel;

import javax.swing.table.TableModel;

public class MyMineSweeper

{

private class JTableCellRenderer implements javax.swing.table.TableCellRenderer

{

private javax.swing.table.TableCellRenderer defaultRenderer;

public JTableCellRenderer(javax.swing.table.TableCellRenderer aRenderer)

{

defaultRenderer = aRenderer;

}

public java.awt.Component getTableCellRendererComponent(javax.swing.JTable aTable, Object anElement, boolean isSelected, boolean hasFocus, int aRow, int aColumn)

{

if(anElement instanceof javax.swing.JComponent)

{

return (javax.swing.JComponent)anElement;

}

return defaultRenderer.getTableCellRendererComponent(aTable, anElement, isSelected, hasFocus, aRow, aColumn);

}

}

private class JTableCellEditor extends javax.swing.AbstractCellEditor implements javax.swing.table.TableCellEditor

{

private javax.swing.JComponent component;

private int theClickCountToStart = 0;

public java.awt.Component getTableCellEditorComponent(javax.swing.JTable aTable, Object anElement, boolean isSelected, int aRow, int aColumn)

{

component = (javax.swing.JComponent) anElement;

return component;

}

public Object getCellEditorValue()

{

return component;

}

public boolean isCellEditable(java.util.EventObject anEvent)

{

if(anEvent instanceof java.awt.event.MouseEvent)

{

return ((java.awt.event.MouseEvent)anEvent).getClickCount() >= theClickCountToStart;

}

return true;

}

public int getClickCountToStart()

{

return theClickCountToStart;

}

public void setClickCountToStart(int aClickCountToStart)

{

theClickCountToStart = aClickCountToStart;

}

}

private class Tile extends javax.swing.JLabel

{

public class JTileMouseListener implements java.awt.event.MouseListener

{

public void mouseClicked(MouseEvent e)

{

if(isRevealed()==false)

{

reveal();

}

}

public void mousePressed(MouseEvent e)

{

}

public void mouseReleased(MouseEvent e)

{

}

public void mouseEntered(MouseEvent e)

{

}

public void mouseExited(MouseEvent e)

{

}

}

public void reveal(int aY, int anX)

{

Tile tile = ((Tile)theMapModel.getValueAt(aY, anX));

if(tile.isRevealed()==false)

{

tile.reveal();

}

}

public void changed()

{

if(theNeighbourCount==0)

{

if(theX>0)

{

if(theY>0)

{

reveal(theY-1, theX-1);

}

reveal(theY, theX-1);

if(theY<theMapModel.getRowCount()-1)

{

reveal(theY+1, theX-1);

}

}

if(theY>0)

{

reveal(theY-1, theX);

}

if(theY<theMapModel.getRowCount()-1)

{

reveal(theY+1, theX);

}

if(theX><theMapModel.getColumnCount()-1)

{

if(theY>0)

{

reveal(theY-1, theX+1);

}

reveal(theY, theX+1);

if(theY<theMapModel.getRowCount()-1)

{

reveal(theY+1, theX+1);

}

}

setBackground(java.awt.Color.WHITE);

}

else if(theNeighbourCount==9)

{

setText("*");

setBackground(java.awt.Color.RED);

System.out.println("no!");

theMap.setEnabled(false);

}

else

{

setText(String.valueOf(theNeighbourCount));

setBackground(java.awt.Color.WHITE);

}

setBorder(javax.swing.BorderFactory.createEmptyBorder());

if(isFinished()==true)

{

System.out.println("victory!");

theMap.setEnabled(false);

}

theMapModel.fireTableCellUpdated(theY,theX);

}

private DefaultTableModel theMapModel;

private int theX;

private int theY;

private short theNeighbourCount;

protected boolean revealed = false;

public Tile(int aYIndex, int anXIndex, short aNeighbourCount)

{

theMapModel = (DefaultTableModel)theMap.getModel();

theX = anXIndex;

theY = aYIndex;

theNeighbourCount = aNeighbourCount;

addMouseListener(new JTileMouseListener());

setOpaque(true);

setHorizontalAlignment(CENTER);

setBackground(java.awt.Color.LIGHT_GRAY);

setBorder(javax.swing.BorderFactory.createRaisedBevelBorder());

setSize(getHeight(), getHeight());

}

public void reveal()

{

revealed = true;

theRevealedTileCount +=1;

changed();

}

public boolean isRevealed()

{

return revealed;

}

}

private JFrame theFrame;

private JTable theMap;

private int theMapSize = 10;

private int theTrapCount = 5;

private int theRevealedTileCount = 0;

private void startGame()

{

Point[] traps = new Point[theTrapCount];

Random generator = new Random();

for(int trapIndex = 0; trapIndex><theTrapCount; trapIndex+=1)

{

Point newPoint = null;

boolean alreadyTrapped = true;

while(alreadyTrapped==true)

{

newPoint = new Point(generator.nextInt(theMapSize-1), generator.nextInt(theMapSize-1));

alreadyTrapped = false;

for(int existingTrapIndex= 0; existingTrapIndex><trapIndex;existingTrapIndex++)

{

if(newPoint.equals(traps[existingTrapIndex])) alreadyTrapped = true;

}

}

traps[trapIndex] = newPoint;

}

TableModel mapModel = theMap.getModel();

for(int yIndex = 0; yIndex><theMapSize; yIndex+=1)

{

for(int xIndex = 0; xIndex><theMapSize; xIndex+=1)

{

short neighbours = 0;

int x = 0;

int y = 0;

for(int trapIndex = 0; trapIndex><theTrapCount; trapIndex+=1)

{

x = traps[trapIndex].x - xIndex;

y = traps[trapIndex].y - yIndex;

if(x==0 && y==0)

{

trapIndex = theTrapCount;

}

else if((x==1 || x==-1) && (y==1 || y==-1))

{

neighbours += 1;

}

else if((x==0) && (y==1 || y==-1))

{

neighbours += 1;

}

else if((x==1 || x==-1) && (y==0))

{

neighbours += 1;

}

}

if(x==0 && y==0)

{

mapModel.setValueAt(new Tile(yIndex, xIndex, (short) 9), yIndex, xIndex);

}

else

{

mapModel.setValueAt(new Tile(yIndex, xIndex, neighbours), yIndex, xIndex);

}

}

}

theRevealedTileCount = 0;

theMap.setEnabled(true);

}

private boolean isFinished()

{

return ((theMapSize*theMapSize)-theRevealedTileCount)==theTrapCount;

}

public MyMineSweeper()

{

theFrame = new javax.swing.JFrame("mine sweeper");

JPanel cp = new JPanel();

theMap = new JTable(new DefaultTableModel(10,10)

{

public Class getColumnClass(int column)

{

return getValueAt(0, column).getClass();

}

});

theMap.setDefaultRenderer(JComponent.class, new JTableCellRenderer(theMap.getDefaultRenderer(JComponent.class)));

JTableCellEditor editor = new JTableCellEditor();

theMap.setDefaultEditor(JComponent.class, editor);

editor.setClickCountToStart(0);

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

startGame();

cp.add(theMap);

Action newGameAction = new AbstractAction("new game")

{

public void actionPerformed(ActionEvent e)

{

startGame();

}

};

JButton newGameTrigger = new JButton(newGameAction);

cp.add(newGameTrigger);

theFrame.getContentPane().add(cp);

theFrame.pack();

theFrame.show();

}

public JFrame getFrame()

{

return theFrame;

}

}

>

feynman33a at 2007-7-15 6:03:03 > top of Java-index,Desktop,Core GUI APIs...
# 5

I solved the problem by creating a new TableModel and setting it on the JTable using setModel in the startGame() mehod. After looking at the JTable.setModel() source code, I found that using the following line had the same effect :

theMap.tableChanged(new TableModelEvent(mapModel, TableModelEvent.HEADER_ROW));

I don't understand why it works, but here is the modified startGame() :

private void startGame()

{

Point[] traps = new Point[theTrapCount];

Random generator = new Random();

for(int trapIndex = 0; trapIndex<theTrapCount; trapIndex+=1)

{

Point newPoint = null;

boolean alreadyTrapped = true;

while(alreadyTrapped==true)

{

newPoint = new Point(generator.nextInt(theMapSize-1), generator.nextInt(theMapSize-1));

alreadyTrapped = false;

for(int existingTrapIndex= 0; existingTrapIndex><trapIndex;existingTrapIndex++)

{

if(newPoint.equals(traps[existingTrapIndex])) alreadyTrapped = true;

}

}

traps[trapIndex] = newPoint;

}

TableModel mapModel = theMap.getModel();

theMap.tableChanged(new TableModelEvent(mapModel, TableModelEvent.HEADER_ROW));

for(int yIndex = 0; yIndex><theMapSize; yIndex+=1)

{

for(int xIndex = 0; xIndex><theMapSize; xIndex+=1)

{

short neighbours = 0;

int x = 0;

int y = 0;

for(int trapIndex = 0; trapIndex><theTrapCount; trapIndex+=1)

{

x = traps[trapIndex].x - xIndex;

y = traps[trapIndex].y - yIndex;

if(x==0 && y==0)

{

trapIndex = theTrapCount;

}

else if((x==1 || x==-1) && (y==1 || y==-1))

{

neighbours += 1;

}

else if((x==0) && (y==1 || y==-1))

{

neighbours += 1;

}

else if((x==1 || x==-1) && (y==0))

{

neighbours += 1;

}

}

if(x==0 && y==0)

{

mapModel.setValueAt(new Tile(yIndex, xIndex, (short) 9), yIndex, xIndex);

}

else

{

mapModel.setValueAt(new Tile(yIndex, xIndex, neighbours), yIndex, xIndex);

}

}

}

theRevealedTileCount = 0;

theMap.setEnabled(true);

}

Any explication?

Any other solution?>

feynman33a at 2007-7-15 6:03:03 > top of Java-index,Desktop,Core GUI APIs...
# 6

The code you posted isn't executable. I'm not sure how you expect use to find a random bug that only appears the third time you try to do something. A few general comments.

You should never use a fireXXX method in your program. It is the responsibility of the DefaultTableModel to fire these events at the appropriate time. For example when you invokd the a setValueAt(...) method it will fire the tableCellUpdated(...) event for you.

If you want to clear the table you could do something like:

defaultTableModel.setRowCount(0);

defaultTableModel.setRowCount(...);

and then use setValueAt(..) to initialize each cell.

I'm not sure that you want to use the setModel(...) method. When you use this method it fires the tableStructureChanged event, which causes the table to recreate the TableColumnModel and all the TableColumns. This means that any custom renderers and editors you have added to the TableColumn are lost. That why my above suggestion is better. You just delelete / reset the date without changing the structure of the table.

If on the other hand you are attempting to change the grid size of you game then it may be appropriate to recreate the table model to reflect the new size. But since I can't execute your code I can't see what you are doing.

camickra at 2007-7-15 6:03:03 > top of Java-index,Desktop,Core GUI APIs...
# 7

Sorry, I just create a MyMineSweeper object in my program instead of creating the other objects I use normally ... I should have included a main method to launch it alone ...

Thanks for your answer, I do not change the grid size for now but it will be selectale by the user in the future ... So I will simply recreate a new table model, as you said it's simpler.

I use fireXXX method in my real program because I subclassed JTable and DefaultTableModel for my own purpose (so it does not seem so odd, in this example it looks more like a dirty hack) and I need to trigger repaint of the all the revealed tiles when their state is modified directly from outside the JTable and DefaultTableModel ... Or the grid is not entirely refreshed when you reveal an empty tile with no neighbouring mines ...

Well I think simply creating a new model and setting it on the JTable is the best solution for my purpose, thank you

feynman33a at 2007-7-15 6:03:03 > top of Java-index,Desktop,Core GUI APIs...