Java Swing - Add leniency when selecting items in submenus - java

When attempting to click on an item in a submenu, it is natural to quickly draw your mouse across the menu items below it. Both Windows and Mac natively handle this by putting a small delay before the a menu is opened. Swing JMenus do not handle this, and the menu the mouse briefly hovers over would be opened before the mouse reaches the intended menu item.
For example, in the image below, if I tried to select Item 3, but in the process my mouse briefly slid across Menu 2, the Menu 1 submenu would disappear before I got to it.
Does anyone have any tips or suggestions for getting around this? My idea was to define a custom MenuUI that added a timer to its mouse handler.
Here is some simple example code that illustrates my problem:
public class Thing extends JFrame {
public Thing()
{
super();
this.setSize(new Dimension(500, 500));
final JPopupMenu pMenu = new JPopupMenu();
for (int i = 0; i < 5; i++)
{
JMenu menu = new JMenu("Menu " + i);
pMenu.add(menu);
for (int j = 0; j < 10; j++)
{
menu.add(new JMenuItem("Item " + j));
}
}
this.addMouseListener(new MouseAdapter() {
#Override
public void mouseReleased(MouseEvent e) {
pMenu.show(Thing.this, e.getX(), e.getY());
}
});
}
public static void main(String[] args)
{
Thing t = new Thing();
t.setVisible(true);
}
}

Call setDelay(delay) on your menu variable, where the delay parameter is the amount of milliseconds to wait for the menu to show, as an int.
This following line of code will set the delay to 1 second, so the user has to mouseover the menu item "Menu n" 1 second, before the submenu is displayed: menu.setDelay(1000);
Here's a snippet of the edited code:
for (int i = 0; i < 5; i++)
{
JMenu menu = new JMenu("Menu " + i);
pMenu.add(menu);
for (int j = 0; j < 10; j++)
{
menu.add(new JMenuItem("Item " + j));
}
menu.setDelay(1000);
}

I came up with a very hacky solution.
I made a UI class that extends BasicMenuUI. I override the createMouseInputListener method to return a custom MouseInputListener instead of the private handler object inside BasicMenuUI.
I then got the code for the MouseInputListener implementation in handler from GrepCode[1], and copied it into my custom listener. I made one change, putting a timer in mouseEntered. My final code for mouseEntered looks like this:
public void mouseEntered(MouseEvent e) {
timer.schedule(new TimerTask() {
#Override
public void run() {
if (menuItem.isShowing())
{
Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
Point menuLoc = menuItem.getLocationOnScreen();
if (mouseLoc.x >= menuLoc.x && mouseLoc.x <= menuLoc.x + menuItem.getWidth() &&
mouseLoc.y >= menuLoc.y && mouseLoc.y <= menuLoc.y + menuItem.getHeight())
{
originalMouseEnteredStuff();
}
}
}
}, 100);
}
Before calling the the original code that was in mouseEntered, I check to make sure the mouse is still within this menu's area. I don't want all the menus my mouse brushes over to pop up after 100 ms.
Please let me know if anyone has discovered a better solution for this.
[1] http://www.grepcode.com/file_/repository.grepcode.com/java/root/jdk/openjdk/7-b147/javax/swing/plaf/basic/BasicMenuUI.java/?v=source

Thank you very much, you saved my day! The solution works as expected but I recommend using the Swing timer to ensure the code is executed by the EDT.
Additionally you should temporary set the menus delay to zero before calling the original stuff. Otherwise the user has to wait twice the delay time.
#Override
public void mouseEntered(MouseEvent e) {
if (menu.isTopLevelMenu() || menu.getDelay() == 0) {
originalMouseEnteredStuff(e);
} else {
final javax.swing.Timer timer = new javax.swing.Timer(menu.getDelay(), new DelayedMouseEnteredAction(e));
timer.setRepeats(false);
timer.start();
}
}
class DelayedMouseEnteredAction implements ActionListener
{
private final MouseEvent mouseEnteredEvent;
private DelayedMouseEnteredAction(MouseEvent mouseEnteredEvent) {
this.mouseEnteredEvent = mouseEnteredEvent;
}
#Override
public void actionPerformed(ActionEvent actionEvent) {
if (menu.isShowing()) {
final Point mouseLocationOnScreen = MouseInfo.getPointerInfo().getLocation();
final Rectangle menuBoundsOnScreen = new Rectangle(menu.getLocationOnScreen(), menu.getSize());
if (menuBoundsOnScreen.contains(mouseLocationOnScreen)) {
/*
* forward the mouse event only if the mouse cursor is yet
* located in the menus area.
*/
int menuDelay = menu.getDelay();
try {
/*
* Temporary remove the delay. Otherwise the delegate would wait the
* delay a second time e.g. before highlighting the menu item.
*/
menu.setDelay(0);
originalMouseEnteredStuff(mouseEnteredEvent);
} finally {
// reset the delay
menu.setDelay(menuDelay);
}
}
}
}
}

