I have a JTable and I need to be able to reorder the columns. However I want the first column to not be able to be re-ordered. I used the following to enable reordering:
table.getTableHeader().setReorderingAllowed(true);
The columns can now be reordered including the first column which I don't want. Is there any way to lock the first column?
I have seen some solutions that use two tables with the first column being in a separate table, but maybe there's a better/simpler way.
This is the solution that I used to prevent the 1st column from being re-ordered
private int columnValue = -1;
private int columnNewValue = -1;
tblResults.getColumnModel().addColumnModelListener(new TableColumnModelListener()
{
public void columnAdded(TableColumnModelEvent e) {}
public void columnMarginChanged(ChangeEvent e) {}
public void columnMoved(TableColumnModelEvent e)
{
if (columnValue == -1)
columnValue = e.getFromIndex();
columnNewValue = e.getToIndex();
}
public void columnRemoved(TableColumnModelEvent e) {}
public void columnSelectionChanged(ListSelectionEvent e) {}
});
tblResults.getTableHeader().addMouseListener(new MouseAdapter()
{
#Override
public void mouseReleased(MouseEvent e)
{
if (columnValue != -1 && (columnValue == 0 || columnNewValue == 0))
tblResults.moveColumn(columnNewValue, columnValue);
columnValue = -1;
columnNewValue = -1;
}
});
Cheers,
Nearly 4 years later, there's still no optimal solution in sight anywhere.
Yet another suboptimal approach to prevent dragging of the first column (and other columns over the first) is to intercept the mouseEvents before the mouseInputListener installed by the uidelegate can handle them (similar to a recent QA).
The collaborators
a custom MouseMotionListener which delegates all events to the originally installed, except the dragged if it would lead to another column above the first
replace the original with the custom
update the replacement whenever the LAF is changed (because the original is controlled by the ui). This requires subclassing of JTableHeader and do the wiring in updateUI
The custom MouseInputListener:
/**
* A delegating MouseInputListener to be installed instead of
* the one registered by the ui-delegate.
*
* It's implemented to prevent dragging the first column or any other
* column over the first.
*/
public static class DragHook implements MouseInputListener {
private JTableHeader header;
private MouseListener mouseDelegate;
private MouseMotionListener mouseMotionDelegate;
private int maxX;
public DragHook(JTableHeader header) {
this.header = header;
installHook();
}
/**
* Implemented to do some tweaks/bookkeeping before/after
* passing the event to the original
*
* - temporarily disallow reordering if hit on first column
* - calculate the max mouseX that's allowable in dragging to the left
*
*/
#Override
public void mousePressed(MouseEvent e) {
int index = header.columnAtPoint(e.getPoint());
boolean reorderingAllowed = header.getReorderingAllowed();
if (index == 0) {
// temporarily disable re-ordering
header.setReorderingAllowed(false);
}
mouseDelegate.mousePressed(e);
header.setReorderingAllowed(reorderingAllowed);
if (header.getDraggedColumn() != null) {
Rectangle r = header.getHeaderRect(index);
maxX = header.getColumnModel().getColumn(0).getWidth()
+ e.getX() - r.x -1;
}
}
/**
* Implemented to pass the event to the original only if the
* mouseX doesn't lead to dragging the column over the first.
*/
#Override
public void mouseDragged(MouseEvent e) {
TableColumn dragged = header.getDraggedColumn();
int index = getViewIndexForColumn(header.getColumnModel(), dragged);
// dragged column is at second position, allow only drags to the right
if (index == 1) {
if (e.getX() < maxX) return;
}
mouseMotionDelegate.mouseDragged(e);
}
//-------- delegating-only methods
#Override
public void mouseReleased(MouseEvent e) {
mouseDelegate.mouseReleased(e);
}
#Override
public void mouseClicked(MouseEvent e) {
mouseDelegate.mouseClicked(e);
}
#Override
public void mouseEntered(MouseEvent e) {
mouseDelegate.mouseEntered(e);
}
#Override
public void mouseExited(MouseEvent e) {
mouseDelegate.mouseExited(e);
}
#Override
public void mouseMoved(MouseEvent e) {
mouseMotionDelegate.mouseMoved(e);
}
//------------ un-/install listeners
protected void installHook() {
installMouseHook();
installMouseMotionHook();
}
protected void installMouseMotionHook() {
MouseMotionListener[] listeners = header.getMouseMotionListeners();
for (int i = 0; i < listeners.length; i++) {
MouseMotionListener l = listeners[i];
if (l.getClass().getName().contains("TableHeaderUI")) {
this.mouseMotionDelegate = l;
listeners[i] = this;
}
header.removeMouseMotionListener(l);
}
for (MouseMotionListener l : listeners) {
header.addMouseMotionListener(l);
}
}
protected void installMouseHook() {
MouseListener[] listeners = header.getMouseListeners();
for (int i = 0; i < listeners.length; i++) {
MouseListener l = listeners[i];
if (l.getClass().getName().contains("TableHeaderUI")) {
this.mouseDelegate = l;
listeners[i] = this;
}
header.removeMouseListener(l);
}
for (MouseListener l : listeners) {
header.addMouseListener(l);
}
}
public void uninstallHook() {
uninstallMouseHook();
uninstallMouseMotionHook();
}
protected void uninstallMouseMotionHook() {
MouseMotionListener[] listeners = header.getMouseMotionListeners();
for (int i = 0; i < listeners.length; i++) {
MouseMotionListener l = listeners[i];
if (l == this) {
listeners[i] = mouseMotionDelegate;
}
header.removeMouseMotionListener(l);
}
for (MouseMotionListener l : listeners) {
header.addMouseMotionListener(l);
}
}
protected void uninstallMouseHook() {
MouseListener[] listeners = header.getMouseListeners();
for (int i = 0; i < listeners.length; i++) {
MouseListener l = listeners[i];
if (l == this) {
listeners[i] = mouseDelegate;
}
header.removeMouseListener(l);
}
for (MouseListener l : listeners) {
header.addMouseListener(l);
}
}
}
Usage which survives switching of LAF, f.i.:
JTable table = new JTable(new AncientSwingTeam()) {
#Override
protected JTableHeader createDefaultTableHeader() {
JTableHeader header = new JTableHeader(getColumnModel()) {
DragHook hook;
#Override
public void updateUI() {
if (hook != null) {
hook.uninstallHook();
hook = null;
}
super.updateUI();
hook = new DragHook(this);
}
};
return header;
}
};
I think that you need to override the columnMoved() method in TableColumnModelListener. the TableColumnModelEvent class has a getFromIndex() method that you should be able to look at to determine if it's your fixed column, and then you should be able to cancel the event.
Hope that helps. A
First you need to define a better and simpler way. What don't you like about the 2 table approach?
You can't use a TableColumnModelListener, because the event is fired "after" the column has already been moved.
The code for dragging the column is found in the BasicTableHeaderUI. So you could try overriding the code there, but then you would need to do it for all LAFs.
The above code invokes JTableHeader.getReorderingAllowed() on a mousePressed event to determine if column reordering is allowed. I guess you could override that method in the JTableHeader and perhaps use the MouseInfo class to get the current mouse location to determine if it was over the first column and then return false. But then now you would also need to create a custom JTable that uses the custom table header.
Of course with either of the above suggestions you might be able to prevent the first column from being moved. But don't forget you also need to prevent the 2nd column from being inserted before the first column. I don't believe there is a short simple answer to the question.
Fixed Column Table is my version of how this would be imlemented with two tables. Is it better? I don't know, but it is simple since its only a single line of code to use it.
I had the same issue, and I was searching about it. So far I found two ways of doing that.
The "if I was rewriting it myself" method : Modifying the base classes from Java.
TableColumn would need a new property, like the "resizingAllowed", it would need the "reorderingAllowed".
From this, the modifications take place in BasicTableHeaderUI :
There is already :
private static boolean canResize(TableColumn column,
JTableHeader header) {
return (column != null) && header.getResizingAllowed()
&& column.getResizable();
}
It would need too :
private static boolean canMove(TableColumn column,
JTableHeader header) {
return (column != null) && header.getReorderingAllowed()
&& column.getReorderable();
}
(Note that if you don't want the first column only to not move, you can do without changing the TableColumns :
private static boolean canMove(TableColumn column,
JTableHeader header) {
return (column != null) && header.getReorderingAllowed()
&& header.getColumnModel().getColumnIndex(column.getIdentifier()) != 0;
}
)
After, two places to modify in the MouseInputListener :
in the mousePressed, calling the canMove() instead of the header.getReorderingAllowed(). This ensures that a column which shouldn't be moved, won't be.
But this is not enough, we need to prevent the immobile columns from being moved during dragging another one. You need to change the mouseDragged, too, when it is getting the "newColumnIndex" :
if (0 < newColumnIndex && newColumnIndex < cm.getColumnCount())
You need to add the condition if this new index can be moved, for example using the "canMove()" method. This way, when you will drag a column to this immobile one, you will still drag it, but it won't swap them.
Note that this method would require you to explicitly set the UI for the JTableHeader used for your JTable, which is not really ideal. But this is the most adapted though, as it deals with the problem on the place it is supposed to.
The "Let's try to block the normal behavior with what we actually have" method : Not modifying the UI, this method focus on the JTableHeader to block the commands made by the UI.
First, to block dragging the first column, we need a subclass from JTableHeader, with this overridden method :
#Override
public void setDraggedColumn(TableColumn pAColumn)
{
int lIndex = -1;
if (pAColumn != null)
lIndex = getColumnModel().getColumnIndex(pAColumn.getIdentifier());
if (lIndex != 0)
super.setDraggedColumn(pAColumn);
}
This will prevent a user from dragging the first column. But like described earlier, this is only one part of the problem, we need to prevent another dragged column from swapping with this first one.
So far, I don't have a correct method for this. I tried by subclassing the TableColumnModel, and overriding the moveColumn() method :
#Override
public void moveColumn(int pColumnIndex, int pNewIndex)
{
//Move only if the first column is not concerned
if (pColumnIndex =! 0 && pNewIndex != 0)
super.moveColumn(pColumnIndex, pNewIndex);
}
But this won't work, as the UI will update anyway the mouse position in the mouseDragged method, you will have a jump from your dragged column to another place.
So I'm still searching, and wonder if someone has propositions concerning this part.
At first, I used the very last Gnoupi's suggestion consisting in subclassing the TableColumnModel and overriding moveColumn but there were still some annoying jumps.
This is "my" fully working and tested solution with no nasty jump, it mainly relies on StanislavKo and kleopatra's suggestions. I added a more complicated mechanism to revert the unwanted move when releasing the mouse button :
table.getTableHeader().setUI(new WindowsTableHeaderUI() {
#Override
protected MouseInputListener createMouseInputListener() {
return new BasicTableHeaderUI.MouseInputHandler() {
#Override
public void mouseDragged(MouseEvent e) {
if (header.isEnabled() && header.getReorderingAllowed() && header.getDraggedColumn() != null && header.getDraggedColumn().getModelIndex() == frozenColumnModelIndex) {
header.setDraggedDistance(0);
header.setDraggedColumn(null);
return;
}
super.mouseDragged(e);
}
#Override
public void mouseReleased(MouseEvent e) {
if (header.isEnabled() && header.getReorderingAllowed() && header.getDraggedColumn() != null &&
0 <= illegalTableColumnMoveFromIndex && illegalTableColumnMoveFromIndex < header.getTable().getColumnModel().getColumnCount()) {
header.setDraggedDistance(0);
header.setDraggedColumn(null);
header.getTable().getColumnModel().moveColumn(illegalTableColumnMoveToIndex, illegalTableColumnMoveFromIndex);
illegalTableColumnMoveFromIndex = -1;
illegalTableColumnMoveToIndex = -1;
return;
}
super.mouseReleased(e);
}
};
}
});
table.getColumnModel().addColumnModelListener(new TableColumnModelListener() {
#Override
public void columnAdded(TableColumnModelEvent e) {
}
#Override
public void columnRemoved(TableColumnModelEvent e) {
}
#Override
public void columnMoved(TableColumnModelEvent e) {
if (e.getFromIndex() != e.getToIndex() && table.getColumnModel().getColumn(e.getFromIndex()).getModelIndex() == frozenColumnModelIndex) {
illegalTableColumnMoveFromIndex = e.getFromIndex();
illegalTableColumnMoveToIndex = e.getToIndex();
} else {
illegalTableColumnMoveFromIndex = -1;
illegalTableColumnMoveToIndex = -1;
}
}
#Override
public void columnMarginChanged(ChangeEvent e) {
}
#Override
public void columnSelectionChanged(ListSelectionEvent e) {
}
});
Note that the latest valid move is accepted instead of completely reverting the column drag.
frozenColumnModelIndex is the index of the "frozen" column in the table model.
illegalTableColumnMoveFromIndex is the index of the column from where it was moved when the latest illegal move was detected.
illegalTableColumnMoveToIndex is the index of the column to where it was moved when the latest illegal move was detected.
The code inside mouseDragged is enough to prevent the frozen column from being dragged, the rest allows to prevent another column from being dragged to the frozen column.
It works as is under Microsoft Windows as I extend WindowsTableHeaderUI but rather use the reflection API to set the mouse input listener of the table header UI, call uninstallerListeners() and finally call header.addMouseListener(mouseInputListener) and header.addMouseMotionListener(mouseInputListener) in order to drive my solution cross-platform without making any assumption on the name of the class for each table header UI.
I admit it might be a bit less robust than kleopatra's solution. I thank you all for your help, I'm really grateful and I'm really happy to see that collaborative work just works :)
I have used the "The 'Let's try to block the normal behavior with what we actually have' method" approach. Gnoupi said that he did not solve the second part of the problem. Here is the solution for just Windows XP L&F:
copy XPStyle class to yourself.
extend WindowsTableHeaderUI. Take a look at the source code.
use it: getTableHeader().setUI(new TreeTableWindowsTableHeaderUI());
Thanks to Gnoupi for the efforts.
I will just put the column back after the move is complete. So something like.
#Override
public void moveColumn(int from, int to) {
super.moveColumn(from, to);
if (from == 0 || to == 0) {
super.moveColumn(to, from);
}
}
Related
My requirement is to use addDocumentListener, the doSearchCmb basically narrows down items in combobox, function is working if keypressed is used. If I remove the function Runnable doSearchCmb and put the narrowing down of items in insertUpdate without using invokeLater, I get an error of 'Attempt to mutate notification' exception.
In my current code, my screen freezes after I type a letter. After waiting several minutes, I get the error of java.lang.OutOfMemoryError: Java heap space.
I tried to add return; after combo.repaint();, my screen didn't freeze, there's no java heap space error but nothing happened at all. I attached the code without the return.
What can I do here to remain the use of addDocumentListener and the function which narrows down the items of the combobox?
TCombo combo = new TCombo();
JTextComponent editor = (JTextComponent) combo.getEditor().getEditorComponent();
editor.getDocument().addDocumentListener(new DocumentListener() {
public void changedUpdate(DocumentEvent arg0) {
}
public void insertUpdate(DocumentEvent arg0) {
searchCmb();
}
public void removeUpdate(DocumentEvent arg0) {
searchCmb();
}
private void searchCmb() {
Runnable doSearchCmb = new Runnable() {
#Override
public void run() {
String item = combo.getEditor().getItem().toString().trim();
boolean isEmpty = item.equals("");
CmbElement[] foundList = null;
String toFind = "";
List list = new ArrayList(0);
if (!isEmpty) {
combo.removeAllItems();
combo.setItems(elements);
for (int i = 1; i < elements.length; i++) {
if (elements[i].getName().contains(toFind)) {
if (i == 1) {
list.add("");
}
list.add(elements[i]);
}
foundList = (CmbElement[]) list.toArray(new CmbElement[list.size()]);
}
if (list.size() > 0) {
combo.removeAllItems();
combo.setItems(foundList);
} else {
combo.removeAllItems();
if (toFind.equals("")) {
combo.setItems(elements);
}
list.add(new DCmbElement("", ""));
foundList = (CmbElement[]) list.toArray(new CmbElement[list.size()]);
combo.setItems(foundList);
}
combo.repaint();
}
}
};
SwingUtilities.invokeLater(doSearchCmb);
}
});
CmbElement:
public abstract interface CmbElement {
public abstract String getKey();
public abstract String getName();
}
Note: Narrow down items in combo box means when user inputs a letter, or paste a word, the items in combo box gets filtered using the current letter or word as parameter. It searches through the items and narrows it down.
For reference the behavior is like the image here: jcombobox filter in java - Look and feel independent
My function indicated in run() is working fine if keypressed of keylistener is used, but my requirement is to use addDocumentListener
I am having an issue implementing a double click handler for my Datagrid. I have found a solution posted on Stack overflow that should fix my problem i believe, however, I can not figure out:
1. How exactly do I implement it?
2. What is <T>?
I am getting various errors I do not understand. The issue is almost certainly with the way I add the CellPreviewHandler (Line 6)
Errors (line 6 & 8)
-the Type new CellPreviewEvent.Handler(){} must implement the inherited abstract method CellPreviewEvent.Handler.onCellPreview(CellPreviewEvent)
-The method onCellPreview(CellPreviewEvent) of type new AsynCallBack(String[][]>(){} must override or implement a supertype method
MyCode:
Public Class DataGrid extends Widget{
Timer singleClickTimer;
int clickCount = 0;
int clickDelay = 300;
myDataTable = new DataGrid<String[]>(result.length, resources, KEY_PROVIDER);
myDataTable.addCellPreviewHandler(new Handler<T>(){
#Override
public void onCellPreview(final CellPreviewEvent<T> event) {
if (Event.getTypeInt(event.getNativeEvent().getType()) == Event.ONMOUSEOVER) {
handleOnMouseOver(event);
} else if (Event.getTypeInt(event.getNativeEvent().getType()) == Event.ONCLICK) {
clickCount++;
if (clickCount == 1) {
singleClickTimer = new Timer() {
#Override
public void run() {
clickCount = 0;
handleOnClick(event);
}
};
singleClickTimer.schedule(clickDelay);
} else if (clickCount == 2) {
singleClickTimer.cancel();
clickCount = 0;
handleOnDblClick(event);
}
}
}
});
private void handleOnMouseOver(CellPreviewEvent<T> event) {
Element cell = event.getNativeEvent().getEventTarget().cast();
GWT.log("mouse over event");
}
private void handleOnClick(CellPreviewEvent<T> event) {
Element cell = event.getNativeEvent().getEventTarget().cast();
GWT.log("click event");
}
private void handleOnDblClick(CellPreviewEvent<T> event) {
Element cell = event.getNativeEvent().getEventTarget().cast();
GWT.log("double click event");
}
Link to original solution:
adding Double click event in CellTable cell - GWT
This is not a very good code (a better option would be to extend DataGrid class), but if you don't want to change much, simply replace <T> with <String[]>.
I have one Table with Playlists. When I double click on a Playlist I get Movies for this Playlist.
However, when I double click on the movies I get an empty table back. Consequently, I do not want to click in the movie table. How to prevent this behaviour?
That's my listener:
playlistTable.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
JTable target = (JTable)e.getSource();
int row = target.getSelectedRow();
videoTableModel = new VideoTableModel(playlistService.getMoviesOfPlaylist(row));
target.setModel(videoTableModel);
movieTable.setEnabled(true);
createPlaylist.setEnabled(false);
setButtonIcon("icons\\playlist_grau.png", createPlaylist, "createPlaylist");
removePlaylist.setEnabled(false);
setButtonIcon("icons\\bin_grau.png", removePlaylist, "removePlaylist");
playlistTable.setEnabled(false);
revalidate();
}
}
});
I appreciate your answer.
Either remove the mouse listener from the table, or set a flag that will make the listener do nothing:
playlistTable.addMouseListener(new MouseAdapter() {
private boolean ignoreDoubleClicks = false;
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2 && !ignoreDoubleClicks) {
JTable target = (JTable)e.getSource();
target.removeMouseListener(this);
or
playlistTable.addMouseListener(new MouseAdapter() {
private boolean ignoreDoubleClicks = false;
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2 && !ignoreDoubleClicks) {
ignoreDoubleClicks = true;
...
You are using the same table to represent both sets of data
JTable target = (JTable)e.getSource();
....
target.setModel(videoTableModel);
Option 1) remove the listener when you change to "video mode"
target.removeMouseListener(this);
Option 2) add a variable and set it to false when you change to "video mode", if the variable is false do nothing.
Option 3) depends of the rest of your code, check the class of the current model (it assumes you have a different one for playlists)
if (!(target.getModel instanceof VideoTableModel)) {
[Your code here]
}
In 1) or 2) you will have to reset the listener when switching back to playlist.
Personally, I would use two tables and hide one or the other based in the mode.
However, when I double click on the movies I get an empty table back.
this has nothing to do with MouseListener, have to disable TableCellEditor
.
public boolean isCellEditable(int rowIndex, int columnIndex){
return false;
}
.
have to test if return -1 (any row is selected) for int row = target.getSelectedRow();
this logics isn't correct why you enables whatever on mouse_double_click, disable ListSelectionMode,
I have a custom JTree implementation that implemts convertValueToText. This implementation depends on some global state. If the returned string is longer (actually I think wider as in pixels triggers it) than a previously returned string, the text will be truncated and padded with "...". This is true when the redrawing is caused by (de)selecting the element or a repaint on the tree.
Is it valid to implement convertValueToText in such a way? (I recognize that the tree display will not automatically be updated).
How can I get rid of the "..." and force all elements to be correctly drawn with their current textual value?
Update2:
Appearently I violated Swings rather strict treading policy (http://docs.oracle.com/javase/6/docs/api/javax/swing/package-summary.html). Doing the update with:
SwingUtilities.invokeAndWait(new Runnable() {
#Override
public void run() {
for (int row = 0; row < tree.getRowCount(); row++) {
((DefaultTreeModel) tree.getModel())
.nodeChanged((TreeNode) tree.getPathForRow(row)
.getLastPathComponent());
}
}
});
appears to work correctly.
Update:
Here is a minimal example:
import java.util.Enumeration;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JTree;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
public class WeirdTreeFrame extends JFrame {
public class WeirdTree extends JTree {
public WeirdTree(TreeNode root) {
super(root);
}
#Override
public String convertValueToText(Object value, boolean selected,
boolean expanded, boolean leaf, int row, boolean hasFocus) {
return super.convertValueToText(value, selected, expanded, leaf, row, hasFocus);
}
}
public class WeirdNode implements TreeNode {
protected WeirdNode parent;
protected WeirdNode children[];
protected int dephth;
protected String name;
protected String names[] = {"Foo", "Bar", "Ohh"};
public long superCrazyNumber;
public WeirdNode(WeirdNode parent, String name) {
this.parent = parent;
this.name = name;
if (parent != null) {
dephth = parent.dephth + 1;
} else {
dephth = 0;
}
if (dephth < 10) {
children = new WeirdNode[3];
for (int i = 0; i < 3; i++) {
children[i] = new WeirdNode(this, name + names[i]);
}
}
}
#Override
public TreeNode getChildAt(int childIndex) {
if (childIndex >= getChildCount()) return null;
return children[childIndex];
}
#Override
public int getChildCount() {
return (children != null) ? children.length : 0;
}
#Override
public TreeNode getParent() {
return parent;
}
#Override
public int getIndex(TreeNode node) {
for (int i = 0; i < children.length; i++) {
if (children[i] == node) return i;
}
throw new RuntimeException();
}
#Override
public boolean getAllowsChildren() {
return true;
}
#Override
public boolean isLeaf() {
return getChildCount() == 0;
}
#Override
public Enumeration children() {
return new Enumeration<TreeNode>() {
int nextIdx = 0;
#Override
public boolean hasMoreElements() {
return nextIdx < getChildCount();
}
#Override
public TreeNode nextElement() {
return getChildAt(nextIdx++);
}
};
}
#Override
public String toString() {
return "[" + dephth + "]" + name + "#" + superCrazyNumber;
}
}
public WeirdNode root;
private WeirdTree tree;
public WeirdTreeFrame() {
root = new WeirdNode(null, "Blubb");
tree = new WeirdTree(root);
add(tree);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(500,500);
setVisible(true);
}
public void update() {
for (int row = 0; row < tree.getRowCount(); row++) {
((DefaultTreeModel) tree.getModel()).nodeChanged((TreeNode)tree.getPathForRow(row).getLastPathComponent());
}
}
public static void main(String[] args) {
WeirdTreeFrame frame = new WeirdTreeFrame();
Random rnd = new Random(41);
for (long i = 0; ; i++) {
for (WeirdNode node = frame.root; node != null; node = (WeirdNode)node.getChildAt(rnd.nextInt(3))) {
node.superCrazyNumber++;
}
if (i % 1e7 == 0) {
System.out.println("Update: " + i);
frame.update();
}
}
}
}
With the update() method I tried to fire the appropriate events to make sure all visible nodes are updated correctly. As you can see some computation is happening in parallel and perioically during the computation I want to update the tree labels (NOT the structure).
The issue with the update() method is that some nodes are labled completely wrong as you can see in the attached picture (should be "[0]..." for root node, "[n].."for nth level)
I assume this is some race condition.
This probably means your are updating the tree without telling it you changed something so when it redraws it just redraws it without recalculating the layout. You have to call DefaultTreeModel.nodeChanged(node) from the model to notify JTree you actually modified something then it will handle recalculating the size of the node. Or use the proper TreeModelListener interface call to notify the Tree something has changed. If you do that it will relayout that node properly.
If you are properly notifying the tree JLabel is redrawing itself, but it is not performing layout again to calculate how much space it needs for the new string. If you revalidate() it (as opposed to repaint()) it should perform layout again and it will calculate its preferred size again based on the new string and resize itself to surround the entire string.
The section in the main() method that manipulates the tree then calls update is violating the swing thread rule because when WeirdFrame is constructed it is realized by the call setVisible(). That means you can't touch it or any model being used to draw from from any thread except the Swing Event Dispatcher thread. It doesn't have anything to do with whether you implement TreeNode or not.
The easiest fix to this is to defer it back to the Event Dispatch thread. like so:
public void update( final WeirdNode node, final long nextSuperCrazyNumber ) {
SwingUtilities.invokeLater( new Runnable() {
public void run() {
node.superCrazyNumber = nextSuperCrazyNumber;
((DefaultTreeModel) tree.getModel()).nodeChanged(node);
}
});
}
public static void main(String[] args) {
WeirdTreeFrame frame = new WeirdTreeFrame();
Random rnd = new Random(41);
for (long i = 0; ; i++) {
for (WeirdNode node = frame.root; node != null; node =(WeirdNode)node.getChildAt(rnd.nextInt(3))) {
long superCrazyNumber = node.superCrazyNumber;
frame.update( node, superCrazyNumber++ );
}
}
}
Now notice how the main thread is running unfettered, but it isn't directly manipulating the data contained in the WeirdNode. Remember WeirdNode's toString() method is called to render a string to display so it's being called on the SwingThread. If you write to WeirdNode.superCrazyNumber on from another thread you are violating the threading rule. The main thread getting a copy doing some calculation to it, then posting the update back to the Swing Thread using SwingUtilities.invokeLater() NOT invokeAndWait(). You don't need invokeAndWait except is very limited situations, and it has to potential to cause lock ups so its best to just avoid it. Your UI doesn't have to reflect the model at every second it just has to eventually reflect it and so invokeLater is your friend. The key thing here is I'm not doing a write operation on the main thread as the code before, and I certainly can call update on the individual nodes without having to resort to shotgun updates.
After changing the node call method nodeChanged.
((DefaultTreeModel)tree).getModel().nodeChanged(node);
The DefaultTreeCellRenderer is a JLabel, which adds the ellipses as a convenience. You can see the effect by resizing the frame in this example. Several alternatives are possible:
Use a suitable layout for the enclosing container; the example uses GridLayout.
Use a component for the renderer, such as JTextField, that can scroll horizontally.
Add a TreeSelectionListener that updates an adjacent text component.
If you have JTable set with table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) and then you click drag on a row that is not already selected, it starts selecting multiple rows. We don't want that behavior. We want it so if you click on a node, even if it's not already selected, it will start dragging it.
We do need the multi select mode on, so setting it to single select (which does result in the behavior we want) is not an option.
Update: At this point, it appears it will require some type of ugly hack since the logic is in a private method BasicTableUI$Handler.canStartDrag
I found one possible solution. You bracket the mouse listeners so you can lie to the call to isCellSelected during the canStartDrag call.
Subclass JTable (or in my case, JXTreeTable). In the constructor call this:
private void setupSelectionDragHack()
{
// Bracket the other mouse listeners so we may inject our lie
final MouseListener[] ls = getMouseListeners();
for (final MouseListener l : ls)
{
removeMouseListener(l);
}
addMouseListener(new MouseAdapter()
{
#Override
public void mousePressed(final MouseEvent e)
{
// NOTE: it might not be necessary to check the row, but... I figure it's safer maybe?
mousingRow = rowAtPoint(e.getPoint());
mousingInProgress = true;
}
});
for (final MouseListener l : ls)
{
addMouseListener(l);
}
addMouseListener(new MouseAdapter()
{
#Override
public void mousePressed(final MouseEvent e)
{
mousingInProgress = false;
}
});
}
And then you'll need this:
#Override
public boolean isCellSelected(final int row, final int column)
{
if (mousingInProgress && row == mousingRow)
{
// Only lie to the canStartDrag caller. We tell the truth to everyone else.
final StackTraceElement[] elms = Thread.currentThread().getStackTrace();
for (int i = 0; i < 3; i++)
{
if (elms[i].getMethodName().equals("canStartDrag"))
{
return mousingInProgress;
}
}
}
return super.isCellSelected(row, column);
}
It's an ugly hack in many ways, but... for now it seems to work.
Unfortunately none of the other answers worked for me.
So I made my own hack/fix for the problem (I'm posting it here for others with the same problem):
public class SFixTable extends JTable {
private static final long serialVersionUID = 1082882838948078289L;
boolean pressed = false;
int currSRow = -100;
public SFixTable(TableModel dm) {
super(dm);
}
public SFixTable() {
super();
}
public SFixTable(Vector<?> rowData, Vector<?> columnNames) {
super(rowData, columnNames);
}
#Override
protected void processMouseEvent(MouseEvent e) {
int row = rowAtPoint(e.getPoint());
int col = columnAtPoint(e.getPoint());
if (SwingUtilities.isLeftMouseButton(e) && !e.isShiftDown() && !e.isControlDown()) {
boolean isDragRelease = (e.getID() == MouseEvent.MOUSE_RELEASED) && row != currSRow;
boolean isStartClick = (e.getID() == MouseEvent.MOUSE_PRESSED);
if (row >= 0 && col >= 0) {
if (isStartClick) {
super.changeSelection(row, col, false, false);
} else if (isDragRelease) {
super.changeSelection(currSRow, col, false, false);
}
}
pressed = (e.getID() == MouseEvent.MOUSE_PRESSED);
if (pressed) {
currSRow = row;
} else {
currSRow = -100;
}
}
super.processMouseEvent(e);
}
#Override
public boolean isCellSelected(int row, int col) {
return (pressed)? (row == currSRow) : super.isCellSelected(row, col);
}
}
It's a bug:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6349223
and as you already assumed, it requires some ugly hack. Here's one (not from me, but from a user Aephyr on old sun forums which didn't survive the migration to OTN)
table = new JTable() {
// fix for http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6349223
// requirement is the option to turn off drag-selection if dragEnabled
// fix posted in sun dev forum by Aephyr
// http://forums.sun.com/thread.jspa?threadID=5436355&tstart=0
private boolean pressed;
#Override
protected void processMouseEvent(MouseEvent e) {
pressed = e.getID() == MouseEvent.MOUSE_PRESSED;
if (pressed && !e.isShiftDown() && !e.isControlDown())
clearSelection();
try {
super.processMouseEvent(e);
} finally {
pressed = false;
}
}
#Override
public boolean isCellSelected(int row, int col) {
return pressed ? true : super.isCellSelected(row, col);
}
};
Similar to kleopatra's answer, but this seems to handle a few issues with the previous one -- you can control-click to both add and remove items from a multiple selection, and you can successfully drag a multi-select group. I've tested this only with an ETable/Outline from NetBeans, but should work with a regular JTable.
table = new JTable() {
private boolean inPress = false;
#Override protected void processMouseEvent(MouseEvent e) {
inPress = e.getID() == MouseEvent.MOUSE_PRESSED && e.getButton() == MouseEvent.BUTTON1 && !e.isShiftDown() && !e.isControlDown();
try {
super.processMouseEvent(e);
} finally {
inPress = false;
}
}
#Override public boolean isCellSelected(int row, int col) {
boolean selected = super.isCellSelected(row, col);
if (inPress) {
if (!selected)
clearSelection();
return true;
}
return selected;
}
};
If what you are looking for is to drag an unselected row in a single selection JTable, setting the table's selection model to SINGLE_SELECTION mode is not enough, you also have to set the column model's selection mode.
JTable table = new JTable();
table.getSelectionModel()
.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.getColumnModel().getSelectionModel()
.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);