I have a JTable and a button next to it that calls deleteSelectedRows(), which does exactly what it sounds like:
public void deleteSelectedRows() {
int[] selected = jTable.getSelectedRows();
for(int i = selected.length - 1; i >= 0; i--) {
model.removeRow(selected[i]);
}
if(model.getRowCount() < 1) {
addEmptyRow();
}
}
But if a cell was in the act of being edited when it (and/or cells above it) were deleted, the edited cell stayed while the rest left, like this:
And then trying to exit out of the editing threw an ArrayIndexOutOfBoundsException since row 5 was trying to be accessed and there was only one row left in the table.
I then tried all sorts of fun and games with jTable.getEditingRow(). At first, adding an if(selected[i] != editing) before the removal seemed to work, but then removing rows above the edited cell caused problems.
Then I tried this:
public void deleteSelectedRows() {
int[] selected = jTable.getSelectedRows();
int editing = jTable.getEditingRow();
for(int s : selected) { //PS: Is there a better way of doing a linear search?
if(s == editing) {
return;
}
}
for(int i = selected.length - 1; i >= 0; i--) {
model.removeRow(selected[i]);
}
if(model.getRowCount() < 1) {
addEmptyRow();
}
}
But that doesn't delete anything, ever. Judging from printlns I sprinkled around, the last cell to be highlighted (that has the special border seen here on spam) is considered part of the editing row, and thus triggers my early return.
So I don't really care whether the solution involves fixing the original problem--that of the wacky results when a cell being edited is deleted--or this new problem--that of getEditingRow() not behaving as I expected, it's just that I need at least one of those to happen. That said, I would be interested to hear both solutions just out of academic curiosity. Thanks in advance.
Try to include the following lines before removing any rows from your model:
if (table.isEditing()) {
table.getCellEditor().stopCellEditing();
}
As Howard stated, it is necessary to stop the cell editing before modifying the model. But it is also necessary to check if the cell is actually being modified to avoid null pointer exceptions.
This is because the getCellEditor() method will return null if the table isn't being edited at the moment:
if (myTable.isEditing()) // Only if it's is being edited
myTable.getCellEditor().stopCellEditing();
...
there are cases where the cell editor may refuse to stop editing,
that can happen i.e. if you are using some complex editor that is waiting for user input on a dialog. In that case you should add an extra check:
if (myTable.isEditing())
if (!myTable.getCellEditor().stopCellEditing()) {
// If your update is user-generated:
JOptionPane.showMessageDialog(null, "Please complete cell edition first.");
// Either way return without doing the update.
return;
}
In your code, you are trying to delete only the rows that are not being edited, but that would also throw an ArrayOutOfBounds Exception when the cell editor stops editing. The best is to stop it before the refresh.
Finally, there seems to be also a property you can set in your table:
table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
as explained here.
Whilst stopping any and all cells from editing before applying any changes works, it's a bit like using a sledge hammer to crack a nut. What happens, for example, if the cell that is editing is not the one being deleted? This is the next problem you'll encounter. For that reason and others there is a better way.
Firstly, use the framework to do the heavy lifting for you. Attach a TableModelListener to your table model table.getModel().addTableModelListener()... then in your listeners implementation catch the delete event and process as follows:
/**
* Implements {#link TableModelListener}. This fine grain notification tells listeners
* the exact range of cells, rows, or columns that changed.
*
* #param e the event, containing the location of the changed model.
*/
#Override
public void tableChanged(TableModelEvent e) {
if (TableModelEvent.DELETE == e.getType()) {
// If the cell or cells beng edited are within the range of the cells that have
// been been changed, as declared in the table event, then editing must either
// be cancelled or stopped.
if (table.isEditing()) {
TableCellEditor editor = table.getDefaultEditor(ViewHolder.class);
if (editor != null) {
// the coordinate of the cell being edited.
int editingColumn = table.getEditingColumn();
int editingRow = table.getEditingRow();
// the inclusive coordinates of the cells that have changed.
int changedColumn = e.getColumn();
int firstRowChanged = e.getFirstRow();
int lastRowChanged = e.getLastRow();
// true, if the cell being edited is in the range of cells changed
boolean editingCellInRangeOfChangedCells =
(TableModelEvent.ALL_COLUMNS == changedColumn ||
changedColumn == editingColumn) &&
editingRow >= firstRowChanged &&
editingRow <= lastRowChanged;
if (editingCellInRangeOfChangedCells) {
editor.cancelCellEditing();
}
}
}
}
}
In the example above I've assigned my own editor as the default editor for the table table.setDefaultRenderer(ViewHolder.class, new Renderer()); table.setDefaultEditor(ViewHolder.class, new Editor());.
Additionally instead of using a specific view I use a ViewHolder. The reason for this is to make the table generic in terms of the views it displays. Here is the generic ViewHolder.class:
/**
* Holds the view in a table cell. It is used by both the {#link Renderer}
* and {#link Editor} as a generic wrapper for the view.
*/
public static abstract class ViewHolder {
private static final String TAG = "ViewHolder" + ": ";
// the position (index) of the model data in the model list
protected final int position;
// the model
protected Object model;
// the view to be rendered
protected final Component view;
// the views controller
protected final Object controller;
/**
* #param view the view to be rendered
* #param position the position (index) of the data
*/
public ViewHolder(int position,
Object model,
Component view,
Object controller) {
this.position = position;
if (view == null) {
throw new IllegalArgumentException("item view may not be null");
}
if (model == null) {
throw new IllegalArgumentException("model may not be null");
}
this.controller = controller;
this.model = model;
this.view = view;
}
Now, each time your renderer or editor is called, construct a ViewHolder class and pass in your view / controller / position etc, and you're done.
The important thing to note here is that you do not have to catch the delete or change event before it happens. You should, in fact, catch it after the model changes. Why? Well after a change you know what has changed, because the TableModelListener tells you, helping you determine as to what to do next.
Related
I have a JComboBox contain 9 image items. How can I arrange them to 3*3?
I want my items arrange like this
I've tried google it for several days, but I don't know what is the keyword for this question. Any advice is appreciated. Thanks for reading my question.
Edit:
Thanks for everyone's feedback.
I am making a map editor where I can put map element together.
The editor.
It is more intuitive to arrange the stone road 3*3. The user can easily know which elements match each other.
I am not necessarily using combo box. I've also consider using buttons, but I think that buttons will waste a lat of space. Because I will have more map elements in the future.
The popup for the combo box uses a JList component to display the items.
You can access and change the orientation of the JList to wrap items:
Object child = comboBox.getAccessibleContext().getAccessibleChild(0);
BasicComboPopup popup = (BasicComboPopup)child;
JList list = popup.getList();
list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
list.setVisibleRowCount(3);
This will allow you to navigate through the items using the up/down keys.
To support navigation using the left/right keys you need to add additional Key Bindings to the combo box:
InputMap im = comboBox.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
im.put(KeyStroke.getKeyStroke("LEFT"), "selectPrevious");
im.put(KeyStroke.getKeyStroke("RIGHT"), "selectNext");
However, the popup is based on the width of the largest item added to the combo box. This will be a problem as you won't see all the items. The items will scroll as you use the keys to navigate, but you won't see all 9 items at one time.
To solve this problem you check out Combo Box Popup. It has features that allow you to control the size/behaviour of the popup. You would use:
BoundsPopupMenuListener listener = new BoundsPopupMenuListener(true, false);
comboBox.addPopupMenuListener( listener );
It is possible to do this by giving the JComboBox a custom UI. However that UI is, at least in my case, a mess, so only use it as a workaround and clearly mark it as such with inline documentation.
The following code is made for the default Swing Look and Feel (Metal). If you want to use another L&F you need to exchange the class the custom UI is extending and maybe do some other adjustements. This will get tricky for distribution specific L&F (may force you to use reflection and delegation).
(If you also want to center your images, see this answer.)
Code:
(Sorry for the wierd comment wrapping, don't know whats wrong with my eclipse.)
To set the UI:
comboBox.setUI(new WrapComboBoxUI(3)); // 3 columns
Source code of WrapComboBoxUI:
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.ComboPopup;
import javax.swing.plaf.metal.MetalComboBoxUI;
public class WrapComboBoxUI extends MetalComboBoxUI {
private int columnCount;
public WrapComboBoxUI() {
this(0);
}
/**
* #param columnCount
* the amount of items to render on one row. <br/>
* A value of 0 or less will cause the UI to fit as many items
* into one row as possible.
*/
public WrapComboBoxUI(int columnCount) {
this.columnCount = columnCount;
}
public static ComponentUI createUI(JComponent c) {
return new WrapComboBoxUI();
}
#Override
protected ComboPopup createPopup() {
ComboPopup created = super.createPopup();
try {
if (created instanceof JPopupMenu) {
JPopupMenu popup = (JPopupMenu) created;
JScrollPane scroll = (JScrollPane) popup.getComponent(0);
JList<?> elementList = (JList<?>) scroll.getViewport().getView();
elementList.setLayoutOrientation(JList.HORIZONTAL_WRAP);
elementList.setVisibleRowCount(-1);
new Thread() {
#Override
public void run() {
while (true) {
try {
int fixedWidth = -1;
if (columnCount > 0) {
int width = elementList.getWidth() - elementList.getInsets().left - elementList.getInsets().right;
fixedWidth = width / columnCount;
}
boolean changed = false;
if (fixedWidth < 0 && elementList.getFixedCellWidth() >= 0 || fixedWidth >= 0 && elementList.getFixedCellWidth() < 0) {
// if changed from not fixed to fixed or
// other way around
changed = true;
} else if (fixedWidth > 0 && fixedWidth - elementList.getFixedCellWidth() > 1) {
// if width itself changed, ignoring slight
// changes
changed = true;
}
final int width = fixedWidth;
// no need to loop again before this is done, so
// we wait
if (changed)
SwingUtilities.invokeAndWait(() -> elementList.setFixedCellWidth(width));
sleep(100);
} catch (Throwable e) {
// ignored
}
}
};
}.start();
}
} catch (Throwable e) {
System.err.println("Failed to customize ComboBoxUI:");
e.printStackTrace();
}
return created;
}
}
I have a column 'Lifecycle' in my NAT Table based on which i have to set respective color to each row.
Adding color to row works fine. The problem is when i use the scroll bar to scroll either left or right, the color disappears. I am not aware what i am missing. Kindly help me if you have any idea of how it can be resolved
My code looks like:
IConfigLabelAccumulator cellLabelAccumulator = new IConfigLabelAccumulator() {
#Override
public void accumulateConfigLabels(final LabelStack configLabels, final int columnPosition,
final int rowPosition) {
Object dataValueByPosition = PhysicalDimensionNatTable.this.bodyLayer.getDataValueByPosition(10, rowPosition);
if ((dataValueByPosition != null) && dataValueByPosition.equals("Valid")) {
configLabels.addLabel("VALID");
}
if ((dataValueByPosition != null) && dataValueByPosition.equals("Invalid")) {
configLabels.addLabel("INVALID");
}
if ((dataValueByPosition != null) && dataValueByPosition.equals("Obsolete")) {
configLabels.addLabel("OBSOLETE");
}
}
};
this.bodyLayer.setConfigLabelAccumulator(cellLabelAccumulator);
this.natTable.addConfiguration(new AbstractRegistryConfiguration() {
#Override
public void configureRegistry(final IConfigRegistry configRegistry) {
Style cellStyle = new Style();
cellStyle.setAttributeValue(CellStyleAttributes.BACKGROUND_COLOR, GUIHelper.COLOR_GREEN);
configRegistry.registerConfigAttribute(CellConfigAttributes.CELL_STYLE, cellStyle, DisplayMode.NORMAL, "VALID");
cellStyle = new Style();
cellStyle.setAttributeValue(CellStyleAttributes.BACKGROUND_COLOR, GUIHelper.COLOR_RED);
configRegistry.registerConfigAttribute(CellConfigAttributes.CELL_STYLE, cellStyle, DisplayMode.NORMAL,
"INVALID");
cellStyle = new Style();
cellStyle.setAttributeValue(CellStyleAttributes.BACKGROUND_COLOR, GUIHelper.COLOR_YELLOW);
configRegistry.registerConfigAttribute(CellConfigAttributes.CELL_STYLE, cellStyle, DisplayMode.NORMAL,
"OBSOLETE");
}
});
The issue that you are facing is that the IConfigLabelAccumulator is registered on the bodyLayer, and I assume that is a stack where the top most layer is the ViewportLayer. bodyLayer.getDataValueByPosition(10, rowPosition); is returning the data value of the column position 10. And the underlying cell changes on scrolling as for example the column with index 11 becomes the column at position 10 if the first column moves out of the visible range. That is the index-position-transformation which is a basic concept in NatTable.
Either you need to perform a transformation calculation e.g. via LayerUtil to get the index, or operate on the DataLayer of the body directly instead of the bodylayer stack. Then you don't need to consider the index-position-transformation. I typically suggest to use the later.
As the index-position-transformation handling is too abstract for several people, another option is to operate on the object on the DataLayer. For this the IConfigLabelAccumulator needs to know the IRowDataProvider to be able to access the row object. And then register it on the DataLayer.
An example would look like the following snippet. Of course it should be transferred to your solution with better class separation.
IRowDataProvider<PersonWithAddress> bodyDataProvider = new ListDataProvider<>(data, accessor);
DataLayer bodyDataLayer = new DataLayer(bodyDataProvider);
bodyDataLayer.setConfigLabelAccumulator(new IConfigLabelAccumulator() {
#Override
public void accumulateConfigLabels(LabelStack configLabels, int columnPosition, int rowPosition) {
PersonWithAddress person = bodyDataProvider.getRowObject(rowPosition);
if ("Simpson".equals(person.getLastName())) {
configLabels.addLabel("YELLOW");
}
}
});
I have a table with a million rows.
If I select all rows and Copy, it takes approximately 5 minutes before responsiveness returns to the GUI and before I can paste it into another application. This is less than great, so I started looking for improvements.
If I make an alternative thread-safe TableModel, then I can background the operation of building the string and then push to the clipboard (on the EDT, if need be). Then the GUI is responsive, but it is still 5 minutes before I can paste into another application, which in some ways is a worse experience, because at least when it blocked the EDT, I was able to tell when the operation finished. I know I could add GUI feedback for it, but it seems wrong for an action like Copy.
If I look at the same thing happening between native applications, I notice that I can copy an enormous amount of data from one application with no delay. I can then immediately paste it into another application with no delay, but just with the caveat that it can take a long time for all the information to end up in there. For instance, if I paste huge text from TextEdit into Terminal, it seems to do this a bit at a time somehow.
Is there some way to do that in AWT as well?
I attempted to do it by only declaring the alternate flavours returning Reader and InputStream, but it turns out that even if you do this, it asks for the reader one, fetches the reader and then immediately reads the entire stream, storing it as a string. So this just moves the 5 minute delay to code inside the JRE, solving nothing. This is also fairly stupid, because there is this whole framework around what type of data transfer methods your Transferable supports, yet no matter which type you expose, the system seems to convert it down to the same type, which in the best case will take forever to build, or in the worst case will run you out of heap.
Maybe there is a type which doesn't though, so I thought I'd post this question to ask whether anyone knows.
Example of one TransferHandler, although the built-in one on JTable itself exhibits the same issue.
private class CombinedTransferHandler extends TransferHandler {
#Override
protected Transferable createTransferable(JComponent component) {
if (component instanceof JTable) {
JTable table = (JTable) component;
int[] rows = table.getSelectedRows();
if (!table.getRowSelectionAllowed()) {
int rowCount = table.getRowCount();
rows = new int[rowCount];
for (int counter = 0; counter < rowCount; counter++) {
rows[counter] = counter;
}
}
// columns omitted because we know we use all
return new BasicTransferable(getPlainData(rows), null);
}
return null;
}
#Override
public int getSourceActions(JComponent c) {
return COPY;
}
private String getPlainData(int[] rows) {
StringBuilder result = new StringBuilder();
int staticColumnCount = staticTable.getColumnCount();
int mainColumnCount = mainTable.getColumnCount();
for (int row : rows) {
for (int viewColumn = 0; viewColumn < staticColumnCount;
viewColumn++) {
if (viewColumn > 0) {
result.append('\t');
}
Object value = staticTable.getValueAt(row, viewColumn);
value = (value == null) ? "" : value.toString();
result.append(value);
}
for (int viewColumn = 0; viewColumn < mainColumnCount;
viewColumn++) {
if (staticColumnCount > 0 || viewColumn > 0) {
result.append('\t');
}
Object value = mainTable.getValueAt(row, viewColumn);
value = (value == null) ? "" : value.toString();
result.append(value);
}
result.append('\n');
}
return result.toString();
}
}
I have two tables, and i want to select appropriate row in first table when i select row in second table. I've done this as following
for (DataRow<Cell> row : formDataTable.getVisibleItems()) {
if (row.getIndex().equals(rowIndex.intValue())) {
formDataTable.getSelectionModel().setSelected(row, true);
}
}
But i don't see it as visual selected row. I see selected row from second table, but i should see it on first.
It done through a setSelected.
Make sure that you use a right selectionModel for yours dataGrid.
Use setSelected method of dataGrid
If you want to have opportunity selecting row on other pages you should use ProvidesKey and manualy set necessary page
My code below
// variables
#UiField
DataGrid<DataRow<Cell>> formDataTable;
public static final ProvidesKey<DataRow<Cell>> KEY_PROVIDER = new ProvidesKey<DataRow<Cell>>() {
#Override
public Object getKey(DataRow<Cell> item) {
return item.getIndex();
}
};
private SingleSelectionModel<DataRow<Cell>> selectionModel;
// this code must be within constructor
selectionModel = new SingleSelectionModel<DataRow<Cell>>(KEY_PROVIDER);
formDataTable.setSelectionModel(selectionModel);
// method to select should view as
#Override
public void setFocus(final Long rowIndex) {
DataRow<Cell> row = new DataRow<Cell>();
row.setIndex(rowIndex.intValue());
selectionModel.setSelected(row, true);
// go to essential page
Long page = rowIndex / pager.getPageSize() + (rowIndex % pager.getPageSize() > 0 ? 1:0);
pager.setPage(page.intValue() - 1);
}
I have 5 JTables on different forms with arbitrary numbers of rows and I would like to have a label for each one that will show me the total number of rows in that table and also change color for 3 seconds when the row count changes. The color should go green if incrementing and red if decrementing. What would be the best way to implement this such that I do not need to duplicate too much code in each of my forms?
basically, you add a TableModelListener to the JTable's model and on receiving change events, update the corresponding labels as appropriate
some code:
public class TableModelRowStorage
// extends AbstractBean // this is a bean convenience lass of several binding frameworks
// but simple to implement directly
implements TableModelListener {
private int rowCount;
public TableModelRowStorage(TableModel model) {
model.addTableModelListener(this);
this.rowCount = model.getRowCount();
}
#Override
public void tableChanged(TableModelEvent e) {
if (((TableModel) e.getSource()).getRowCount() != rowCount) {
int old = rowCount;
rowCount = ((TableModel) e.getSource()).getRowCount();
doStuff(old, rowCount);
}
}
protected void doStuff(int oldRowCount, int newRowCount) {
// here goes what you want to do - all in pseudo-code
// either directly configuring a label/start timer
label.setText("RowCount: " + newRowCount);
label.setForeground(newRowCount - oldRowCount > 0 ? Color.GREEN : Color.RED);
timer.start();
// or indirectly by firing a propertyChange
firePropertyChange("rowCount", oldRowCount, newRowCount);
}
}