Update JPopupMenu height while visible - java

I have a popup menu in my application that I want to replace with a customized one so that it matches the look & feel of the rest of the application. Essentially, instead of having the normal menu items in the popup, I want reuse a component that already exist elsewhere in the application that lets you navigate through a hierarchy of items in a "paging" way instead of with sub-menus. So if you click on an item in the list that contains children then the next page will be displayed replacing the current items in the list with a list of the children of the clicked item.
The advantage of using the "paging" component is that it will fit in well with the rest of the application (it is already used in other places that are not popups) and it has some nice looking animation effects when navigating between pages.
The problem I'm having is that the preferred height of the paging component changes when showing different pages (the amount of items in the list change) and I want the popup menu height to update so that it fits the paging component exactly, but so far my attempts to update the height of the popup menu while it is visible have failed.
Below is an example application that demonstrates the problem:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
public class PopupSizeTestFrame extends JFrame {
private static final int PREFFERED_WIDTH = 300;
private static JPanel resizingPanel;
private static JPopupMenu popupMenu;
private static PopupSizeTestFrame frame;
private static void resizePopupMenu() {
// What should I do here so that the size of the popup menu is updated?
/**
* Calling pack() works in this example, but I cannot call pack in the
* actual application since there is a smooth transition animation when
* clicking on the inner panel and pack() essentially re-adds the
* components of the popup menu (removeNotify and addNotify is called),
* which interferes with the animation and causes the menu to flicker.
* The flickering when clicking the inner panel can also be seen in this
* example when uncommenting the pack() call.
*/
//popupMenu.pack();
//
// I tried combinations of the following, without success:
//popupMenu.invalidate();
//popupMenu.revalidate();
//popupMenu.validate();
//popupMenu.doLayout();
// Repaint
popupMenu.repaint();
}
public static void main(String args[]) {
// Create popup child panel with height that changes when clicked
resizingPanel = new JPanel(new BorderLayout());
int initialHeight = 30;
final JLabel label = new JLabel("Click me (" + initialHeight + "px)");
resizingPanel.add(label);
resizingPanel.setBackground(Color.GREEN);
resizingPanel.setPreferredSize(new Dimension(PREFFERED_WIDTH, initialHeight));
resizingPanel.addMouseListener(new MouseAdapter() {
private int clickCount = 0;
#Override
public void mouseClicked(MouseEvent e) {
int height = ((clickCount % 3) + 1) * 50;
resizingPanel.setPreferredSize(new Dimension(PREFFERED_WIDTH, height));
clickCount++;
label.setText("Click me (" + height + "px)");
resizePopupMenu();
}
});
// Create popup
popupMenu = new JPopupMenu();
popupMenu.setLayout(new BorderLayout());
popupMenu.add(new JLabel("Header"), BorderLayout.NORTH);
popupMenu.add(resizingPanel, BorderLayout.CENTER);
popupMenu.add(new JLabel("Footer"), BorderLayout.SOUTH);
// Create frame
frame = new PopupSizeTestFrame();
frame.setLayout(new BorderLayout());
frame.setPreferredSize(new Dimension(400, 400));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.addMouseListener(new MouseAdapter() {
#Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
popupMenu.show(frame, e.getX(), e.getY());
}
}
});
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
frame.setVisible(true);
}
});
}
}
When running the example above you will see that the popup menu size is updated if it is closed and opened after the inner panel was clicked, but it is not updated while the popup is visible.
What can I do in resizePopupMenu() to update the height of the JPopupMenu?

