I am showing some results in a JTable that consists of 2 columns.
File - Result
I implemented a JPopupMenu which displays a copy entry, and I try to copy the value of the cell, where I right-clicked.
filelistTable.addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
if(SwingUtilities.isRightMouseButton(e))
{
TablePopupMenu popup = new TablePopupMenu(filelistTable, e.getPoint());
filelistTable.setComponentPopupMenu(popup);
}
}
});
--
public TablePopupMenu(JTable table, Point p) {
this.table = table;
this.p = p;
JMenuItem mntmKopieren = new JMenuItem("Kopieren");
mntmKopieren.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
copyCellToClipboard();
}
});
add(mntmKopieren);
}
public void copyCellToClipboard()
{
int r = table.rowAtPoint(p);
int c = table.columnAtPoint(p);
System.out.println(table.getValueAt(table.convertRowIndexToView(r),
table.convertRowIndexToView(c)));
StringSelection entry = new StringSelection(table.getValueAt(table.convertRowIndexToView(r),
table.convertRowIndexToView(c)).toString());
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents( entry, this );
}
Anyhow, this only works for a small number of tests.
Did I do something wrong or something missing? It looks to me, as if the cell will not even get choosen correctly.
Two thingies are slightly off:
setting the componentPopup in the clicked is too late in the sequence of mouseEvents (popups are typically triggered on pressed or released which happen before the click)
the value is taken from the incorrect cell: all coordinates in a JTable are in view coordinate system, converting them to view coordinates will be completely off
That said: getting cell-coordinate related context is poorly supported. Often, the best bet is to (code snippet below)
override getPopupLocation(MouseEvent) and store the location somewhere
implement a popup/action to access the location
Fails if (as should be done in a well-behaved application), the popup could be triggered by keyboard: if that's the case, you'll need to provide some other marker (f.i. the focused cell) to act on.
final String popupLocation = "table.popupLocation";
final JTable table = new JXTable(new AncientSwingTeam()) {
#Override
public Point getPopupLocation(MouseEvent event) {
// event may be null if triggered by keyboard, f.i.
// thanks to #Mad for the heads up!
((JComponent) event.getComponent()).putClientProperty(
popupLocation, event != null ? event.getPoint() : null);
return super.getPopupLocation(event);
}
};
JPopupMenu popup = new JPopupMenu();
Action printLocation = new AbstractAction("print cell") {
#Override
public void actionPerformed(ActionEvent e) {
Point p = (Point) table.getClientProperty(popupLocation);
if (p != null) { // popup triggered by mouse
int row = table.rowAtPoint(p);
int column = table.columnAtPoint(p);
LOG.info("" + table.getValueAt(row, column));
} else { // popup triggered otherwise
// could choose f.i. by leadRow/ColumnSelection
...
}
}
};
popup.add(printLocation);
table.setComponentPopupMenu(popup);
Edit (triggered by Mad's comment):
You should be checking MouseEvent.isPopupTrigger as the trigger point is platform dependent. This does mean you need to monitor mousePressed, mouseReleased and mouseClicked
No, that's not needed (just checked :-): the mechanism that shows the componentPopup in response to a mouseEvent - happens in BasicLookAndFeel.AWTEventHelper - only does so if it is a popupTrigger.
By reading the api doc (should have done yesterday ;-) again, it turns out that the method is called always before showing the componentPopup, that is also if triggered by other means, f.i. keyboard. In that case the event param is null - and the original code would blow. On the bright side, with that guarantee, all the logic of finding the target cell/s could be moved into that method. Didn't try though, so it might not be feasable (f.i. if then the location should be based on the leadRow/ColumnSelection that might not yet be fully handled at that time)
Related
When I receive a ChangeEvent from my JSpinner, I'd like to detect if user used the arrows to increase/decrease the number value, or directly typed a new number.
What would be the best approach to do this ?
EDIT: for now my non-reliable solution is just to save the last JSpinner changed value, and if new change value is +/- equals to the step value, then I assume user clicked on an arrow. It works except if user typed a value which is equals to (oldValue +/- step).
EDIT: why ?
I want to reproduce the behavior found in Midi editors of several famous DAWs. The JSpinner represents the velocity (0-127) of selected notes. It shows the velocity of the first selected note. Usually notes velocity differ. When you increase with arrow, you want to increase all selected notes by the same amount. When you type in a new value, you want all velocities to be reset to this value.
Distinguishing the trigger of a value change is not supported - the only value-related event fired by JSpinner is a ChangeEvent which carries no state except the source. We need another type of listener or a combination of listeners.
First thought: listen to changes of the editor's textField, f.i. an actionListener or a propertyChangeListener on the value property. This doesn't work, mainly because
both change- and propertyChangeListener are fired always (change before property)
actionListener is not fired on focusLost
Second thought: go dirty and dig into implementation details to hook into the action fired by the arrow buttons.
The idea:
look up the action for increment/decrement from the spinner's actionMap: this is the same as the arrows' actions and also used by up/down keys (which I assume not counting a "editing")
for each, create a wrapper that sets a flag before delegating to super
put that wrapper into the spinner's actionMap
look up the arrow buttons in the spinner's children and replace their respective actionListener with the wrapper
Client code would change the tweak's changeListener to acts according to the flag as appropriate.
Some code doing the tweaking (beware: not formally tested!):
public static class SpinnerTweaker {
private JSpinner spinner;
private boolean wasButtonAction;
private Object oldValue;
public SpinnerTweaker(JSpinner spinner) {
this.spinner = spinner;
AbstractAction incrementDelegate = createDelegate("increment");
spinner.getActionMap().put("increment", incrementDelegate);
AbstractAction decrementDelegate = createDelegate("decrement");
spinner.getActionMap().put("decrement", decrementDelegate);
// replacing arrow button's action
Component[] components = spinner.getComponents();
for (Component component : components) {
if (component instanceof JButton) {
if (component.getName() == "Spinner.nextButton") {
ActionListener[] actions = ((JButton) component).getActionListeners();
ActionListener uiAction = actions[0];
((JButton) component).removeActionListener(uiAction);
((JButton) component).addActionListener(incrementDelegate);
}
if (component.getName() == "Spinner.previousButton") {
ActionListener[] actions = ((JButton) component).getActionListeners();
ActionListener uiAction = actions[0];
((JButton) component).removeActionListener(uiAction);
((JButton) component).addActionListener(decrementDelegate);
}
}
}
spinner.addChangeListener(e -> {
if (wasButtonAction) {
System.out.println("value changed by button: " + spinner.getValue());
} else {
System.out.println("value changed by editing: " + spinner.getValue());
}
wasButtonAction = false;
});
}
protected AbstractAction createDelegate(String actionCommand) {
// hooking into original button action to set button flag
AbstractAction action = (AbstractAction) spinner.getActionMap().get(actionCommand);
AbstractAction delegate = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
oldValue = spinner.getValue();
wasButtonAction = true;
action.actionPerformed(e);
// hit min/max - TBD: needs testing!
if (oldValue.equals(spinner.getValue())) {
wasButtonAction = false;
}
}
};
return delegate;
}
}
#kleopatra solution works but I found a simpler solution.
The trick is that commitEdit() is only called internally by JSpinner when change results from the increment or decrement action.
public class Spinner2 extends JSpinner
{
// To be checked by ChangeListener after receiving the ChangeEvent
public boolean wasManualEdit=true;
#Override
public void commitEdit() throws ParseException
{
wasManualEdit = false;
super.commitEdit();
}
#Override
protected void fireStateChanged()
{
super.fireStateChanged();
wasManualEdit = true;
}
}
If a Java Swing JTextField gets the focus, all its text is marked.
Now, I would like to modify the behaviour, so that if the user now presses the right arrow, the cursor should be set to the texts beginning (position 0).
I'm able to get the RIGHT Key event and set the cursor to position 0,
but I don't know how to pass the RIGHT Key event to the original code so that it can handle the normal behaviour.
JTextField preisFieldEUR = new JTextField(…);
AbstractAction right = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
if (preisFieldEUR.getSelectionEnd() - preisFieldEUR.getSelectionStart() == preisFieldEUR.getText().length()) {
preisFieldEUR.setSelectionStart(0);
preisFieldEUR.setSelectionEnd(0);
} else {
// Todo: How to pass this event to the original keyboard handler to
// keep the normal behaviour?
}
}
};
preisFieldEUR.getInputMap().put(KeyStroke.getKeyStroke("RIGHT"), right);
Thanks a lot for any help!
You need something like this:
Action oldAction = preisFieldEUR.getActionMap().get("caret-forward"); // probably another parameter?
AbstractAction right = new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
if (preisFieldEUR.getSelectionEnd() - preisFieldEUR.getSelectionStart() == preisFieldEUR.getText().length()) {
preisFieldEUR.setSelectionStart(0);
preisFieldEUR.setSelectionEnd(0);
} else {
oldAction.actionPeformed(e)
}
}
};
I´m trying to implement an undo (and redo) function for an editable JTable with the default components. The JTable has an extra class to specify its properties called SpecifiedJTable.
To do so I wanted to grab the moment when a cell is doubleclicked (i.e. the moment when a cell is chosen/marked to be edited) to push the information in the cell and its coordinates onto the stack.
This should be done by a MouseListener ...at least that was my idea.
I tried this (standing in the constructor of my SpecifiedJTable class)
class JTableSpecified extends JTable {
private static final long serialVersionUID = 1L;
private int c; // the currently selected column
private int r; // the currently selected row
public JTableSpecified(String[][] obj, String[] columnNames) {
super(obj, columnNames); // constructs the real table
// makes that you can only select one row at a time
this.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
// makes that columns are not squeezed
this.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
// forbids to rearrange the columns
getTableHeader().setReorderingAllowed(false);
// adds action listener
this.getModel().addTableModelListener(new TableModelListener() {
public void tableChanged(TableModelEvent e) {
r = getSelectedRow();
c = getSelectedColumn();
// get the String at row r and column c
String s = (String) getValueAt(r, c);
if (jobDisplayed) jobSwitch(c, s);
else resSwitch(c, s);
}
});
this.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
System.out.println("test");
}
}
});
}
}
but somehow the clickCounter doesn´t want to reach anything that´s higher than 1.
I am glad about any answer and help. Thanks.
The problem you are experiencing is related to use of mouseClicked() rather than using mousePressed(). In this case it appears to be very hard to increase the click counter, yet still it is possible. It took me lots of clicking and also mouse movement to increase the click counter over 1. You could try it by yourself, in your code. To get the counter over 1 you need to go crazy on the mouse by pressing & releasing fast while moving the mouse from cell to cell at the same time (or maybe I was just luckily clicking between the cells?).
As you can see in this fully working sample, made from your code, two mouse presses, using the mousePressed() method are being detected just fine.
public class JTableSpecified extends JTable {
private static final long serialVersionUID = 1L;
public JTableSpecified(String[][] obj, String[] columnNames) {
super(obj, columnNames); // constructs the real table
// makes that you can only select one row at a time
this.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
// makes that columns are not squeezed
this.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
// forbids to rearrange the columns
getTableHeader().setReorderingAllowed(false);
// adds action listener
this.getModel().addTableModelListener(new TableModelListener() {
#Override
public void tableChanged(TableModelEvent e) {
}
});
this.addMouseListener(new MouseAdapter() {
#Override
public void mousePressed(MouseEvent e) {
if (e.getClickCount() == 2) {
System.out.println("test");
}
System.out.println("e.getClickCount() = " + e.getClickCount());
}
});
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JPanel panel = new JPanel();
panel.add(new JTableSpecified(new String[][]{{"oi", "oi2"}, {"oi3", "oi4"}}, new String[]{"Col1", "Col2"}));
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setContentPane(panel);
f.pack();
f.setVisible(true);
}
});
}
}
Conclusion: Maybe you in fact want to use the mousePressed() method?
This answer extends Boro´s answer.
To catch every case that enables the user to edit the table I will also need to add a KeyListener for F2 (which has the same effect as double clicking onto a cell) and disable the automatic cell editing by pressing any key.
I just added it to the constructor right behind the mouseListener (see above)
// forbids the editing by striking a key
this.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE);
// keyListener to react on pressing F2 (key code 113)
this.addKeyListener(new KeyAdapter(){
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == 113) System.out.println("test");
}
});
The BasicTableUI is responding to the double-click by going into an edit mode on the cell that was double-clicked. It does lots of complicated stuff, part of which involves creating a JTextField (or other component) to allow the data to be edited, and then preventing the mouse click event from propagating any further.
If your table, or that table cell, is not editable, you can easily capture mouse events with click count 2, 3, 4, .... But since you want your table to be editable, you need a different approach.
One idea would be to override JTable.editCellAt()
A better idea is to forget about messing with the JTable and instead listen for data changes on the table model itself.
the error in the code is that the mouseClicked method is called as soon as the first click takes place. when a double click takes place the mouseClicked method is called again. you can place a static variable (or a class variable) for the earlier click event storing the time (using the e.getWhen() method).
Check for the time difference and if it's small enough, execute your actions (I'd suggest calling a doubleClick method).
you may have to implement mouse listener in your class JTableSpecified since a static variable might not be placed in your existing code.
I have enabled tooltips in my JTable by overriding the JComponent method that the JTable inherits:
public String getToolTipText(MouseEvent e) { ... }
Now, suppose a user hovers over a cell, the tooltip appears, and then (s)he starts editing the cell, I want to forcefully dismiss the tooltip.
Currently, the tooltip just hangs around until the value that I specified using ToolTipManager#setDismissDelay expires. The tooltip can sometimes obscure the view of the cell being edited which is why I want to dismiss it the moment any cell on the table goes into edit mode.
I tried the following approach (this is pseudo-code)
public String getToolTipText(MouseEvent e)
{
if(table-is-editing)
return null;
else
return a-string-to-display-in-the-tooltip;
}
Of course, this only had the effect of NOT showing tooltips of the table is ALREADY in edit mode. I knew this wouldn't work, but it was more of a shot in the dark.
You can show/hide a tooltip by using code like:
//Action toolTipAction = component.getActionMap().get("postTip");
Action toolTipAction = component.getActionMap().get("hideTip");
if (toolTipAction != null)
{
ActionEvent ae = new ActionEvent(component, ActionEvent.ACTION_PERFORMED, "");
toolTipAction.actionPerformed( ae );
}
You couuld probably override the prepareCellEditor(...) method of JTable to add this code and it should hide any tooltip before displaying the editor.
Edit:
In response to Kleopatra's comment I then add the following to make sure the Action is added to the ActionMap:
table.getInputMap().put(KeyStroke.getKeyStroke("pressed F2"), "dummy");
ToolTipManager.sharedInstance().registerComponent( table );
Was about to comment "something wrong with your" - but remembered a use case when not hiding the Tooltip on starting edits may happen :-)
Some facts:
tooltips are hidden on mouseExit and on focusLost the component which is registered with the ToolTipManager
when starting an edit and the editing component gets focus so the tooltip is hidden automatically
by default, JTable does not yield focus to the editing component is the editing is started by typing into the cell (as opposed by double-click or F2): in this case no focusLost is fired and consequently the tooltip not hidden
the ToolTipManager indeed installs a hideAction which might be re-used (as #camickr mentioned). But - that action is installed only if the component has a inputMap of type WHEN_FOCUSED. Which is not the case for JTable (all its bindings are in WHEN_ANCESTOR)
So it requires a handful of tweaks to implement the required behaviour, below is a code snippet (note to myself: implement in SwingX :-)
JTable table = new JTable(new AncientSwingTeam()) {
{
// force the TooltipManager to install the hide action
getInputMap().put(KeyStroke.getKeyStroke("ctrl A"),
"just some dummy binding in the focused InputMap");
ToolTipManager.sharedInstance().registerComponent(this);
}
#Override
public boolean editCellAt(int row, int column, EventObject e) {
boolean editing = super.editCellAt(row, column, e);
if (editing && hasFocus()) {
hideToolTip();
}
return editing;
}
private void hideToolTip() {
Action action = getActionMap().get("hideTip");
if (action != null) {
action.actionPerformed(new ActionEvent(
this, ActionEvent.ACTION_PERFORMED, "myName"));
}
}
};
Check out this JTable tutorial. In particular this webstart. There are two editable columns with tooltips - 'Sport' and 'Vegetarian' work just fine. Are you using any custom cell renderers?
This worked for me and seems simpler than using an action:
ToolTipManager.sharedInstance().mouseExited(new MouseEvent(myJframe, 0, 0, 0, 0, 0, 0, 0, 0, false, 0));
This seems to hide any tooltip shown inside the specified JFrame.
With JDK 1.6, when a cell tooltip is being displayed, the user cannot extend the range of selected rows using the keyboard. The solution presented above worked very nicely for that problem as well. Here is the code:
public class ToolTipTable extends JTable
{
/**
* Constructor
*/
public ToolTipTable()
{
super();
// force the TooltipManager to install the hide action
getInputMap().put(KeyStroke.getKeyStroke("ctrl A"),
"just some dummy binding in the focused InputMap");
ToolTipManager.sharedInstance().registerComponent(this);
//hide the tool tip when row selection changes
this.getSelectionModel().addListSelectionListener(
new ListSelectionListener()
{
#Override
public void valueChanged(ListSelectionEvent e)
{
hideToolTip();
}
});
}
/**
* Make the cell tool tip show the contents of the cell. (Useful if the
* cell contents are wider than the column.)
*/
#Override
public Component prepareRenderer(TableCellRenderer renderer, int row,
int column)
{
Component c = super.prepareRenderer(renderer, row, column);
if (c instanceof JComponent)
{
JComponent jc = (JComponent) c;
Object valueObj = getValueAt(row, column);
if (valueObj != null)
{
jc.setToolTipText(getValueAt(row, column).toString());
}
}
return c;
}
/**
*
*/
private void hideToolTip()
{
Action action = getActionMap().get("hideTip");
if (action != null)
{
action.actionPerformed(new ActionEvent(this,
ActionEvent.ACTION_PERFORMED, "myName"));
}
}
}
here is my problem:
I have a jList and a popup menu. When I right click the jList, the popup menu shows. The problem is that the jList item which the mouse is pointing at won't select.
And I want it to do that. When I point my cursor at an item in the list and press the right button, I want two things to happen. Select the item on which I clicked and show the popup menu.
I tried this:
jLists.addMouseListener(new MouseAdapter() {
#Override
public void mousePressed(MouseEvent e) {
jList.setSelectedIndex(jList.locationToIndex(e.getPoint()));
}
});
jList.setComponentPopupMenu(jPopupMenu);
But it only shows the popup menu.
If I delete this line:
jList.setComponentPopupMenu(jPopupMenu);
then the right-click select works (but the popup menu doesn't show).
So, what do you think is the best way to make these two functions (both) work ?
Thanks and sorry for my english.
Don't do setComponentPopupMenu. In the MouseAdapter do the following:
public void mousePressed(MouseEvent e) {check(e);}
public void mouseReleased(MouseEvent e) {check(e);}
public void check(MouseEvent e) {
if (e.isPopupTrigger()) { //if the event shows the menu
jList.setSelectedIndex(jList.locationToIndex(e.getPoint())); //select the item
jPopupMenu.show(jList, e.getX(), e.getY()); //and show the menu
}
}
This should work.
EDIT: The code now checks both press and release events, because some platforms show popups when mouse presses and some other on release. See the Swing tutorial for more info.
If you want to continue to use setComponentPopupMenu (which is nice because it handles mouse and keyboard invocations of the popup in a cross platform way), you could override JPopupMenu.show(Component, int, int) to select the appropriate row.
JPopupMenu jPopupMenu = new JPopupMenu() {
#Override
public void show(Component invoker, int x, int y) {
int row = jList.locationToIndex(new Point(x, y));
if (row != -1) {
jList.setSelectedIndex(row);
}
super.show(invoker, x, y);
}
};
jList.setComponentPopupMenu(jPopupMenu);
Note that when your popup is invoked via the keyboard (and you don't also override getPopupLocation on your target component), the x, y location you get in JPopupMenu.show will be the midpoint of your component. If there's already a selection in this case you probably don't want to change the selection.
The solution I came up with to solve the keyboard vs. mouse invocation problem was to set a client property on the component in an override of getPopupLocation and then check it when showing the popup. The argument to getPopupLocation will be null when invoked via the keyboard. Here's the core code (perhaps implemented in a utility class available to your component and its popup menu).
private static final String POPUP_TRIGGERED_BY_MOUSE_EVENT = "popupTriggeredByMouseEvent"; // NOI18N
public static Point getPopupLocation(JComponent invoker, MouseEvent event)
{
boolean popupTriggeredByMouseEvent = event != null;
invoker.putClientProperty(POPUP_TRIGGERED_BY_MOUSE_EVENT, Boolean.valueOf(popupTriggeredByMouseEvent));
if (popupTriggeredByMouseEvent)
{
return event.getPoint();
}
return invoker.getMousePosition();
}
public static boolean isPopupTriggeredByMouseEvent(JComponent invoker)
{
return Boolean.TRUE.equals(invoker.getClientProperty(POPUP_TRIGGERED_BY_MOUSE_EVENT));
}
Then override getPopupLocation in your component:
#Override
public Point getPopupLocation(MouseEvent event)
{
return PopupMenuUtils.getPopupLocation(this, event);
}
and call isPopupTriggeredByMouseEvent in an override of JPopupMenu.show to determine whether or not to select the row at the popup location (or whatever action may make sense for the underlying component):
JPopupMenu jPopupMenu = new JPopupMenu() {
#Override
public void show(Component invoker, int x, int y) {
int row = jList.locationToIndex(new Point(x, y));
if (row != -1 && PopupMenuUtils.isPopupTriggeredByMouseEvent((JComponent) invoker)) {
jList.setSelectedIndex(row);
}
super.show(invoker, x, y);
}
};