I have a BoxPanel of buttons in my scala swing app that looks ugly to me because the buttons are all different sizes. I had changed it to a GridPanel but then they filled the panel vertically aswell which I found uglier. How can I have all buttons fill the width of the BoxPanel but stay their perferred height?
I tried a work around, shown below, where the panel sets all the contents to the max width but it had no effect.
val buttons = new BoxPanel(Orientation.Vertical) {
contents += new Button("Normal Button")
contents += new Button("small")
contents += new Button("Significantly larger button than the rest")
val maxWidth = contents map {
(button: Component) => button.preferredSize
} maxBy { _.width }
contents foreach {
(button: Component) => button.preferredSize = maxWidth
}
}
Is there a way to make the above workaround work or a way that isn't a workaround?
A s discussed in Box Layout Features, "if all the components have identical X alignment, then all components are made as wide as their container." Override the button's getMaximumSize() implementation as shown below to return an arbitrary width and the button's preferred hight. Change setHorizontalAlignment() and/or resize the frame to see the effect.
#Override
public Dimension getMaximumSize() {
return new Dimension(
Short.MAX_VALUE, getPreferredSize().height);
}
Code as shown:
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
/** #see http://stackoverflow.com/a/34443937/230513 */
public class ButtonBoxTest {
private void display() {
JFrame f = new JFrame("Test");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new JPanel() { //arbitrary filler
#Override
public Dimension getPreferredSize() {
return new Dimension(320, 240);
}
});
f.add(createButtonPanel(), BorderLayout.EAST);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private JPanel createButtonPanel() {
JPanel btnPanel = new JPanel();
btnPanel.setLayout(new BoxLayout(btnPanel, BoxLayout.Y_AXIS));
btnPanel.add(createButton("Button 1"));
btnPanel.add(createButton("Button 2"));
btnPanel.add(createButton("Long Button 3"));
btnPanel.add(createButton("Button 4"));
btnPanel.add(createButton("Button 5"));
return btnPanel;
}
private JButton createButton(String name) {
final JButton b = new JButton(name) {
#Override
public Dimension getMaximumSize() {
return new Dimension(
Short.MAX_VALUE, getPreferredSize().height);
}
};
b.setAlignmentX(0.5f);
b.setHorizontalAlignment(JButton.RIGHT);
return b;
}
public static void main(String[] args) {
EventQueue.invokeLater(new ButtonBoxTest()::display);
}
}
TrashGod is right but I figured I would post an answer translated to Scala as his is in Java. I got my original workaround to work by replacing preferredSize with maximumSize, thanks to reading trashgod's answer.
val buttons = new BoxPanel(Orientation.Vertical) {
contents += new Button("Normal Button")
contents += new Button("small")
contents += new Button("Significantly larger button than the rest")
val maxWidth = contents map {
(button: Component) => button.maximumSize
} maxBy { _.width }
contents foreach {
(button: Component) => button.maximumSize = maxWidth
}
}
I was also able to get a solution translated from trashgod's answer with just
private def equalButtons(name: String): Button = {
new Button(name) {
maximumSize = new Dimension(Short.MaxValue, maximumSize.height)
}
}
val buttons = new BoxPanel(Orientation.Vertical) {
contents += new equalButtons("Normal Button")
contents += new equalButtons("small")
contents += new equalButtons("Significantly larger button than the rest")
}
Related
I am developing an application with a Scala-swing front-end. I have a MainFrame that is populated and working well. I have a dialog that works well too. But when I access the parent frame from the dialog the contents of the frame clear. The MemuBar is still there.
I only need to centre the dialog on the frame, so I just passed a Point (after trying to do it right) and even that causes the problem. I can set a location point created in the dialog I just can't access the frame to do it. And this I really don't get; I create the point in the frame and send it to the dialog, that is fine, but setting the dialog location to it clears the frame.
I am using "org.scala-lang.modules" % "scala-swing_2.11" % "1.0.2"
Any one have any ideas?
Thanks!
On the other hand this demo code works fine, so it isn't that simple
package hack
import scala.swing.{Action, BorderPanel, Button, Dialog, Dimension, FlowPanel, Frame, MainFrame, Menu, MenuBar, MenuItem, SimpleSwingApplication, TabbedPane}
/**
* Created by bday on 3/31/16.<br>
* <br>
* FrameClearingDialog will do something useful I'm sure
*/
class FrameClearingDialog (parent: Frame) {
val dialog = new Dialog
dialog.contents = new FlowPanel() {
preferredSize = new Dimension(500,500)
}
dialog.open()
dialog.setLocationRelativeTo(parent)
}
class Parent extends SimpleSwingApplication {
override def top: Frame = new MainFrame {
title = "Hack "
preferredSize = new Dimension(1000,1000)
menuBar = new MenuBar() {
contents += new Menu("Menu") {
contents += new MenuItem(Action("Show Dialog") {
createAndShowDialog
})
}
}
val panel = new BorderPanel() {
layout(new Button() {text="button"}) = BorderPanel.Position.North
layout(new Button() {text="button"}) = BorderPanel.Position.Center
layout(new Button() {text="button"}) = BorderPanel.Position.South
}
contents = new TabbedPane() {
pages += new TabbedPane.Page("Page", panel)
}
}
def createAndShowDialog = {
new FrameClearingDialog(top)
}
}
object Starter extends App {
val demo = new Parent
demo.main(args)
}
This is not the answer, but it does explain the issue enough to understand and avoid it.
The problem seems to be scope. If the content is created inside the MainFrame constructor it survives the call from the child, if created outside it does not. Swing does some strange things sometimes and I am not going to spend more time on this now.
If you move the creation of "map" into the MainFrame then this example will work correctly.
package hack
import scala.swing.{Action, BorderPanel, BoxPanel, Button, Dialog, Dimension, FlowPanel, Frame, MainFrame, Menu, MenuBar, MenuItem, Orientation, Panel, Point, RichWindow, SimpleSwingApplication, TabbedPane}
/**
* Created by bday on 3/31/16.<br>
* <br>
* Utils will do something useful I'm sure
*/
object Utils {
def findCenter(window: RichWindow) = {
new Point(window.location.x + window.size.width/2, window.location.y + window.size.height/2)
}
def centerMe(parent: RichWindow, child: RichWindow) = {
val parentCenter = findCenter(parent)
val childCenter = findCenter(child)
new Point(parentCenter.x - childCenter.x, parentCenter.y - childCenter.y)
}
}
/**
* Created by bday on 3/31/16.<br>
* <br>
* FrameClearingDialog will do something useful I'm sure
*/
class FrameClearingDialog (parent: Frame) {
val dialog = new Dialog
dialog.contents = new FlowPanel() {
preferredSize = new Dimension(500, 500)
}
dialog.location = Utils.centerMe(parent, dialog)
dialog.open()
}
class Parent extends SimpleSwingApplication {
val map = {
var map = Map.empty[Int, Panel]
for (x <- 1 to 5) {
map += x -> createPanel(x)
}
map
}
override def top: Frame = new MainFrame {
title = "Hack "
preferredSize = new Dimension(1000,1000)
menuBar = new MenuBar() {
contents += new Menu("Menu") {
contents += new MenuItem(Action("Show Dialog") {
createAndShowDialog
})
}
}
contents = new TabbedPane() {
for (x <- 1 to 5) {
pages += new TabbedPane.Page(s"Page $x", map(x))
}
}
}
def createPanel(x: Int) = {
new BoxPanel(Orientation.Vertical) {
contents += new Button() {text=s"button $x"}
}
}
def createAndShowDialog = {
new FrameClearingDialog(top)
}
}
object Starter extends App {
val demo = new Parent
demo.main(args)
}
I wonder if there is a way to change to source code format automatically produced
by Net Beans IDE in GUI - applet applications. For example placement of the items in the source code are relational but what if I want them in absolute coordinates. I am asking this question because I need source code in that format so that I can easily change source code and can do some manual job. More specially, I want to create a Button Group of 12x8 array with no gap between them . But using IDE to do this takes long time and indeed, I couldn't even placed the buttons with no gap between them. Any help highly appreciated!
This is simple to put together manually. GUI builders usually harm more than they help.
Here's the test run:
And here's the code. I put the classes together in one file to make it easier to paste. The classes should be in separate files.
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class ButtonArray implements Runnable {
#Override
public void run() {
JFrame frame = new JFrame("JButton Array Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ButtonPanel buttonPanel = new ButtonPanel();
frame.add(buttonPanel.getMainPanel());
frame.setLocationByPlatform(true);
// frame.setSize(new Dimension(800, 600));
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new ButtonArray());
}
public class ButtonPanel {
private static final int WIDTH = 12;
private static final int HEIGHT = 8;
private JButton[][] buttonArray;
private JPanel mainPanel;
public ButtonPanel() {
buttonArray = new JButton[WIDTH][HEIGHT];
createPartControl();
}
private void createPartControl() {
mainPanel = new JPanel();
mainPanel.setLayout(new GridLayout(HEIGHT, WIDTH));
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
buttonArray[j][i] =
new JButton(createButtonText(j, i));
mainPanel.add(buttonArray[j][i]);
}
}
}
private String createButtonText(int j, int i) {
StringBuilder builder = new StringBuilder();
builder.append("(");
builder.append(i);
builder.append(", ");
builder.append(j);
builder.append(")");
return builder.toString();
}
public JPanel getMainPanel() {
return mainPanel;
}
}
}
You need to use some grid like layout for the panel (ex. FormLayout) configure it and simply add all buttons there.
I'm trying to paint component inside paint(Graphics) method of JPanel.
The following code snippet works just fine, a JButton is painted nicely in my JPanel:
#Override
public void paint(Graphics g) {
super.paint(g);
JButton btn = new JButton("hello");
Dimension dim = btn.getPreferredSize();
btn.setSize(dim.width, dim.height);
btn.paint(g); // paint the button
}
The code snippet works perfectly also for other components (JLabel, JTree, ...) except JPanel.
The following code will cause very strange NullPointerException at java.awt.Window.access$700(Window.java:132).
#Override
public void paint(Graphics g) {
super.paint(g);
JPanel panel = new JPanel();
panel.setSize(10, 10);
panel.paint(g); // paint the panel
}
Here the full stacktrace:
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
at java.awt.Window.access$700(Window.java:132)
at java.awt.Window$1.isOpaque(Window.java:3458)
at javax.swing.RepaintManager.getVolatileOffscreenBuffer(RepaintManager.java:983)
at javax.swing.RepaintManager$PaintManager.paint(RepaintManager.java:1395)
at javax.swing.BufferStrategyPaintManager.paint(BufferStrategyPaintManager.java:294)
at javax.swing.RepaintManager.paint(RepaintManager.java:1224)
at javax.swing.JComponent.paint(JComponent.java:1015)
at test.paintcontainer.TestPaintContainerMain$TestContentPane.paint(TestPaintContainerMain.java:48)
at javax.swing.JComponent.paintChildren(JComponent.java:862)
at javax.swing.JComponent.paint(JComponent.java:1038)
at javax.swing.JLayeredPane.paint(JLayeredPane.java:567)
at javax.swing.JComponent.paintChildren(JComponent.java:862)
at javax.swing.JComponent.paintToOffscreen(JComponent.java:5131)
at javax.swing.BufferStrategyPaintManager.paint(BufferStrategyPaintManager.java:278)
at javax.swing.RepaintManager.paint(RepaintManager.java:1224)
at javax.swing.JComponent.paint(JComponent.java:1015)
at java.awt.GraphicsCallback$PaintCallback.run(GraphicsCallback.java:21)
at sun.awt.SunGraphicsCallback.runOneComponent(SunGraphicsCallback.java:60)
at sun.awt.SunGraphicsCallback.runComponents(SunGraphicsCallback.java:97)
at java.awt.Container.paint(Container.java:1780)
at java.awt.Window.paint(Window.java:3375)
at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:796)
at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:713)
at javax.swing.RepaintManager.seqPaintDirtyRegions(RepaintManager.java:693)
at javax.swing.SystemEventQueueUtilities$ComponentWorkRequest.run(SystemEventQueueUtilities.java:125)
at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209)
at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:641)
at java.awt.EventQueue.access$000(EventQueue.java:84)
at java.awt.EventQueue$1.run(EventQueue.java:602)
at java.awt.EventQueue$1.run(EventQueue.java:600)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.AccessControlContext$1.doIntersectionPrivilege(AccessControlContext.java:87)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:611)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:269)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:184)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:174)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:169)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:161)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)
Any idea how to solve this problem? I need to paint JPanel inside paint(Graphics) method.
I wrote a simple test application which you can copy-paste to reproduce the aforementioned exception:
package test.paintcontainer;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
public class TestPaintContainerMain extends JFrame {
public static void main(String[] args) {
TestPaintContainerMain test = new TestPaintContainerMain();
test.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
test.setBounds(0, 0, 300, 200);
test.setContentPane(new TestContentPane());
test.setVisible(true);
}
static class TestContentPane extends JPanel {
JRadioButton paintButtonCheck;
JRadioButton paintPanelCheck;
public TestContentPane() {
paintButtonCheck = createRadioButton("paint button", true);
paintPanelCheck = createRadioButton("paint panel", false);
ButtonGroup buttonGroup = new ButtonGroup();
buttonGroup.add(paintButtonCheck);
buttonGroup.add(paintPanelCheck);
add(paintButtonCheck);
add(paintPanelCheck);
}
#Override
public void paint(Graphics g) {
super.paint(g);
g.translate(100, 100);
if (paintButtonCheck.isSelected()) {
createButton().paint(g);
} else {
createPanel().paint(g);
}
}
private JButton createButton() {
JButton button = new JButton("button");
button.setSize(button.getPreferredSize().width, button.getPreferredSize().height);
return button;
}
private JPanel createPanel() {
JPanel panel = new JPanel();
panel.setBackground(Color.GREEN);
panel.add(createButton());
panel.setSize(panel.getPreferredSize().width, panel.getPreferredSize().height);
return panel;
}
private JRadioButton createRadioButton(String title, boolean selected) {
JRadioButton radio = new JRadioButton(title, selected);
radio.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
TestContentPane.this.repaint();
}
});
return radio;
}
}
}
This is most likely not a bug in Swing, but more of a problem because you are trying to paint a component which has not yet been realized, meaning it has no active graphic context. You can realize a component by adding it to already realized component like your JFrame - which itself gets realized by setVisible(true).
Also one should probably never call JComponent.paint(Graphics) manually, because this is the job of Swing (more precisely the Event Dispatcher Thread) - it even says so in the documentation of the paint method:
Applications should not invoke paint directly, but should instead use the repaint method to schedule the component for redrawing.
What you can call is the method printAll(Graphics g), which paints the component and all its subcomponents. Also in Swing one should also not override paint but paintComponent.
So here is a test code:
JButton button = createButton();
JPanel panel = createPanel();
public TestContentPane() {
paintButtonCheck = createRadioButton("paint button", true);
paintPanelCheck = createRadioButton("paint panel", false);
ButtonGroup buttonGroup = new ButtonGroup();
buttonGroup.add(paintButtonCheck);
buttonGroup.add(paintPanelCheck);
add(paintButtonCheck);
add(paintPanelCheck);
//Hack, just prove something (realize both components)
add(panel);
add(button);
}
...
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.translate(100, 100);
if (paintButtonCheck.isSelected()) {
button.paintAll(g);
} else {
panel.paintAll(g);
}
g.translate(-100, -100);
}
This should work (although you will obviously have two components on the screen you don't want). Also note "reset" the graphics object, because it will still be used afterwards by Swing.
So this is the theory, but it's not yet an actual solution.
My solution to your problem is: "Don't do it like this"!
Components are not like images, in the sense that they don't look the same everywhere. The output of the paintAll call will be different, depending on how (or where) the components were realized.
So one suggestion is to show actual components. Create your tooltip box, add your panel and your button and let them draw themselves. You can even subclass these components and override their paintComponent() methods, add transparency and all. It will require some work, but Swing was never known to be easy.
I just found a solution.
The only modification of a sample code from my question is that called panel.setDoubleBuffered(false) on JPanel I was trying to paint.
However, I would still consider the exeption to be a Swing bug. If double buffering should be turned off by design you shouldn't get NullPointerException but some other, more meaningful exception which explains the condition.
Here is a fixed sample application:
package test.paintcontainer;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
public class TestPaintContainerMain extends JFrame {
public static void main(String[] args) {
TestPaintContainerMain test = new TestPaintContainerMain();
test.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
test.setBounds(0, 0, 300, 200);
test.setContentPane(new TestContentPane());
test.setVisible(true);
}
static class TestContentPane extends JPanel {
JRadioButton paintButtonCheck;
JRadioButton paintPanelCheck;
public TestContentPane() {
paintButtonCheck = createRadioButton("paint button", false);
paintPanelCheck = createRadioButton("paint panel", true);
ButtonGroup buttonGroup = new ButtonGroup();
buttonGroup.add(paintButtonCheck);
buttonGroup.add(paintPanelCheck);
add(paintButtonCheck);
add(paintPanelCheck);
}
#Override
public void paint(Graphics g) {
super.paint(g);
g.translate(100, 100);
if (paintButtonCheck.isSelected()) {
createButton().paint(g);
} else {
createPanel().paint(g);
}
}
private JButton createButton() {
JButton button = new JButton("button");
button.setSize(button.getPreferredSize().width, button.getPreferredSize().height);
return button;
}
private JPanel createPanel() {
JPanel panel = new JPanel();
panel.setBackground(Color.GREEN);
panel.add(createButton());
// --------------------------------
panel.setDoubleBuffered(false); // <-- TURN OFF DOUBLE BUFFERING
// --------------------------------
panel.setSize(panel.getPreferredSize().width, panel.getPreferredSize().height);
return panel;
}
private JRadioButton createRadioButton(String title, boolean selected) {
JRadioButton radio = new JRadioButton(title, selected);
radio.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
TestContentPane.this.repaint();
}
});
return radio;
}
}
}
I use JToolbarButton button, I want to make it be "pressed" when I click on it, just like JButton works.How can I do this?
Please help!Thanks.
As mentioned in Costis' reply, you are probably after a JToggleButton. It might also be necessary to suppress the painting of the border, as in the 2nd tool bar in this example.
import java.awt.*;
import java.awt.image.*;
import javax.swing.*;
class ToggleBar {
public static JToggleButton getButton(
Image selected,
Image unselected,
boolean decorated) {
JToggleButton b = new JToggleButton();
b.setSelectedIcon(new ImageIcon(selected));
b.setIcon(new ImageIcon(unselected));
b.setBorderPainted(decorated);
return b;
}
public static Image getCircleImage(Color c) {
BufferedImage bi = new BufferedImage(
32,32,BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bi.createGraphics();
g.setColor(c);
g.fillOval(0,0,32,32);
g.dispose();
return bi;
}
public static void main(String[] args) {
SwingUtilities.invokeLater( new Runnable() {
public void run() {
Image red = getCircleImage(Color.RED);
Image green = getCircleImage(Color.GREEN);
JPanel p = new JPanel(new GridLayout(0,1));
JToolBar tb1 = new JToolBar();
for (int ii=0; ii<5; ii++) {
tb1.add( getButton(red, green, true) );
}
p.add(tb1);
JToolBar tb2 = new JToolBar();
for (int ii=0; ii<5; ii++) {
tb2.add( getButton(red, green, false) );
}
p.add(tb2);
JOptionPane.showMessageDialog(null, p);
}
});
}
}
There is a JToolbarButton in Apache Batik.
It seems to me though that what you are looking for is a JToggleButton. You can adjust it to display a small image and to be small.
jToggleButton1.setIcon(new javax.swing.ImageIcon("image.png"));
jToggleButton1.setSelectedIcon(new javax.swing.ImageIcon("selected_image.png"));
Add another image with setSelectedIcon in order to make you button look pressed.
The basic setup is this: I have a vertical JSplitPane that I want to have a fixed-size bottom component and a resizing top component, which I accomplished by calling setResizeWeight(1.0). In this application there is a button to restore the "default" window configuration. The default height of the window is the desktop height, and the default divider location is 100 pixels from the bottom of the split pane.
To set the divider location to 100px, I take the JSplitPane height - 100. The problem is, just before this I resize the JFrame, and since the code is in a button callback, the JSplitPane has been invalidated but not yet resized. So the divider location is set incorrectly.
Here is a SSCCE. Click the button twice to see the problem. The first click will resize the window, but the divider location remains the same (relative to the bottom of the window). The second click properly moves the divider, since the window size didn't change.
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JSplitPane;
public class SSCCE {
/**
* #param args unused
*/
public static void main(String[] args) {
new SSCCE();
}
private final JFrame f = new JFrame("JSplitPane SSCE");
private final JSplitPane sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT,true);
public SSCCE() {
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
sp.add(new JLabel("top"));
sp.add(new JLabel("bottom"));
sp.setResizeWeight(1.0);
f.getContentPane().add(sp);
f.getContentPane().add(new JButton(new AbstractAction("Resize to Default") {
#Override
public void actionPerformed(ActionEvent e) {
restoreDefaults();
}
}),BorderLayout.PAGE_END);
f.setSize(400,300);
f.setVisible(true);
}
void restoreDefaults() {
f.setSize(f.getWidth(), getDesktopRect(f.getGraphicsConfiguration()).height);
sp.setDividerLocation(sp.getSize().height - 100); // Does not work on first button press
}
Rectangle getDesktopRect(GraphicsConfiguration gc) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension size = toolkit.getScreenSize();
Insets insets = toolkit.getScreenInsets(gc);
return new Rectangle(insets.left, insets.top, size.width - (insets.left + insets.right), size.height - (insets.top + insets.bottom));
}
}
I have thought of a few ways I might get around this, but they all seem sort of hackish. So far the best idea I've had has been to call f.validate() in between setting the frame size and setting the divider location, but I'm concerned there might be side effects to forcing validation early.
The other option I thought of is to use EventQueue.invokeLater() to put the call to set the divider location at the end of the event queue. But that seems risky to me - I'm assuming the JSplitPane will have been validated at that point, and I'm concerned that may be a faulty assumption to make.
Is there a better way?
Took a while (probably due to being early morning here :-) to understand the problem, so just to make sure I got it:
the size of the bottom component can be whatever the user decides at all times
when resizing the frame all height change should happen to the top component
there's an option to restore to default sizes, independent of any setting before
"default" means the bottom component must have a fixed height of xx
If so, the solution is to separate the frame resizing from the sizing the bottom component. Your second option is dead on: resize the frame and wrap the bottom comp resize into a invokeLater (EventQueue or SwingUtilities, doesn't matter).
void restoreDefaults() {
f.setSize(f.getWidth(), getDesktopRect(f.getGraphicsConfiguration()).height);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
sp.setDividerLocation(sp.getSize().height - 100);
}
});
}
That's guaranteed to work as expected, because the invokeLater puts the request as last after all already queued events:
/**
* Causes <i>doRun.run()</i> to be executed asynchronously on the
* AWT event dispatching thread. This will happen after all
* pending AWT events have been processed. [...]
* If invokeLater is called from the event dispatching thread --
* for example, from a JButton's ActionListener -- the <i>doRun.run()</i> will
* still be deferred until all pending events have been processed.
You could create a custom action class that handles the button click and the resize event. This approach would look like this:
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JSplitPane;
public class SSCCE {
/**
* #param args unused
*/
public static void main(String[] args) {
new SSCCE();
}
private final JFrame f = new JFrame("JSplitPane SSCE");
private final JSplitPane sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT,true);
public SSCCE() {
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
sp.add(new JLabel("top"));
sp.add(new JLabel("bottom"));
sp.setResizeWeight(1.0);
f.getContentPane().add(sp);
CustomListener resizeViaButtonListener = new CustomListener("Resize to Default");
f.getContentPane().add(new JButton(resizeViaButtonListener), BorderLayout.PAGE_END);
f.addComponentListener(resizeViaButtonListener);
f.setSize(400,300);
f.setVisible(true);
}
void restoreDefaults() {
f.setSize(f.getWidth(), getDesktopRect(f.getGraphicsConfiguration()).height);
sp.setDividerLocation(sp.getSize().height - 100); // Does not work on first button press
}
Rectangle getDesktopRect(GraphicsConfiguration gc) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension size = toolkit.getScreenSize();
Insets insets = toolkit.getScreenInsets(gc);
return new Rectangle(insets.left, insets.top, size.width - (insets.left + insets.right), size.height - (insets.top + insets.bottom));
}
class CustomListener extends AbstractAction implements ComponentListener {
CustomListener(String actionDescription) {
super(actionDescription);
}
private boolean resizedViaButtonClick = false;
#Override
public void actionPerformed(ActionEvent arg0) {
resizedViaButtonClick = true;
f.setSize(f.getWidth(), getDesktopRect(f.getGraphicsConfiguration()).height);
sp.setDividerLocation(sp.getSize().height - 100);
// you need this also here because if the component is not resized when clicking the button
// it is possible that the divider location must be changed. This happens when the user clicks
// the button after changing the divider but not resizing the frame.
}
#Override
public void componentResized(ComponentEvent e) {
if ( resizedViaButtonClick ) {
resizedViaButtonClick = false;
sp.setDividerLocation(sp.getSize().height - 100);
}
}
#Override
public void componentHidden(ComponentEvent e) { /* do nothing */ }
#Override
public void componentMoved(ComponentEvent e) { /* do nothing */ }
#Override
public void componentShown(ComponentEvent e) { /* do nothing */ }
}
}
This way the code that is responsible for handling the logical task of setting the standard size will be in one single and easy to understand class.
nothing complicated, basic Swing Rules
import java.awt.*;
import java.awt.event.ActionEvent;
import javax.swing.*;
public class SSCCE {
/**
* #param args unused
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
SSCCE sSCCE = new SSCCE();
}
});
}
private final JFrame f = new JFrame("JSplitPane SSCE");
private final JSplitPane sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
true);
public SSCCE() {
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
sp.add(new JLabel("top"));
sp.add(new JLabel("bottom"));
sp.setResizeWeight(1.0);
f.getContentPane().add(sp);
f.getContentPane().add(new JButton(new AbstractAction(
"Resize to Default") {
private static final long serialVersionUID = 1L;
#Override
public void actionPerformed(ActionEvent e) {
System.out.println(sp.getLastDividerLocation());
restoreDefaults();
}
}), BorderLayout.PAGE_END);
f.setPreferredSize(new Dimension(400, 300));
f.pack();
f.setVisible(true);
}
void restoreDefaults() {
//EventQueue.invokeLater(new Runnable() {
// #Override
// public void run() {
f.setPreferredSize(new Dimension(f.getWidth(),
getDesktopRect(f.getGraphicsConfiguration()).height));
f.pack();
sp.setDividerLocation(sp.getSize().height - 100);
// Does not work on first button press
// }
//});
}
Rectangle getDesktopRect(GraphicsConfiguration gc) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension size = toolkit.getScreenSize();
Insets insets = toolkit.getScreenInsets(gc);
return new Rectangle(insets.left, insets.top,
size.width - (insets.left + insets.right),
size.height - (insets.top + insets.bottom));
}
}
but I think pack() may be better than validate()
I generally try to avoid invoking setPreferredSize() on any component. I would rather let the layout manager do its job. In this case this would mean setting the size of the frame and let the BorderLayout take all the available space.
void restoreDefaults() {
// f.setSize(f.getWidth(), getDesktopRect(f.getGraphicsConfiguration()).height);
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
Rectangle bounds = env.getMaximumWindowBounds();
f.setSize(f.getWidth(), bounds.height);
f.validate();
sp.setDividerLocation(sp.getSize().height - 100); // Does not work on first button press
}