For the test app in the question the popup was always a heavyweight component and the following worked (as suggested by mKorbel)
private static void resizePopupMenu() {
SwingUtilities.getWindowAncestor(popupMenu).pack();
}
For the actual application when the popup was inside the bounds the application it was created as a lightweight component which prevented it from being resized with pack() and also prevented it from resizing past the application bounds.
I tried setting this property...
JPopupMenu.setDefaultLightWeightPopupEnabled(false);
but then a mediumweight component was created and it would still not resize past the application bounds.
So I had to first force all popups of the "owner" component to be heavyweight with the following piece of unfortunate code
// Set owner to component on which popup is shown or any of its parents
final Component owner = ...;
AccessController.doPrivileged(new PrivilegedAction<Void>() {
#Override
public Void run() {
try {
Field field;
if (System.getProperty("java.version").startsWith("1.6.0")) {
Class clazz = Class.forName("javax.swing.PopupFactory");
field = clazz.getDeclaredField("forceHeavyWeightPopupKey");
} else { //JDK 1.7.0, 1.8.0
Class clazz = Class.forName("javax.swing.ClientPropertyKey");
field = clazz.getDeclaredField("PopupFactory_FORCE_HEAVYWEIGHT_POPUP");
}
field.setAccessible(true);
owner.putClientProperty(field.get(null), Boolean.TRUE);
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
return null;
}
});
After forcing the popup menu to always be a heavyweight component this
SwingUtilities.getWindowAncestor(popupMenu).pack();
also worked to update the size of the popup, even beyond the bounds of the application.

I think you can add a PopupMenuListener to the popup menu and handle the menuWillBecomeVisible(...) event to set the size of the popup.

Related

Java Swing JScrollPane not scrolling when shift held down and using mouse wheel

I have a simple Java Swing program defined below:
import javax.swing.*;
import java.awt.*;
public class Test implements Runnable {
public static void main(String[] args) {
Test main = new Test();
SwingUtilities.invokeLater(main);
}
#Override
public void run() {
// Application window.
JFrame mainFrame = new JFrame();
// Set up window.
mainFrame.setPreferredSize(new Dimension(600,700));
mainFrame.setFocusable(true);
mainFrame.requestFocus();
JScrollPane scrollPane = new JScrollPane();
scrollPane.setPreferredSize(new Dimension(600,700));
JPanel scrollPanel = new JPanel();
scrollPanel.setLayout(new GridLayout(0,1));
// Add buttons.
for (int i = 0; i < 40; i ++) {
JButton button = new JButton("Button " + i);
button.setPreferredSize(new Dimension(600,100));
scrollPanel.add(button);
}
scrollPane.setViewportView(scrollPanel);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
JPanel mainPanel = new JPanel();
mainPanel.add(scrollPane);
// Fill up window.
mainFrame.getContentPane().setLayout(new BorderLayout());
mainFrame.getContentPane().add(mainPanel, BorderLayout.CENTER);
SwingUtilities.updateComponentTreeUI(mainFrame);
mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainFrame.pack();
mainFrame.setVisible(true);
}
}
The program consists of a simple JScrollPane with multiple buttons inside of it. Only vertical scrolling is enabled. It works fine.
However, the problem is, when I am holding down the 'shift' key, vertical scrolling does not work when I am using the mouse wheel to scroll. Vertical scrolling only works when I drag the scrollbar or let go of the 'shift' key.
Usually, in a JScrollPane, when the 'shift' key is held down, and the mouse wheel is used, it scrolls horizontally instead of vertically. However, I have disabled horizontal scrolling.
How would I go about enabling vertical scrolling using the mouse wheel if the user is holding down 'shift'?
I don't think there is a simple way to do this because normally you would only need the shift scroll when scrolling in more than one direction.
You could try adding a key listener to your JFrame and setting the wheel on your mouse to scroll vertically whenever the shift key is pressed. I tried it with this code and it worked just fine:
frame.addKeyListener(new KeyListener() {
#Override
public void keyPressed(KeyEvent arg0) {
// TODO Auto-generated method stub
if(arg0.isShiftDown()) {
frame.addMouseWheelListener(new MouseWheelListener() {
#Override
public void mouseWheelMoved(MouseWheelEvent arg0) {
// TODO Auto-generated method stub
pane.getVerticalScrollBar().setValue(pane.getVerticalScrollBar().getValue()+arg0.getWheelRotation());
}
});
}
if(!arg0.isShiftDown()) {
frame.removeMouseWheelListener(frame.getMouseWheelListeners()[0]);
}
}
}
I have figured out a cleaner and simpler answer to my question. Here is the code:
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class CustomScrollPane extends JScrollPane {
public CustomScrollPane(Component component) {
super(component);
initialize();
}
private void initialize() {
// Set up the scroll bar.
setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
// Disable scrolling so we can do it manually.
setWheelScrollingEnabled(false);
addMouseWheelListener(new MouseWheelListener() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
// Manually scroll.
getVerticalScrollBar().setValue(getVerticalScrollBar().getValue() + (e.getWheelRotation()));
}
});
}
}
The solution involves disabling JScrollPane from scrolling. It detects when the mousewheel is being rotated, and manually scrolls. This way, it will scroll when the mousewheel is rotated; it does not matter if 'shift' is being held down or not. The only drawback that I can see is that getVerticalScrollBar().setUnitIncrement() will not change the scrolling speed. The scrolling speed has to be changed manually, perhaps with a constant multiplier.

