JTable - drag and drop - java

OK, this problem is out of my league. I'm trying to implement a GUI widget in swing that allows files to be dropped onto a JTable, and allows the rows of the JTable to be dragged for re-sorting. Think VLC's playlists or the one in iTunes.
I got dropping files from the OS (Explorer, Finder, etc.) working just fine, but I'm having an impossible time with re-arranging the rows of the table, once the files are in. The problem is that when I add a custom TransferHandler to the table, dragging from the table is instantly killed. Here is some example code:
import javax.swing.*;
public class TableTest
{
public static void main (String [] argv)
{
// setup table data
String [] columns = new String [] {"Foo", "Bar", "Baz", "Quux"};
String [][] data = new String [][] {{"A", "B", "C", "D"},
{"1", "2", "3", "4"},
{"i", "ii", "iii", "iv"}};
// create table
JTable table = new JTable(data, columns);
// set up drag and drop
table.setDragEnabled(true);
table.setDropMode(DropMode.INSERT_ROWS);
table.setFillsViewportHeight(true);
TransferHandler dnd = new TransferHandler() {
// here be code to handle drops, and one would
// presume drag exporting, too
};
table.setTransferHandler(dnd);
JScrollPane scroll = new JScrollPane(table);
// create and show window
JFrame window = new JFrame();
window.getContentPane().add(scroll);
window.pack();
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
}
Run this code as-is and you'll see that you cannot initiate a drag on the table.If you comment out the call to setTransferHandler() on the table, dragging works (i.e., when I start dragging a table row, I get the X'd out circle cursor saying I can't drop there). But as soon as a TransferHandler is set for the table, I can't drag any rows. The problem has to be in the TransferHandler, but I've thoroughly troubleshot and debugged it, and have determined that dragging is never started once there is a TransferHandler on the table. What am I doing wrong?

I had the same problem, it's got nothing to do with your custom implementation of the TransferHandler. When you replace the TransferHandler you also need to get a hold of the default DragSource and tell it to recognize drag gesture. You may also need to implement your own Transferable because you'll need to pass it to the DragGestureEvent.startDrag() method.
table.setTransferHandler(new MyTransferHandler());
table.setDragEnabled(true);
DragSource source = DragSource.getDefaultDragSource();
source.createDefaultDragGestureRecognizer(table, DnDConstants.ACTION_COPY, new DragGestureListener() {
#Override
public void dragGestureRecognized(DragGestureEvent dge) {
//grab the selected files from the table model
ArrayList<File> files = new ArrayList<File>();
for (int row : table.getSelectedRows()) {
files.add((File) dm.getValueAt(row, 1));
}
//FileTransferable is a custom Transferable implementation
Transferable transferable = new FileTransferable(files);
//and this is the magic right here
dge.startDrag(null,transferable);
}
});

It doesn't look like you are using the TransferHandler properly. Try to read through the tutorial here.
See the TransferHandler doc here. The empty constructor doesn't look like it's meant for usage outside a subclass of TransferHandler.
And you don't implement any of the functionality provided in the standard TransferHandler provided on Swing components. See exerpt from the DnD tutorial here (my bold):
Note: If you install a custom TransferHandler onto a Swing component, the default support is replaced. For example, if you replace JTextField's TransferHandler with one that handles colors only, you will disable its ability to support import and export of text.
If you must replace a default TransferHandler — for example, one that handles text — you will need to re-implement the text import and export ability. This does not need to be as extensive as what Swing provides — it could be as simple as supporting the StringFlavor data flavor, depending on your application's needs.

I think the problem is that the empty TransferHandler actually prevents DnD events from occurring. There is a sample here which may be relevant.
http://www.java2s.com/Code/Java/Swing-JFC/ExtendedDnDDragandDropDemo.htm

I did not want to get into the nuts and bolts of what was going on so I just delegated the methods I wasn't interested to the old TransferHandler.
tree.setDragEnabled(true);
tree.setDropMode(DropMode.XXXX);
tree.setTransferHandler(new MyTransferHandler(tree.getTransferHandler());
Start with a standard setup but pass the old TransferHandler into your custom TransferHandler.
private class MyTransferHandler extends TransferHandler {
private TransferHandler delegate;
public MyTransferHandler(TransferHandler delegate) {
this.delegate = delegate;
}
public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {
return delegate.canImport(comp, transferFlavors);
}
public boolean canImport(TransferSupport support) {
return true;
}
protected Transferable createTransferable(JComponent c) {
try {
Method method = delegate.getClass().getDeclaredMethod("createTransferable", JComponent.class);
method.setAccessible(true);
return (Transferable) method.invoke(delegate, c);
} catch (Exception e) {
return super.createTransferable(c);
}
}
public void exportAsDrag(JComponent comp, InputEvent event, int action) {
delegate.exportAsDrag(comp, event, action);
}
protected void exportDone(JComponent source, Transferable data, int action) {
try {
Method method = delegate.getClass().getDeclaredMethod("exportDone", JComponent.class, Transferable.class,
int.class);
method.setAccessible(true);
method.invoke(delegate, source, data, action);
} catch (Exception e) {
super.exportDone(source, data, action);
}
}
public int getSourceActions(JComponent c) {
return delegate.getSourceActions(c);
}
public Icon getVisualRepresentation(Transferable t) {
return delegate.getVisualRepresentation(t);
}
public boolean importData(JComponent comp, Transferable t) {
return delegate.importData(comp, t);
}
public boolean importData(TransferHandler.TransferSupport support) {
return delegate.importData(support);
}
}
One gotcha is that createTransferable(JComponent) and exportDone(JComponent, Transferable, int) methods are protected so you need to do reflection in order to delegate to those methods. When I didn't do this reflection delegation the strategy did not work. Once I did this delegation drag and drop worked as expected without changing the DragSource or having to write a new Transferable.

Related

JComboBox to display multiple lines of text

I'm currently writing a small tool for sending sql queries to a database and recieving the according data.
Now to my problem:
I want to allow the user to enter a new search query or select from a "latest" list, where the last few queries are saved.
For that, I planned on using an editable JComboBox, but I'm having trouble diplaying multiple lines of text in the box itself.
The reason I want to do that, is because sql queries can get quite long and since I want make the box editable and at the same time keep the frame clean.
I've found ways to display multiple lines in the dropdown menu, but nothing for the box itself.
Thank you in advance and please forgive me if I overlooked something simple ;)
Greetings
Zeus
Extended editing functionality is supplied by the ComboBoxEditor, this allows you to define the actual component which is used as the combobox's editor
Based on your requirements, you're going to need (at the very least) a JTextArea, to provide (optionally) word wrapping and multiple lines
A rough and ready example might look something like this...
public class TextAreaComboBoxEditor implements ComboBoxEditor {
private JTextArea ta = new JTextArea(4, 20);
private JScrollPane sp = new JScrollPane(ta);
public TextAreaComboBoxEditor() {
ta.setWrapStyleWord(true);
ta.setLineWrap(true);
}
#Override
public Component getEditorComponent() {
return sp;
}
#Override
public void setItem(Object anObject) {
if (anObject instanceof String) {
ta.setText((String) anObject);
} else {
ta.setText(null);
}
}
#Override
public Object getItem() {
return ta.getText();
}
#Override
public void selectAll() {
ta.selectAll();
}
#Override
public void addActionListener(ActionListener l) {
}
#Override
public void removeActionListener(ActionListener l) {
}
}
This doesn't support ActionListener, as JTextArea uses the Enter key for it's own purposes. If you wanted to, you could use the key bindings API to add your own Action that can trigger the ActionListeners, for that, you'd need to supply a List or other means for managing them so you can call them back

Why is my JTable always reported as empty using VoiceOver on OS X?

VoiceOver on OSX 10.10.4 (Yosemite), using JRE 1.7.0_75 and JRE 1.8.0_45, reports the following table as "empty".
package stackoverflow.examples.jtable;
import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
public class TableDemo extends JFrame
{
private static final long serialVersionUID = 1L;
public TableDemo()
{
super("Accessible JTable?");
final String[] columnNames =
{
"First Name",
"Last Name",
"Sport",
"# of Years",
"Vegetarian"
};
final Object[][] data =
{
{"Kathy", "Smith", "Snowboarding", new Integer(5), new Boolean(false)},
{"John", "Doe", "Rowing", new Integer(3), new Boolean(true)},
};
final JTable jTable = new JTable(data, columnNames);
jTable.getAccessibleContext().setAccessibleName("data table");
System.out.println("rows: " + jTable.getAccessibleContext().getAccessibleTable().getAccessibleRowCount());
System.out.println("cols: " + jTable.getAccessibleContext().getAccessibleTable().getAccessibleColumnCount());
System.out.println("java: " + System.getProperty("java.version"));
jTable.setOpaque(true);
setContentPane(jTable);
}
private static void createAndShowGUI()
{
final TableDemo frame = new TableDemo();
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
}
Apart from VoiceOver saying the table is empty, everything else seems OK:
VoiceOver picks up the name I've given the table using setAccessibleName()
The JTable correctly returns an instance of AccessibleTable.
Calling getAccessibleRowCount() and getAccessibleColumnCount() prints the expected values.
What am I missing?
Further info:
I've tried using JRE 1.6.0_65 which was the Apple supplied JRE and I get the same issue. I tried this in case it was something that changed when moving to the Oracle supplied JRE.
The reason why VoiceOver says "empty" is because the accessibility hierarchy is not being exposed correctly. You can use the Accessibility Inspector tool (one of the Xcode developer tools) to examine the accessibility hierarchy. In this case, with the Accessibility Inspector tool running, hovering the mouse pointer over the JTable shows that there is an AXTable element with 10 AXStaticText children (one for each of the cells). Tables should be exposed as AXTable > AXRow > AXCell > … . Also, according to the Roles reference, an AXTable element should have a Rows attribute among other required attributes, but these have not been exposed to the accessibility hierarchy by the JRE.
I have tried out your program on Windows 8.1 Pro using Java 1.8.0_51 and I see a similar problem. Similar to the Accessibility Inspector tool, the Windows SDK comes with an Inspect tool that can be used to inspect accessibility data. When running your test case, it appears that the JTable has not been exposed at all. Enabling Windows Narrator, I am unable to navigate to the table or its cells.
So, it appears that the JRE does not fully support table accessibility.
In the source of javax.accessibility.AccessibleRole, you can see that the code to define a ROW constant is commented out along with other constants which are documented as "under consideration for potential future use".
In the source of JTable, you can see that AccessibleJTable, AccessibleTableHeader, AccessibleJTableCell, and AccessibleJTableHeaderCell helper classes are defined, but there isn't an Accessible implementation for the rows of the table.
In theory you could write a custom AccessibleContext implementation that would operate to expose a more complete accessibility hierarchy for the JTable to the OS. However, I am not sure whether it is possible to completely work around Java's apparent lack of support for table accessibility.
Whether this is possible might depend on the platform. For example, examining the source code of src/macosx/native/sun/awt/JavaAccessibilityUtilities.m, you can see how Java's accessibility roles are mapped to the NSAccessibility*Role constants. You can see that the ROW_HEADER accessible role is mapped to AXRow. Thus, you could expose AXRow children of the AXTable by using the ROW_HEADER accessible role. Here is some code which succeeds in doing this:
public static class MyJTable extends JTable {
public MyJTable(TableModel tm) {
super(tm);
}
#Override
public MyAccessibleJTable getAccessibleContext() {
if (accessibleContext == null) {
accessibleContext = new MyAccessibleJTable();
}
return (MyAccessibleJTable)accessibleContext;
}
protected class MyAccessibleJTable extends AccessibleJTable {
#Override
public int getAccessibleChildrenCount() {
if (MyJTable.this.getColumnCount() <= 0) {
return 0;
}
return MyJTable.this.getRowCount();
}
#Override
public Accessible getAccessibleChild(int i) {
if (i < 0 || getAccessibleChildrenCount() <= i) {
return null;
}
TableColumn firstColumn = getColumnModel().getColumn(0);
TableCellRenderer renderer = firstColumn.getCellRenderer();
if (renderer == null) {
Class<?> columnClass = getColumnClass(0);
renderer = getDefaultRenderer(columnClass);
}
Component component = renderer.getTableCellRendererComponent(MyJTable.this, null, false, false, i, 0);
return new MyAccessibleRow(MyJTable.this, i, component);
}
}
protected static class MyAccessibleRow extends AccessibleContext implements Accessible {
private MyJTable table;
private int row;
private Component rendererComponent;
protected MyAccessibleRow(MyJTable table, int row, Component renderComponent) {
this.table = table;
this.row = row;
this.rendererComponent = rendererComponent;
}
#Override
public AccessibleRole getAccessibleRole() {
// ROW_HEADER is used because it maps to NSAccessibilityRowRole
// on Mac.
return AccessibleRole.ROW_HEADER;
}
#Override
public Locale getLocale() {
AccessibleContext ac = rendererComponent.getAccessibleContext();
if (ac != null) {
return ac.getLocale();
} else {
return null;
}
}
#Override
public int getAccessibleChildrenCount() {
return 0; // TODO return the number of columns in this row
}
#Override
public Accessible getAccessibleChild(int i) {
return null; // TODO return a MyAccessibleJTableCell
}
#Override
public int getAccessibleIndexInParent() {
return row;
}
#Override
public AccessibleStateSet getAccessibleStateSet() {
return null; // TODO
}
#Override
public AccessibleContext getAccessibleContext() {
return this; // TODO
}
#Override
public AccessibleComponent getAccessibleComponent() {
return table.getAccessibleContext(); // TODO
}
}
}
As you can see from this screenshot:
.. there are now two AXRow children on the AXTable. However, VoiceOver still announces the table as "empty". I am not sure whether this is because the rows do not have AXCell children, or because the AXTable is missing its required attributes, or some other reason.
If you were to go the custom AccessibleContext route, it would probably be better to completely avoid trying to expose a table hierarchy. Instead, you could model the table as a list, where each row corresponds to a list item, and each list item contains a group for each of the cells. This is similar to the approach used by Firefox (tested version 39.0). Currently on Mac, Firefox does not use the table role when exposing an HTML table. This should be fixed in Firefox 41, though. See Bug 744790 - [Mac] HTML table semantics are not communicated to VoiceOver at all.
I am also using Mac OS 10.10.4 and Java 1.8.0_51.
EDIT: The "empty" table issue has already been reported as OpenJDK Bug JDK-7124284 [macosx] Nothing heard from VoiceOver when navigating in a table. From the comments, there are several known issues with Mac Swing accessibility that are currently deferred to JDK 9.
Another possible work around is to use the JavaFX TableView class. Trying out the TableView sample from http://docs.oracle.com/javafx/2/ui_controls/table-view.htm I am seeing that VoiceOver is properly announcing the table. JavaFX accessibility was implemented as part of JEP 204 which was implemented in Java 8u40.
Actually Java accessibility comes with Java 7 and Java 8 packages and for earlier Java versions you need to install it manually.
Whereas if you will look at the java documentation you will see they recommend few softwares like JAWS, NonVisual Desktop Access (NVDA), SuperNova, Window-Eyes etc. over OS default narrator software. But that's strictly for Windows.
http://docs.oracle.com/javase/7/docs/technotes/guides/access/enable_and_test.html
Since you are running onto OSX 10.10.4 (Yosemite) OS, you are not required to alter anything. But you may still try one of the softwares listed.

Remove HTML from JTable CellEditor

I have a JTable and some editable cells which are dynamically formatted with very specific HTML based on some business rules.
However, when you edit these cells all the HTML is in the CellEditor. I just want the plain text in the CellEditor.
I am trying to do this to the entire table. Here is the code I used. I threw together an extended DefaultCellEditor but its still showing the HTML. I don't even see the debugger entering the getCellEditorValue() method. What do I do?
public class MyTable extends JTable
{
public MyTable()
{
MyTable.setCellEditor(new DefaultCellEditor(new JTextField())
{
#Override
public Object getCellEditorValue() {
// get content of textField
String str = (String) super.getCellEditorValue();
if (str == null) {
return null;
}
if (str.length() == 0) {
return null;
}
//remove HTML and return plain text
return Jsoup.parse(str).text();
}
});
}
}
I'm not sure where things are going awry; a complete example may shed some light. The normal editing sequence is outlined here, suggesting that you should probably create your own renderer and editor:
class MyRenderer extends DefaultTableCellRenderer {…}
class MyEditor extends DefaultCellEditor {…}
and apply them as follows:
table.setDefaultRenderer(String.class, new MyRenderer());
table.setDefaultEditor(String.class, new MyEditor());
Be certain that your TableModel returns the correct type token from getColumnClass().

How to add a function in jTable that sorts the column?

I know that by using JTable the column is sorted when we click on the column heading, but what I want is that, when I right-click on the column name a function name 'sort' should be displayed. Any suggestion in doing it?
Start by adding a MouseListener to the table. See How to write mouse listeners
You will need to translate the click point to a column, see JTable#columnAtPoint.
You will then need to update the SortKey for the table. Check out Sorting and Filtering for an example
If I understand you correctly, you want to sort by some explicit action (triggered f.i. in a popup) instead of by the normal left-click.
If so, the tricky part is to force the ui-delegate to do nothing. There are two options:
hook into the default mouse listener installed by the ui delegate, as described in a recent QA
let the ui do its stuff, but fool it by a sorter implementation that doesn't follow the rules (beware: that's as dirty as the first approach!)
The mis-behaving sorter:
public class MyTableRowSorter extends TableRowSorter {
public MyTableRowSorter(TableModel model) {
super(model);
}
/**
* Implemented to do nothing to fool tableHeader internals.
*/
#Override
public void toggleSortOrder(int column) {
}
/**
* The method that really toggles, called from custom code.
*
* #param column
*/
public void realToggleSortOrder(int column) {
super.toggleSortOrder(column);
}
}
// usage
final JTable table = new JXTable(new AncientSwingTeam());
table.setRowSorter(new MyTableRowSorter(table.getModel()));
Action toggle = new AbstractAction("toggleSort") {
#Override
public void actionPerformed(ActionEvent e) {
JXTableHeader header = SwingXUtilities.getAncestor(
JXTableHeader.class, (Component) e.getSource());
Point trigger = header.getPopupTriggerLocation();
int column = trigger != null ? header.columnAtPoint(trigger) : -1;
if (column < 0) return;
int modelColumn = header.getTable().convertColumnIndexToModel(column);
((MyTableRowSorter) header.getTable().getRowSorter())
.realToggleSortOrder(modelColumn);
}
};
JPopupMenu menu = new JPopupMenu();
menu.add(toggle);
table.getTableHeader().setComponentPopupMenu(menu);
Yeah, couldn't resist throwing in some SwingX api, lazy me :-) With plain Swing, you'll have to write some lines more but the basics are the same: install the tricksy sorter and use its custom toggle sort to really sort whereever needed, f.i. in a mouseListener.

JTable: single selection for the user, multiple selections programmatically

I have a JTable where the user should be able to select only a single row, but whenever a row is selected by the user, some other rows (that are related according to some logic) should also be selected programmatically. The problem is that if I set the selection mode of the table to ListSelectionModel.SINGLE_SELECTION, addRowSelectionInterval will also select only one row. Any ideas?
EDIT: I think all ideas (custom selection model, clearing all but last user selections, custom renderer for highlighting) were good, but the best is to use SwingX, because it doesn't require much infrastructure-code, only a clever usage of the library. (and it's easy to be clever when a SwingX-guru is helping :)
Biased me would say: certainly much easier in SwingX :-)
All you need is
a custom HighlightPredicate which decides about what is related
a ColorHighlighter configured with the selectionColors
set the custom predicate on receiving change notification from the selection model
Some code:
// the custom predicate
public static class RelatedHighlightPredicate implements HighlightPredicate {
List<Integer> related;
public RelatedHighlightPredicate(Integer... related) {
this.related = Arrays.asList(related);
}
#Override
public boolean isHighlighted(Component renderer,
ComponentAdapter adapter) {
int modelIndex = adapter.convertRowIndexToModel(adapter.row);
return related.contains(modelIndex);
}
}
// its usage
JXTable table = new JXTable(someModel);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
final ColorHighlighter hl = new ColorHighlighter(HighlightPredicate.NEVER,
table.getSelectionBackground(), table.getSelectionForeground());
table.addHighlighter(hl);
ListSelectionListener l = new ListSelectionListener() {
#Override
public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting()) return;
invokeUpdate((ListSelectionModel) e.getSource());
}
private void invokeUpdate(final ListSelectionModel source) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
int singleSelection = source.getMinSelectionIndex();
if (singleSelection >= 0) {
int first = Math.max(0, singleSelection - 2);
int last = singleSelection + 2;
hl.setHighlightPredicate(new RelatedHighlightPredicate(first, last));
} else {
hl.setHighlightPredicate(HighlightPredicate.NEVER);
}
}
});
}
};
table.getSelectionModel().addListSelectionListener(l);
You might set multiselection possible for the table, but with each selection change - take only 1 (last selected) row, clear other selections and add your own computed selection.
The problem is that if I set the selection mode of the table
use ListSelectionModel.SINGLE_SELECTION for events came from mouse and keyborad
some other rows (that are related according to some logic) should also be selected programmatically
have look at Renderer for JTable, then required row(s), columns or whatever could be highlighted until programmatic rules stay unchanged
... maybe will help you

Categories