/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package edu.mit.csail.sdg.alloy4;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.net.URL;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JSplitPane;
import javax.swing.KeyStroke;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.plaf.basic.BasicSplitPaneUI;
/** Graphical convenience methods.
*
* <p><b>Thread Safety:</b> Can be called only by the AWT event thread.
*/
public final class OurUtil {
/** This constructor is private, since this utility class never needs to be instantiated. */
private OurUtil() { }
/** Assign the given attributes to the given JComponent, then return the JComponent again.
* <p> If <b>Font</b> x is given in the list, we call obj.setFont(x)
* <p> If <b>String</b> x is given in the list, we call obj.setToolTipText(x)
* <p> If <b>Border</b> x is given in the list, we call obj.setBorder(x)
* <p> If <b>Dimension</b> x is given in the list, we call obj.setPreferredSize(x)
* <p> If <b>Color</b> x is given in the list, and it's the first color, we call obj.setForeground(x)
* <p> If <b>Color</b> x is given in the list, and it's not the first color, we call obj.setBackground(x) then obj.setOpaque(true)
* <p> (If no Font is given, then after all these changes have been applied, we will call obj.setFont() will a default font)
*/
public static<X extends JComponent> X make(X obj, Object... attributes) {
boolean hasFont = false, hasForeground = false;
if (attributes!=null) for(Object x: attributes) {
if (x instanceof Font) { obj.setFont((Font)x); hasFont=true; }
if (x instanceof String) { obj.setToolTipText((String)x); }
if (x instanceof Border) { obj.setBorder((Border)x); }
if (x instanceof Dimension) { obj.setPreferredSize((Dimension)x); }
if (x instanceof Color && !hasForeground) { obj.setForeground((Color)x); hasForeground=true; continue; }
if (x instanceof Color) { obj.setBackground((Color)x); obj.setOpaque(true); }
}
if (!hasFont) obj.setFont(getVizFont());
return obj;
}
/** Make a JLabel, then call Util.make() to apply a set of attributes to it.
* @param attributes - see {@link edu.mit.csail.sdg.alloy4.OurUtil#make OurUtil.make(component, attributes...)}
*/
public static JLabel label (String label, Object... attributes) { return make(new JLabel(label), attributes); }
/** Make a JTextField with the given text and number of columns, then call Util.make() to apply a set of attributes to it.
* @param attributes - see {@link edu.mit.csail.sdg.alloy4.OurUtil#make OurUtil.make(component, attributes...)}
*/
public static JTextField textfield (String text, int columns, Object... attributes) {
return make(new JTextField(text, columns), attributes);
}
/** Make a JTextArea with the given text and number of rows and columns, then call Util.make() to apply a set of attributes to it.
* @param attributes - see {@link edu.mit.csail.sdg.alloy4.OurUtil#make OurUtil.make(component, attributes...)}
*/
public static JTextArea textarea (String text, int rows, int columns, boolean editable, boolean wrap, Object... attributes) {
JTextArea ans = make(new JTextArea(text, rows, columns), Color.BLACK, Color.WHITE, new EmptyBorder(0,0,0,0));
ans.setEditable(editable);
ans.setLineWrap(wrap);
ans.setWrapStyleWord(wrap);
return make(ans, attributes);
}
/** Make a JScrollPane containing the given component (which can be null), then apply a set of attributes to it.
* @param attributes - see {@link edu.mit.csail.sdg.alloy4.OurUtil#make OurUtil.make(component, attributes...)}
*/
public static JScrollPane scrollpane (Component component, Object... attributes) {
JScrollPane ans = make(new JScrollPane(), new EmptyBorder(0,0,0,0));
if (component!=null) ans.setViewportView(component);
ans.setMinimumSize(new Dimension(50, 50));
return make(ans, attributes);
}
/** Returns the recommended font to use in the visualizer, based on the OS. */
public static Font getVizFont() {
return Util.onMac() ? new Font("Lucida Grande", Font.PLAIN, 11) : new Font("Dialog", Font.PLAIN, 12);
}
/** Returns the screen width (in pixels). */
public static int getScreenWidth() { return Toolkit.getDefaultToolkit().getScreenSize().width; }
/** Returns the screen height (in pixels). */
public static int getScreenHeight() { return Toolkit.getDefaultToolkit().getScreenSize().height; }
/** Make a graphical button
* @param label - the text to show beneath the button
* @param tip - the tooltip to show when the mouse hovers over the button
* @param iconFileName - if nonnull, it's the filename of the icon to show (it will be loaded from an accompanying jar file)
* @param func - if nonnull, it's the function to call when the button is pressed
*/
public static JButton button (String label, String tip, String iconFileName, ActionListener func) {
JButton button = new JButton(label, (iconFileName!=null && iconFileName.length()>0) ? loadIcon(iconFileName) : null);
if (func != null) button.addActionListener(func);
button.setVerticalTextPosition(JButton.BOTTOM);
button.setHorizontalTextPosition(JButton.CENTER);
button.setBorderPainted(false);
button.setFocusable(false);
if (!Util.onMac()) button.setBackground(new Color(0.9f, 0.9f, 0.9f));
button.setFont(button.getFont().deriveFont(10.0f));
if (tip != null && tip.length() > 0) button.setToolTipText(tip);
return button;
}
/** Load the given image file from an accompanying JAR file, and return it as an Icon object. */
public static Icon loadIcon(String pathname) {
URL url = OurUtil.class.getClassLoader().getResource(pathname);
if (url!=null) return new ImageIcon(Toolkit.getDefaultToolkit().createImage(url));
return new ImageIcon(new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB));
}
/** Make a JPanel with horizontal or vertical BoxLayout, then add the list of components to it (each aligned by xAlign and yAlign)
* <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
* <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
* <br> If a component is String, we will insert a JLabel with it as the label.
* <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
* <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
*/
private static JPanel makeBox(boolean horizontal, float xAlign, float yAlign, Object[] components) {
JPanel ans = new JPanel();
ans.setLayout(new BoxLayout(ans, horizontal ? BoxLayout.X_AXIS : BoxLayout.Y_AXIS));
ans.setAlignmentX(0.0f);
ans.setAlignmentY(0.0f);
Color color = null;
for(Object x: components) {
Component c = null;
if (x instanceof Color) { color = (Color)x; ans.setBackground(color); continue; }
if (x instanceof Dimension) { ans.setPreferredSize((Dimension)x); ans.setMaximumSize((Dimension)x); continue; }
if (x instanceof Component) { c = (Component)x; }
if (x instanceof String) { c = label((String)x, Color.BLACK); }
if (x instanceof Integer) { int i = (Integer)x; c = Box.createRigidArea(new Dimension(horizontal?i:1, horizontal?1:i)); }
if (x==null) { c = horizontal ? Box.createHorizontalGlue() : Box.createVerticalGlue(); }
if (c==null) continue;
if (color!=null) c.setBackground(color);
if (c instanceof JComponent) { ((JComponent)c).setAlignmentX(xAlign); ((JComponent)c).setAlignmentY(yAlign); }
ans.add(c);
}
return ans;
}
/** Make a JPanel using horizontal BoxLayout, and add the components to it (each component will be center-aligned).
* <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
* <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
* <br> If a component is String, we will insert a JLabel with it as the label.
* <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
* <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
*/
public static JPanel makeH(Object... components) { return makeBox(true, 0.5f, 0.5f, components); }
/** Make a JPanel using horizontal BoxLayout, and add the components to it (each component will be top-aligned).
* <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
* <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
* <br> If a component is String, we will insert a JLabel with it as the label.
* <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
* <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
*/
public static JPanel makeHT(Object... components) { return makeBox(true, 0.5f, 0.0f, components); }
/** Make a JPanel using horizontal BoxLayout, and add the components to it (each component will be bottom-aligned).
* <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
* <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
* <br> If a component is String, we will insert a JLabel with it as the label.
* <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
* <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
*/
public static JPanel makeHB(Object... components) { return makeBox(true, 0.5f, 1.0f, components); }
/** Make a JPanel using vertical BoxLayout, and add the components to it (each component will be left-aligned).
* <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
* <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
* <br> If a component is String, we will insert a JLabel with it as the label.
* <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
* <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
*/
public static JPanel makeVL(Object... components) { return makeBox(false, 0.0f, 0.5f, components); }
/** Make a JPanel using vertical BoxLayout, and add the components to it (each component will be right-aligned).
* <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
* <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
* <br> If a component is String, we will insert a JLabel with it as the label.
* <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
* <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
*/
public static JPanel makeVR(Object... components) { return makeBox(false, 1.0f, 0.5f, components); }
/** Constructs a new SplitPane containing the two components given as arguments
* @param orientation - the orientation (HORIZONTAL_SPLIT or VERTICAL_SPLIT)
* @param first - the left component (if horizontal) or top component (if vertical)
* @param second - the right component (if horizontal) or bottom component (if vertical)
* @param initialDividerLocation - the initial divider location (in pixels)
*/
public static JSplitPane splitpane (int orientation, Component first, Component second, int initialDividerLocation) {
JSplitPane x = make(new JSplitPane(orientation, first, second), new EmptyBorder(0,0,0,0));
x.setContinuousLayout(true);
x.setDividerLocation(initialDividerLocation);
x.setOneTouchExpandable(false);
x.setResizeWeight(0.5);
if (Util.onMac() && (x.getUI() instanceof BasicSplitPaneUI)) {
boolean h = (orientation != JSplitPane.HORIZONTAL_SPLIT);
((BasicSplitPaneUI)(x.getUI())).getDivider().setBorder(new OurBorder(h,h,h,h)); // Makes the border look nicer on Mac OS X
}
return x;
}
/** Convenience method that recursively enables every JMenu and JMenuItem inside "menu".
* @param menu - the menu to start the recursive search
*/
public static void enableAll (JMenu menu) {
for(int i = 0; i < menu.getMenuComponentCount(); i++) {
Component x = menu.getMenuComponent(i);
if (x instanceof JMenuItem) ((JMenuItem)x).setEnabled(true); else if (x instanceof JMenu) enableAll((JMenu)x);
}
}
/** Construct a new JMenu and add it to an existing JMenuBar.
* <p> Note: every time the user expands then collapses this JMenu, we automatically enable all JMenu and JMenuItem objects in it.
*
* @param parent - the JMenuBar to add this Menu into (or null if we don't want to add it to a JMenuBar yet)
* @param label - the label to show on screen (if it contains '&' followed by 'a'..'z', we'll remove '&' and use it as mnemonic)
* @param func - if nonnull we'll call its "run()" method right before expanding this menu
*/
public static JMenu menu (JMenuBar parent, String label, final Runnable func) {
final JMenu x = new JMenu(label.replace("&", ""), false);
if (!Util.onMac()) {
int i = label.indexOf('&');
if (i>=0 && i+1<label.length()) i = label.charAt(i+1);
if (i>='a' && i<='z') x.setMnemonic((i-'a')+'A'); else if (i>='A' && i<='Z') x.setMnemonic(i);
}
x.addMenuListener(new MenuListener() {
public void menuSelected (MenuEvent e) { if (func != null) func.run(); }
public void menuDeselected (MenuEvent e) { OurUtil.enableAll(x); }
public void menuCanceled (MenuEvent e) { OurUtil.enableAll(x); }
});
if (parent!=null) parent.add(x);
return x;
}
/** Construct a new JMenuItem then add it to an existing JMenu.
* @param parent - the JMenu to add this JMenuItem into (or null if you don't want to add it to any JMenu yet)
* @param label - the text to show on the menu
* @param attrs - a list of attributes to apply onto the new JMenuItem
* <p> If one positive number a is supplied, we call setMnemonic(a)
* <p> If two positive numbers a and b are supplied, and a!=VK_ALT, and a!=VK_SHIFT, we call setMnemoic(a) and setAccelerator(b)
* <p> If two positive numbers a and b are supplied, and a==VK_ALT or a==VK_SHIFT, we call setAccelerator(a | b)
* <p> If an ActionListener is supplied, we call addActionListener(x)
* <p> If an Boolean x is supplied, we call setEnabled(x)
* <p> If an Icon x is supplied, we call setIcon(x)
*/
public static JMenuItem menuItem (JMenu parent, String label, Object... attrs) {
JMenuItem m = new JMenuItem(label, null);
int accelMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
boolean hasMnemonic = false;
for(Object x: attrs) {
if (x instanceof Character || x instanceof Integer) {
int k = (x instanceof Character) ? ((int)((Character)x)) : ((Integer)x).intValue();
if (k < 0) continue;
if (k==KeyEvent.VK_ALT) { hasMnemonic = true; accelMask = accelMask | InputEvent.ALT_MASK; continue; }
if (k==KeyEvent.VK_SHIFT) { hasMnemonic = true; accelMask = accelMask | InputEvent.SHIFT_MASK; continue; }
if (!hasMnemonic) { m.setMnemonic(k); hasMnemonic=true; } else m.setAccelerator(KeyStroke.getKeyStroke(k, accelMask));
}
if (x instanceof ActionListener) m.addActionListener((ActionListener)x);
if (x instanceof Icon) m.setIcon((Icon)x);
if (x instanceof Boolean) m.setEnabled((Boolean)x);
}
if (parent!=null) parent.add(m);
return m;
}
/** This method minimizes the window. */
public static void minimize(JFrame frame) { frame.setExtendedState(JFrame.ICONIFIED); }
/** This method alternatingly maximizes or restores the window. */
public static void zoom(JFrame frame) {
int both = JFrame.MAXIMIZED_BOTH;
frame.setExtendedState((frame.getExtendedState() & both)!=both ? both : JFrame.NORMAL);
}
/** Make the frame visible, non-iconized, and focused. */
public static void show(JFrame frame) {
frame.setVisible(true);
frame.setExtendedState(frame.getExtendedState() & ~JFrame.ICONIFIED);
frame.requestFocus();
frame.toFront();
}
}