How to create a "Game Option/Pause Screen" in Swing?

Background: Making a game in Swing. It is simple turn base game. Not a whole lot going on. Because of this I didn't think I would need to implement a Game Tick. Rather, my thought was when a component got changed or needed to be updated just simply revalidate/repaint that component on the fly rather than repainting the whole screen.
I have a GameJPanel which currently has all the components on it. This JPanel is the one that contains the components that get revalidated/repainted etc.
I figured I could make JLayeredPane that holds GameJPanel and my OptionJPanel. Have a Button on GameJPanel that when pressed causes the OptionJPanel to show on top of it and having its JPanel 50% transparent (so it gives the affect it dims the GameJPanel).
However, once I did this what happened was that the GameJPanel started to replace OptionJPanel components (because of the events... etc; repainting of the components).
So currently I am at a loss on what to do. I'm thinking if I had some sort of game tick I wouldn't be having this issue, however, I am not 100% certain. I'm a little worried if I implemented a gametick that the events in game will cause the GameJPanel components to show through for half a second then get replaced. There are some events that cause components to repaint themselves without manually doing it (like quick example for JLabel setText();)
As an example of what I'm trying to go for.
I have tried with a CardLayout but I couldn't figure out how to have the OptionJPanel be on top of GameJPanel while seeing GameJPanel in the background (I tried setting background color, setOpaque(false)..., tried to limit Option JPanel size but I think the CardLayout stretches it (not 100% sure)) all I got was a gray background when doing so.
I would prefer not to go the CardLayout route because in the future I also plan on placing components on top of the GameJPanel (like someone clicks a button, have another panel on a different layer have a component slide in or out etc).
I use CardLayout a ton with my other components in GameJPanel to swap screens around, but haven't had the need to have the other components behind the one showing to show through.
Any ideas on how to go about this would be great or even example code that shows this.
As noted above, you would use a JDialog, a component that is easy to make (similar to making a JFrame) and easy to place. Simply place it "relative-to" the JFrame, e.g.,
myDialog.setLocationRelativeTo(myJFrame);
... and it will automatically center itself on the JFrame. The tricky part is dimming the underlying JFrame, and for this you would need to use a JGlassPane added to the JFrame's rootpane, one set with a background color that uses an alpha composite value. The tricky part with this is to draw the darker background without causing side effects, and to do this, please read Rob Camick's (StackOverflow user camickr) excellent tutorial on drawing in Swing with alpha composites which you can find here: Java Tips Weblog: Backgrounds with Transparency
An example of such a program is shown here:
import java.awt.Color;
import java.awt.Component;
import java.awt.Dialog.ModalityType;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.swing.*;
public class DialogEg {
// path to example image used as "game" background
private static final String IMG_PATH = "https://upload.wikimedia.org/"
+ "wikipedia/commons/7/76/Jump_%27n_Bump.png";
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> createAndShowGui());
}
private static void createAndShowGui() {
// get the "game" background image, or exit if fail
BufferedImage img = null;
try {
URL imgUrl = new URL(IMG_PATH);
img = ImageIO.read(imgUrl);
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
// pass "game" image into main JPanel so that it will be drawn
DeMainPanel mainPanel = new DeMainPanel(img);
JFrame frame = new JFrame("Dialog Example");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.add(mainPanel); // add main JPanel to JFrame
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
}
// main JPanel
#SuppressWarnings("serial")
class DeMainPanel extends JPanel {
private BufferedImage img; // background image
// JButton action that shows the JDialog and darkens the glasspane
private PauseAction pauseAction = new PauseAction("Pause");
public DeMainPanel(BufferedImage img) {
super();
this.img = img;
add(new JButton(pauseAction));
}
// draw the "game" background image within the JPanel if not null
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (img != null) {
g.drawImage(img, 0, 0, this);
}
}
// size this JPanel to match the image's size
#Override
public Dimension getPreferredSize() {
if (isPreferredSizeSet() || img == null) {
return super.getPreferredSize();
}
int width = img.getWidth();
int height = img.getHeight();
return new Dimension(width, height);
}
}
// Action / ActionListener for JButton -- shows JDialog and darkens glasspane
#SuppressWarnings("serial")
class PauseAction extends AbstractAction {
private static final int ALPHA = 175; // how much see-thru. 0 to 255
private static final Color GP_BG = new Color(0, 0, 0, ALPHA);
private DeDialogPanel deDialogPanel = new DeDialogPanel(); // jpanel shown in JDialog
public PauseAction(String name) {
super(name);
}
#Override
public void actionPerformed(ActionEvent e) {
// comp is our JButton
Component comp = (Component) e.getSource();
if (comp == null) {
return;
}
// create our glass pane
JPanel glassPane = new JPanel() {
#Override
protected void paintComponent(Graphics g) {
// magic to make it dark without side-effects
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
super.paintComponent(g);
}
};
// more magic below
glassPane.setOpaque(false);
glassPane.setBackground(GP_BG);
// get the rootpane container, here the JFrame, that holds the JButton
RootPaneContainer win = (RootPaneContainer) SwingUtilities.getWindowAncestor(comp);
win.setGlassPane(glassPane); // set the glass pane
glassPane.setVisible(true); // and show the glass pane
// create a *modal* JDialog
JDialog dialog = new JDialog((Window)win, "", ModalityType.APPLICATION_MODAL);
dialog.getContentPane().add(deDialogPanel); // add its JPanel to it
dialog.setUndecorated(true); // give it no borders (if desired)
dialog.pack(); // size it
dialog.setLocationRelativeTo((Window) win); // ** Center it over the JFrame **
dialog.setVisible(true); // display it, pausing the GUI below it
// at this point the dialog is no longer visible, so get rid of glass pane
glassPane.setVisible(false);
}
}
// JPanel shown in the modal JDialog above
#SuppressWarnings("serial")
class DeDialogPanel extends JPanel {
private static final Color BG = new Color(123, 63, 0);
public DeDialogPanel() {
JLabel pausedLabel = new JLabel("PAUSED");
pausedLabel.setForeground(Color.ORANGE);
JPanel pausedPanel = new JPanel();
pausedPanel.setOpaque(false);
pausedPanel.add(pausedLabel);
setBackground(BG);
int eb = 15;
setBorder(BorderFactory.createEmptyBorder(eb, eb, eb, eb));
setLayout(new GridLayout(0, 1, 10, 10));
add(pausedPanel);
add(new JButton(new FooAction("RESUME")));
add(new JButton(new FooAction("RESTART")));
add(new JButton(new FooAction("EXIT TO MAP")));
}
// simple action -- all it does is to make the dialog no longer visible
private class FooAction extends AbstractAction {
public FooAction(String name) {
super(name);
}
#Override
public void actionPerformed(ActionEvent e) {
Component comp = (Component) e.getSource();
Window win = SwingUtilities.getWindowAncestor(comp);
win.dispose(); // here -- dispose of the JDialog
}
}
}
The GUI looks like this initially:
but then when the dialog shows and the glass pane is darkened, it looks like this:
So after about a month of working on my game I was drawn to this post once again. I implemented part of my game with what DontKnowMuchButGettingBetter's way and also implemented this by just adding the components to the GlassPane so to speak (Made a JPanel, set it to be GlassPane, did whatever on that Panel)...
The later implementation (GlassPane), isn't the best way to go about this because then you can't use the glass pane for other useful things.
I came back to my original idea to use a JLayeredPane. Having different Components on different levels and working off that. My issue before was that when components were getting repainted, the components in the backer layers were over painting the ones in the front layer.
Well I just came across a method called isOptimizedDrawingEnabled()... By making this method always return false for the JLayeredPane I was able to achieve what I wanted.