Related

How to make a method in a jbutton?

So, i made a button like this
parameter nomor in LoadPlanet and LoadRR is parameter to show some data to textbox.
my code is like this
the button appear correctly, but if i clicked the buttons, all of them show data from the last data which supposed to be data in last button.
-> a result from tblplanet.JmlPlanet() is 8, so the parameter was like LoadPlanet(8), so that every button show 8th data.
my question is how to make the parameter in sequence, so the button can show data correctly? Any ideas?
public void createButton() {
for (i = 0; i < tblplanet.JmlPlanet(); i++) {
tblplanet.draw(i + 1);
planet_name = tblplanet.getNama_planet();
JButton PlanetJButton = new JButton();
PlanetJButton.setBounds(10, 5 + (i * 35), 95, 26);
PlanetJButton.setText(planet_name);
PanelButton.add(PlanetJButton);
PlanetJButton.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent event) {
for (int i = 0; i < tblplanet.JmlPlanet(); i++) {
nomor = i;
LoadPlanet(nomor);
LoadRR(nomor);
}
}
});
}
}
Create a class which implements ActionListener and takes a parameter (int) of the planet that it represents
public class PlanetActionListener implements ActionListener {
private final int planet;
public PlanetActionListener(int planet) {
this.planet = planet;
}
#Override
public void actionPerformed(ActionEvent e) {
LoadPlanet(planet);
LoadRR(planet);
}
}
Then simply add the ActionListener to your button
PlanetJButton.addActionListener(new PlanetActionListener(i));
Depending on how you code is structured, you may need to make the PlanetActionListener an inner class so it can access the appropriate methods. See Nested Classes for more details

Timer isn't working correctly in my code

So the plan of the program I am making is to have an object move from one point to another. The catch is the path is drawn. I am able to draw and place a shape to move through the path from start to finish. The problem I am having is that the object is appearing at the end of the path. Even with timer set at the end of the for loop. All it does is simply wait for the timer to finish then the shape is at the end of the path.
I've gone through the code, even printed out each point that has been stored and I am getting points, not just the last point. The object's path is based on a for loop going through each point and placing the object at said point. It's crude atm and uses absolute position (just for the object).
What am I missing?
Here's the code:
JButton add = new JButton("add");
add(add);
//new Timer(50, updateTask).start();
updateTask = new ActionListener() {
#Override
public void actionPerformed(ActionEvent evt) {
repaint(); // Refresh the JFrame, callback paintComponent()
}
};
add.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
for (int i = 0; i < path.size(); i++) {
update(i); // update the (x, y) position
new Timer(1000, updateTask).start();
}
}
});
}
public void update(int i) {
if (released == 1) {
Point p = path.get(i);
System.out.println("position at: " + (int) p.getX() + " " + (int) p.getY());
xPos = (int) p.getX();
yPos = (int) p.getY();
System.out.println(i);
}
}
You can't write your loop within an ActionListener, since that would block the EventDispatchThread (EDT) and therefore block your UI.
Instead you can use the updateTask ActionListener to step through your path each time it is called from the Timer:
final Timer timer = new Timer(1000, null);
updateTask = new ActionListener() {
#Override
public void actionPerformed(ActionEvent evt) {
if (pathIndex < path.size()) {
update(pathIndex++);
} else {
timer.stop();
}
repaint(); // Refresh the JFrame, callback paintComponent()
}
};
timer.addActionListener(updateTask);
add.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
pathIndex = 0;
timer.start();
}
});
For this to work your class needs an additional int field pathIndex