Thread sleep inside of actionPerformed method

First of all I want to say I'm aware this aproach is wrong so I'm asking this question because of pure curiousity. Lets say I have a swing application like this:
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class ThreadSleeping {
JFrame frame = new JFrame();
JPanel panel = new JPanel();
JButton button = new JButton("Load");
JLabel label = new JLabel();
public ThreadSleeping() {
panel.add(button);
button.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent arg0) {
label.setIcon(new ImageIcon(
"C:/Users/Public/Pictures/Sample Pictures/Tulips.jpg"));
System.out.println("Tulips painted");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
label.setIcon(new ImageIcon(
"C:/Users/Public/Pictures/Sample Pictures/Koala.jpg"));
System.out.println("Koala painted");
}
});
frame.add(panel, BorderLayout.NORTH);
frame.add(label, BorderLayout.CENTER);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setSize(1024, 768);
// frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new ThreadSleeping();
}
});
}
}
Basically when I click a Load button I expect that Tulips.jpg image displays then GUI freezes for a 2 seconds and after that I expect that Koala.jpg image displays. But what happens is that: I click on button, GUI freezes for a 2 seconds and Koala.jpg displays. No Tulips.jpg before that. But thing that confuses me is when I put those System.out.println("Tulips painted"); and System.out.println("Koala painted");. So when I click on button it prints "Tulips painted" and after 2 seconds "Koala painted". Can someone tell me whats going on here? Regards.
works in this case, because you programatically freeze ouf Swing GUI, but there is/aren't another update(s), ot another JComponent(s)
doens't works in the case that there are a few another updated to the Swing GUI, Thread.sleep(int) freeze Event Dispatch Thread,
by default all updates to the JComponents XxxModels never will be visible on the JComponents view
example until sleep ended you'll lost all updated to the GUI
The point I intended to make in my comment:
if you sleep the edt, the resulting mis-behaviour is basically unpredictable.
Unpredictable in the sense that you can't know what will happen or not. All we can do, is guess ...
The technical reason is that most ui updates don't happen immediately but are scheduled: we can't really know what's waiting in the line behind us. Remember: it's only one lane, and when in the actionPerformed, it's we that are sitting in it.
For educational reasons, the code below is the original code with a couple of lines to un/comment to demonstrate different scenarios.
[0] resetting the icon before/after sleeping: as you already noticed, the first doesn't show even though the property is taken. Technical reason: visual update happens via label.repaint() which is scheduled on the EDT for latter processing (as its api doc states)
[1] skimming the api doc, we notice another method: paintImmediately(...) which is documented to do exactly what it's name says and allowed - as we are on the EDT - is allowed to be called. Looks like success, the yellow icon shows up.
[2] but wait: being in the center of a Borderline, the label fills that area anyway, independent of whether or not it has an icon. Let's try to put it into a region that requires a re-layout, as f.i. into the south. Back to square [0], yellow icon not showing.
[3] looking into the source of setIcon(..) reveals that layout is ... scheduled again. We learned in square [1] that we can force thingies to happen immediately, in case of layout that would be the pair invalidate() / validate(). Bingo, yellow icon even when in south.
[4] nasty subclass which schedules the icon property setting (note: while contrived here there is nothing in its contract that hinders subclasses to do it!). As the property isn't even set, yellow isn't showing, back to square [0]
At the end of the day (but before going to sleep the EDT :-), there is simply no way to reliably predict the visual outcome during the sleep. And visuals are only the tip of the ice...
/**
* Unpredictable behaviour when sleeping the EDT.
* http://stackoverflow.com/q/15600203/203657
*
* [0] run as-is: property set but yellow not showing
* [1] uncomment paintImmediately: yellow showing in center
* [2] add label to south: yellow not showing
* [3] force immediate in-/validation: yellow showing in south
* [4] subclass label with invoked property setting:
* property not set, yellow not showing
*
*/
public class ThreadSleeping {
JFrame frame = new JFrame();
JPanel panel = new JPanel();
JButton button = new JButton("Load");
JLabel label = new JLabel() {
// [4] subclass implemented to schedule the property setting
// #Override
// public void setIcon(final Icon icon) {
// SwingUtilities.invokeLater(new Runnable() {
// public void run() {
// superSetIcon(icon);
//
// }
// });
// }
//
// protected void superSetIcon(Icon icon) {
// super.setIcon(icon);
// }
//
};
public ThreadSleeping() {
panel.add(button);
button.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent arg0) {
Icon firstIcon = new FixedIcon(Color.YELLOW, 100, 100);
Icon secondIcon = new FixedIcon(Color.RED, 500, 500);
label.setIcon(firstIcon);
// check if the property is already set
System.out.println(label.getIcon());
// following lines try to force the output before going to sleep
// [3] paintImmediately + force immediate re-layout
// label.invalidate();
// label.getParent().validate();
// {1] paintImmediately (fine for center, no effect for south)
// ((JComponent) label.getParent()).paintImmediately(0, 0, 5000, 5000);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
label.setIcon(secondIcon);
}
});
frame.add(panel, BorderLayout.NORTH);
// un/comment one of the following, placing the
// label either in CENTER (= sized to fill)
frame.add(label, BorderLayout.CENTER);
// [2] or in SOUTH (= sized to pref)
// frame.add(label, BorderLayout.SOUTH);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setSize(1024, 768);
frame.setVisible(true);
}
/**
* Simple fixed size Icon filling its area.
*/
public static class FixedIcon implements Icon {
private Color background;
private int width;
private int height;
public FixedIcon(Color background, int width, int height) {
this.background = background;
this.width = width;
this.height = height;
}
#Override
public void paintIcon(Component c, Graphics g, int x, int y) {
g.setColor(background);
g.fillRect(0, 0, width, height);
}
#Override
public int getIconWidth() {
return width;
}
#Override
public int getIconHeight() {
return height;
}
#Override
public String toString() {
return "[" + background + " w/h " + width + "/" + height + "]";
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new ThreadSleeping();
}
});
}
}

How to move 3 buttons inside a window

In the below code I am attempting to move the three buttons to the left when you click the left button. When I click it; nothing happens currently. Can anyone explain to me what I am doing wrong here? Also, for some reason it has stopped compiling correctly and I am unsure why but I BELIEVE it is because of a mistake in my code while attempting to get the buttons to move to the left when you click the button. I do NOT want the window to move. Just the buttons within the window. Does any one see what I am doing wrong and can you explain it?
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Buttons extends JFrame {
//Control Definitions
JButton resetButton;
JButton leftButton;
JButton colorButton;
JPanel buttonPanel;
// Layout Definiton
eventHandle evt;
FlowLayout flt;
Point point; //to Hold Previous Window Position
Color color; //to Hold Previous Color
public Buttons() {
super("Buttons Window");
flt = new FlowLayout();//inialize the Flow Layout
buttonPanel = new JPanel(flt);
//inialize the buttonPanel With Flow Layout
//initialize buttons
resetButton = new JButton("Reset");
leftButton = new JButton("Left");
colorButton = new JButton("Blue");
evt = new eventHandle(); //initiate the eventhandle class
buttonPanel.add(leftButton); //add leftButton
buttonPanel.add(colorButton);//add colorButton
buttonPanel.add(resetButton);//add colorButton
getContentPane().add(buttonPanel);//buttonPanel
//add actionlistners
leftButton.addActionListener(evt);
colorButton.addActionListener(evt);
resetButton.addActionListener(evt);
setBounds(20, 120, 250, 70);
//following Initate the point with Center of Scren
point = new Point((Toolkit.getDefaultToolkit().
getScreenSize().width - getWidth()) / 2,
(Toolkit.getDefaultToolkit().getScreenSize().height
- getHeight()) / 2);
setLocation(point); //locates the window in center
color = buttonPanel.getBackground();//stores the initial color
setDefaultCloseOperation(EXIT_ON_CLOSE);
setVisible(true);
}
class eventHandle implements ActionListener { //Event Handler
public void actionPerformed(ActionEvent e) {
{
if (e.getSource() == leftButton) ///if its from leftButton
{
leftButton.setAlignmentX(Component.LEFT_ALIGNMENT);
colorButton.setAlignmentX(Component.LEFT_ALIGNMENT);
resetButton.setAlignmentX(Component.LEFT_ALIGNMENT);
//setLocation( (point.x -150), point.y);//shift the window 150 pixels left
} else if (e.getSource() == colorButton) {
buttonPanel.setBackground(color.BLUE);
//sets the backgorund to Blue
} else {
leftButton.setAlignmentX(Component.CENTER_ALIGNMENT);
//sets the location to previous location
colorButton.setAlignmentX(Component.CENTER_ALIGNMENT);
resetButton.setAlignmentX(Component.CENTER_ALIGNMENT);
}
}
}
}
public static void main(String[] args) {
Buttons buttonwindow = new Buttons();
}
}
It has stopped compiling, because you deleted one accolade, so put one accolade "}" just above the method:
public static void main(String[] args)
and the code should compile. pls feedback.
EDIT:
Also rewrite your main method like this:
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
Buttons buttonwindow = new Buttons();
}
}
);
}
Every usage of Swing components must be done thorugh the Event Dispatch Thread (abbreviated EDT) or you will probably get unwanted visual effects. See here for explanation.
EDIT^2:
To achieve the desired behavior, rewrite the the action listener like this:
if (e.getSource() == leftButton) {
((FlowLayout)buttonPanel.getLayout()).setAlignment(FlowLayout.LEFT); //1
buttonPanel.revalidate(); //2
}
else if (e.getSource() == colorButton) {
buttonPanel.setBackground(color.BLUE);
}
else {
((FlowLayout)buttonPanel.getLayout()).setAlignment(FlowLayout.CENTER);
buttonPanel.revalidate();
}
Any change to the visual appereance to the Swing component must be done through the assigned layout manager, in this case FlowLayout - in line 1.
To see the change you must notify the Swing components layout manager to rearrange the components - in line 2 the revalidate() method "notifies" the layout manager to recalculate the new positions and eventually "notifies" the EDT to draw it on the screen.
You should update the layout manager to align the components to the left or right. Try something like;
((FlowLayout)getLayout()).setAlignment(FlowLayout.LEFT);
Instead
You code won't compile as the static main method appears inside the inner class eventHandle. You can fix simply by moving it into the class body of the outer class Buttons.
As you have all the objects references at class level, you could do the button alignment using, for instance:
flt.setAlignment(FlowLayout.RIGHT);
buttonPanel.revalidate();
...
Here you are adjusting the layout alignment of your FlowLayout and revalidating to visually reflect the updated changes on your panel.