Getting JPanel[][] coordinates by clicking on a JPanel

I've created a JPanel[][] Array.
private JPanel[][] pnlFeld;
And filled it with panels
for (int i = 0; i < world.getSize(); i++) {
for (int j = 0; j < world.getSize(); j++) {
pnlFeld[i][j] = new JPanel();
pnlFeld[i][j].setBorder(new EtchedBorder());
pnlFeld[i][j].addMouseListener(ml);
pnlFeld[i][j].setBackground(off);
add(pnlFeld[i][j]);
}
}
Now I want to get the array coordinates ([][]) by clicking on them and I have no clue how to do that.
I've only added methods to change the color of the panel I clicked on, nothing related to my problem.
MouseListener ml = new MouseListener() {
#Override
public void mouseEntered(MouseEvent e) {
if (world.getMode().equals("Malen")) {
if (e.getSource() instanceof JPanel)
e.getComponent().setBackground(on);
// check();
}
else if (world.getMode().equals("Radieren")) {
if (e.getSource() instanceof JPanel)
e.getComponent().setBackground(off);
// check();
}
}
#Override
public void mousePressed(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
if (world.getMode().equals("Setzen")) {
if (e.getSource() instanceof JPanel) {
if (e.getComponent().getBackground() == off) {
e.getComponent().setBackground(on);
} else
e.getComponent().setBackground(off);
}
// check();
}
}
}
#Override
public void mouseClicked(MouseEvent e) {}
#Override
public void mouseExited(MouseEvent e) {}
#Override
public void mouseReleased(MouseEvent e) {}
};
Actually you can use getBounds() to get component location and size. If you mean the array indexes there could be multiple solutions.
Define a Map and place all your panels in the Map with String value e.g. i+":"+j (or define simple pojo class with 2 fields i and j.
Create unique listener for each JPanel to keep the i and j.
Place the panels in a containr with GridBagLayout then you can use gridBagLayoutInstance.getConstraints(theClickedPanel) and check row column of the constraint
Getting JPanel[][] coordinates by clicking on a JPanel
Use a JButton[][] with ActionListener for easier coding and a better user experience. The ActionEvent has a getSource() method that will identify the button that was activated.
This chess GUI uses buttons for the 64 places on the chessboard.
If you create your MouseListener in your loop, you can use i en j within the code of your listener. The drawback is that you'll have a bit more instances of your listener.
I could suggest that you set the name of each JPanel as i and j in the loop where you declare and initialise the panels. As illustrated
pnlFeld[i][j].setName(i + "" + j);
And in your mouseClicked event check if the event source is an instance of JPanel and parse the name to get the x and y coordinates like this:
Integer.parseInt(p.getName().subString(0,1))
Integer.parseInt(p.getName().subString(1));

JLabel.setText() is only setting text for last element in a loop

First of all, apologies for how long winded this is.
I'm trying to make a simple roulette game that allows a user to add players, place bets for these players, and spin the roulette wheel, which is represented as a simple JLabel that updates it's text with each number it passes.
However, I've run into a bug that I'm having a lot of trouble with: the JLabel only updates the text for the last element in my loop.
Basically, my solution works like this:
When a user presses a button labelled "Spin" (given that users have been added to the game), I call a method from a class called SpinWheelService, which is an Observable singleton which in turn calls the notifyObservers() method:
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
String description = null;
if (ADD_PLAYER.equals(cmd)) {
addDialog();
} else if (PLACE_BET.equals(cmd)) {
betDialog();
} else if (SPIN.equals(cmd)) {
SpinWheelService.sws.setSpinWheelService();
} else if (DISPLAY.equals(cmd)) {
System.out.println("Display selected!");
}
}
Here is my SpinWheelService class:
package model;
import java.util.*;
public class SpinWheelService extends Observable {
public static SpinWheelService sws = new SpinWheelService();
public void setSpinWheelService() {
setChanged();
notifyObservers();
}
}
The only listener registered for SpinWheelService is this class, where GameEngine is my game engine that handles internal game logic, WheelCallbackImpl is a class that updates the View:
class SpinWheelObserver implements Observer {
GameEngine gameEngine;
ArrayList<SimplePlayer> players;
WheelCallbackImpl wheelCall;
int n;
public SpinWheelObserver(GameEngine engine, WheelCallbackImpl wheel, ArrayList<SimplePlayer> playerList) {
players = playerList;
gameEngine = engine;
wheelCall = wheel;
}
public void update(Observable sender, Object arg) {
// check if any players are present
if (players.size() == 0) {
System.out.println("Empty player array!");
return;
}
do {
gameEngine.spin(40, 1, 300, 30, wheelCall);
n = wheelCall.playback();
} while (n== 0);
}
}
The main point of note here is my gameEngine.spin() method, which is this:
public class GameEngineImpl implements GameEngine {
private List<Player> playerList = new ArrayList<Player>();
// method handles the slowing down of the roulette wheel, printing numbers at an incremental delay
public void delay(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
System.out.println("Sleep method failed.");
}
}
public void spin(int wheelSize, int initialDelay, int finalDelay,
int delayIncrement, WheelCallback callback) {
Random rand = new Random();
int curNo = rand.nextInt(wheelSize) + 1;
int finalNo = 0;
assert (curNo >= 1);
// while loop handles how long the wheel will spin for
while (initialDelay <= finalDelay) {
delay(initialDelay);
initialDelay += delayIncrement;
// handles rotating nature of the wheel, ensures that if it reaches wheel size, reverts to 1
if (curNo > wheelSize) {
curNo = 1;
callback.nextNumber(curNo, this);
curNo++;
}
assert (curNo <= wheelSize);
callback.nextNumber(curNo, this);
curNo++;
finalNo = curNo - 1;
}
calculateResult(finalNo);
callback.result(finalNo, this);
}
The method callback.nextNumber(curNo, this):
public void nextNumber(int nextNumber, GameEngine engine) {
String strNo = Integer.toString(nextNumber);
assert (nextNumber >= 1);
System.out.println(nextNumber);
wcWheel.setCounter(strNo);
}
Where in, wcWheel is my singleton instance of my View, which contains the method setCounter():
public void setCounter(String value) {
label.setText(value);
}
Sorry for how convoluted my explanation is, but basically what it boils down to is that setCounter() is definitely being called, but seems to only call the setText() method on the final number. So what I'm left with is an empty label that doesn't present the number until the entire roulette has finished spinning.
I've determined that setCounter() runs on the event dispatch thread, and I suspect this is a concurrency issue but I have no idea how to correct it.
I've tried to include all relevant code, but if I'm missing anything, please mention it and I'll post it up as well.
I'm at my wits end here, so if anyone would be kind of enough to help, that would be so great.
Thank you!
Your while loop along Thread.sleep() will block and repainting or changing of the UI until the loop is finished.
Instead you'll want to implement a javax.swing.Timer for the delay, and keep a counter for the number of ticks, to stop it. You can see more at How to Use Swing Timers
The basic construct is
Timer ( int delayInMillis, ActionListener listener )
where delayInMillis is the millisecond delay between firing of an ActionEvent. This event is listened for by the listener. So every time the event is fired, the actionPerfomed of the listener is called. So you might do something like this:
Timer timer = new Timer(delay, new ActionListener()(
#Override
public void actionPerformed(ActionEvent e) {
if (count == 0) {
((Timer)e.getSource()).stop();
} else {
//make a change to your label
count--;
}
}
));
You can call timer.start() to start the timer. Every delay milliseconds, the label will change to what you need it to, until some arbitrary count reaches 0, then timer stops. You can then set the count variable to whatever you need to, if you want to to be random, say depending on how hard the wheel is spun :D
I think you didn't post all the relevant code that is required to know exactly the problem.
But most likely the problem is due to you run your loop and JLabel.setText() in the EDT (Event Dispatching Thread).
Note that updating the UI components (e.g. the text of a JLabel) also happens in the EDT, so while your loop runs in the EDT, the text will not be updated, only after your loop ended and you return from your event listener. Then since you modified the text of the JLabel it will be refreshed / repainted and you will see the last value you set to it.
Example to demonstrate this. In the following example a loop in the event listener loops from 0 to 9 and sets the text of the label, but you will only see the final 9 be set:
JPanel p = new JPanel();
final JLabel l = new JLabel("-1");
p.add(l);
JButton b = new JButton("Loop");
p.add(b);
b.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
for ( int i = 0; i < 10; i++ ) {
l.setText( "" + i );
try { Thread.sleep( 200 ); } catch ( InterruptedException e1 ) {}
}
}
} );
A proposed solution: Use javax.swing.Timer to do the loop's work. Swing's timer calls its listeners in the EDT so it's safe to update swing components in it, and once the listener returns, a component UI update can happen immediately:
JPanel p = new JPanel();
final JLabel l = new JLabel("-1");
p.add(l);
JButton b = new JButton("Loop");
p.add(b);
b.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
new Timer(200, new ActionListener() {
int i = 0;
#Override
public void actionPerformed(ActionEvent e2) {
l.setText("" + i);
if ( ++i == 10 )
((Timer)e2.getSource()).stop();
}
}).start();
}
} );
In this solution you will see the label's text counting from 0 up to 9 nicely.
It's appears to me that your entire game must block in the action handler until the while loop has finished? So the text of the label will be getting updated but only the last update will be visible once the AWT thread is running again.

JPopupMenu on JTable -> Get the cell the menu was created on

I have a situation where I have a popup menu created when a JTable is right clicked on. Standard way of creating the popup menu:
aJTable.setComponentPopupMenu(rightClickMenu);
Now afterwards in the action that gets registered, I am unable to find out which cell was right clicked on to get that popup menu to appear.
rightClickMenuItem.addActionListener(new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
// Work out what cell was right clicked to generate the menu
}
});
Any ideas on how you do this?
Astonishing fact: with a componentPopupMenu installed, a mouseListener never sees the mouseEvent that is the popupTrigger (reason is that showing the componentPopup is handled globally by a AWTEventListener installed by BasicLookAndFeel, and that listener consumes the event).
The only place which sees the mousePosition of that trigger is the getPopupLocation(MouseEvent), so the only reliable way to get hold of it (for doing location dependent config/actions) is #Mad's suggestion to override that method and store the value somewhere for later use.
The snippet below uses a clientProperty as storage location:
final JTable table = new JTable(new AncientSwingTeam()) {
#Override
public Point getPopupLocation(MouseEvent event) {
setPopupTriggerLocation(event);
return super.getPopupLocation(event);
}
protected void setPopupTriggerLocation(MouseEvent event) {
putClientProperty("popupTriggerLocation",
event != null ? event.getPoint() : null);
}
};
JPopupMenu popup = new JPopupMenu();
Action action = new AbstractAction("show trigger location") {
#Override
public void actionPerformed(ActionEvent e) {
JPopupMenu parent = (JPopupMenu) SwingUtilities.getAncestorOfClass(
JPopupMenu.class, (Component) e.getSource());
JTable invoker = (JTable) parent.getInvoker();
Point p = (Point) invoker.getClientProperty("popupTriggerLocation");
String output = p != null ? "row/col: "
+ invoker.rowAtPoint(p) + "/" + invoker.columnAtPoint(p) : null;
System.out.println(output);
}
};
popup.add(action);
popup.add("dummy2");
table.setComponentPopupMenu(popup);
#MadProgrammer's suggestion of getPopupLocation looked promising, but I couldn't work out how to get the information across between the table and the actionEvent...
I got around this by making sure that the row was selected when you rightclicked on it -> since the popup menu prevents the selection of the row, you can add in a mouse listener that makes sure the row gets selected no matter what click (left or right) is pressed.
aTable.addMouseListener(new MouseAdapter() {
#Override
public void mousePressed(MouseEvent e) {
int r = aTable.rowAtPoint(e.getPoint());
if (r >= 0 && r < clt.getRowCount()) {
aTable.setRowSelectionInterval(r, r);
} else {
aTable.clearSelection();
}
}
});
This means that in the rightClickMenuItem's action listener, you can grab the table's selected cell / row
rightClickMenuItem.addActionListener(new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
aTable.get details about the selected one....
}
});
Too easy! Thanks everyone for the help.
JTable has methods
int row = rowAtPoint(p);
int col = columnAtPoint(p);
So pass the MouseEvent's point and use the values
Add a MouseListener and store the last right click point somewhere.

Categories

Resources