How to effectively add a call back method for the JPanel.add(component) method?

edit: now solved, but can't mark as accepted for two days
In my class I have a JScrollPanel and that has a JPanel inside of it too.
My code resembles something like this:
import java.awt.Color;
import java.awt.Container;
import javax.swing.*;
public class MyClass {
private JPanel p;
private JScrollPane s;
private Container contentPane;
public MyClass(Container contentPane) {
this.contentPane = contentPane;
this.p = new JPanel();
this.p.setBackground(Color.WHITE);
BoxLayout boxLayout = new BoxLayout(this.p, BoxLayout.Y_AXIS);
this.p.setLayout(boxLayout);
this.s = new JScrollPane(this.p);
this.s.setSize(400, 364);
this.contentPane.add(this.s);
}
public final JLabel makeJLabel(String message) {
JLabel jLabel = new JLabel("<html><p style=\"padding-left:15px;padding-right:15px;width:280px;\">" + message.replaceAll("(\r\n|\n)", "<br />") + "</p></html>");
/*
some stuff here to calculate pref/max size and add an imageicon
*/
p.add(jLabel);
this.p.revalidate();
this.s.revalidate(); //just added because the above line made no effect
scrollToBottom();
return jLabel;
}
public void scrollToBottom() {
JScrollBar vertical = s.getVerticalScrollBar();
vertical.setValue(vertical.getMaximum());
}
}
Elsewhere in my class I have a method which adds a JLabel to the JPanel. This actual method is quite long, so I wont post it all, but this is the code which adds it to the panel: p.add(jLabel1);
All of these JLabels are added in a vertical fashion thanks to the Box Layout.
After the JLabel has been added to the JPanel I want the JScrollPane to scroll to the bottom. But this can't be done until after the JPanel has actually been drawn (painted?) onto JPanel. Otherwise I get this result:
So what I want to do is add some form of listener to the JPanel which detects when my JLabel has been painted to it, so that I can tell my JScrollPane to scroll to the bottom. I have already written a method which scrolls the pane to the bottom, but I don't have anywhere suitable to call it from yet.
Does anyone have any ideas on this please? Thanks.
I'm assuming you just want the label to be visible in the scrollpane so I would gues you should be able to do something like:
panel.add( label );
panel.revalidate();
label.scrollRectToVisible( label.getBounds() );
Or if you really do want to just scroll bo the bottom then you would do something like:
panel.revalidate();
scrollPane.getVerticalScrollBar().setValue( getVerticalScrollBar().getMaximum() );
Both of these answers assume the GUI is already visible.
The first part of Rob's answer is the way to go - the missing piece is to wrap the scrollRectToVisible into SwingUtilities.invokeLater. Doing so delays the scrolling until all pending events are processed, that is until all internal state is updated. A code snippet (in swingx test support speak, simply replace the frame creation and scrollpane wrapping with manually created code)
final JComponent panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
for(int i = 0; i < 10; i++)
panel.add(new JLabel("initial message " + i));
JXFrame frame = wrapWithScrollingInFrame(panel, "scroll to bottom");
Action action = new AbstractAction("addMessage") {
int count;
#Override
public void actionPerformed(ActionEvent e) {
final JLabel label = new JLabel("added message " + count++);
panel.add(label);
panel.revalidate();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
label.scrollRectToVisible(label.getBounds());
}
});
}
};
frame.add(new JButton(action), BorderLayout.SOUTH);
show(frame);
Scratched my head for ages over this, but after asking the question I finally figured it out.
To solve my problem all I need to do was listen for a change in value in the JScrollPane's scroll bar. If it changed, done some calculations, and scroll to the bottom if necessary.
Care has to be taken to ensure that you're not overriding the user moving the scroll bar however.
In particular you're looking at the track which is an AdjustmentEvent. This event is also fired when a user moves the scroll bar.
In order to allow the user to scroll without forcibly scroll it to the bottom, I always keep track of the maximum scroll bar value. If when track is fired the new max value is higher than the current one then a new item has been added and we should think about scrolling to the bottom. If the values are equal then the user is scrolling the scroll bar and we do nothing.
The event listeners can be found on this website and can be make to work very easily: Listening for Scrollbar Value Changes in a JScrollPane Container
import java.awt.Color;
import java.awt.Container;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import javax.swing.*;
public class MyClass {
private JPanel p;
private JScrollPane s;
private Container contentPane;
private int scrollBarMax;
public MyClass(Container contentPane) {
this.contentPane = contentPane;
this.p = new JPanel();
this.p.setBackground(Color.WHITE);
BoxLayout boxLayout = new BoxLayout(this.p, BoxLayout.Y_AXIS);
this.p.setLayout(boxLayout);
this.s = new JScrollPane(this.p);
this.s.setSize(400, 364);
this.contentPane.add(this.s);
this.s.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener(){
#Override
public void adjustmentValueChanged(AdjustmentEvent evt) {
if (evt.getValueIsAdjusting()) {
return;
}
if (evt.getAdjustmentType() == AdjustmentEvent.TRACK) {
if (scrollBarMax < s.getVerticalScrollBar().getMaximum()) {
if ((s.getVerticalScrollBar().getValue() + s.getVerticalScrollBar().getSize().height) == scrollBarMax) {
//scroll bar is at the bottom, show the last added JLabel
scrollBarMax = s.getVerticalScrollBar().getMaximum();
scrollToBottom();
} else {
//scroll bar is not at the bottom, user has moved it
scrollBarMax = s.getVerticalScrollBar().getMaximum();
}
}
}
}
});
scrollBarMax = s.getVerticalScrollBar().getMaximum();
}
public final JLabel makeJLabel(String message) {
JLabel jLabel = new JLabel("<html><p style=\"padding-left:15px;padding-right:15px;width:280px;\">" + message.replaceAll("(\r\n|\n)", "<br />") + "</p></html>");
/*
some stuff here to calculate pref/max size and add an imageicon
*/
p.add(jLabel);
scrollBarMax = s.getVerticalScrollBar().getMaximum();
return jLabel;
}
public void scrollToBottom() {
JScrollBar vertical = s.getVerticalScrollBar();
vertical.setValue(vertical.getMaximum());
}
}

Categories