/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.osedu.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package tufts.vue.gui;
import tufts.Util;
import tufts.vue.Resource;
import tufts.vue.EventRaiser;
import tufts.vue.VueUtil;
import tufts.vue.VUE;
import tufts.vue.VueResources;
import tufts.vue.DEBUG;
import java.util.*;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.GeneralPath;
import java.awt.dnd.*;
import java.awt.datatransfer.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.filechooser.FileSystemView;
import javax.swing.border.*;
import javax.swing.UIManager;
import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.FontUIResource;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.metal.MetalLookAndFeel;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.SimpleAttributeSet;
/**
* Various constants for GUI variables and static method helpers.
*
* @version $Revision: 1.169 $ / $Date: 2010-02-03 19:15:47 $ / $Author: mike $
* @author Scott Fraize
*/
// todo: move most of VueConstants to here
public class GUI
implements tufts.vue.VueConstants/*, java.beans.PropertyChangeListener*/
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(GUI.class);
/** special property key for sending a finalize message to JComponents */
public static final String FINALIZE = "GUI_FINALIZE";
public static Font LabelFace;
public static Font LabelFaceItalic;
public static Font ValueFace;
public static Font TitleFace;
public static Font FixedFace;
public static Font ErrorFace;
public static Font StatusFace;
public static Font StatusFaceSmall;
public static Font DataFace;
public static Font ContentFace;
public static final Color LabelColor = new Color(61,61,61);
public static final int LabelGapRight = 6;
public static final int FieldGapRight = 6;
public static final Insets WidgetInsets = new Insets(8,6,8,6);
public static final Insets WidgetInsets2 = new Insets(16,12,16,12);
public static final Insets WidgetInsets3 = new Insets(20,20,20,20);
public static final Border WidgetInsetBorder = DEBUG.BOXES
? new MatteBorder(WidgetInsets, Color.yellow)
: new EmptyBorder(WidgetInsets);
public static final Border WidgetInsetBorder2 = DEBUG.BOXES
? new MatteBorder(WidgetInsets2, Color.yellow)
: new EmptyBorder(WidgetInsets2);
public static final Border WidgetInsetBorder3 = DEBUG.BOXES
? new MatteBorder(WidgetInsets3, Color.yellow)
: new EmptyBorder(WidgetInsets3);
//public static final Border WidgetBorder = new MatteBorder(WidgetInsets, Color.orange);
/** the special name AWT/Swing gives to pop-up Windows (menu's, rollovers, etc) */
public static final String OVERRIDE_REDIRECT = "###overrideRedirect###";
public static final String POPUP_NAME = "###VUE-POPUP###";
public static final Color AquaFocusBorderLight = new Color(157, 191, 222);
public static final Color AquaFocusBorderDark = new Color(137, 170, 201);
//public static final boolean ControlMaxWindow = false;
//public static final Image NoImage32 = VueResources.getImage("NoImage");
public static final Image NoImage32 = VueResources.getImage("/icon_noimage32.gif");
public static final Image NoImage64 = VueResources.getImage("/icon_noimage64.gif");
public static final Image NoImage128 = VueResources.getImage("/icon_noimage128.gif");
public static boolean UseAlwaysOnTop = false;
/** the currently most "active" device as determined by the position of VUE windows */
private static GraphicsDevice GDevice;
private static GraphicsEnvironment GEnvironment;
private static GraphicsDevice[] GScreenDevices;
/** The raw screen coordiates of the currently "active" display. This is relative
* to the entire space of all displays. In a multi-monitor environment, each
* display device will have it's own coordiates in the global space of all attached
* display devices. Will also record inset margins as termined by java.awt.Toolkit.
*/
private static Screen GScreen;
/** the maximum coordinates of the entire space of all devices -- note that
* this will actually include off-screen regions if the display layout is not perfectly
* rectangular */
private static Insets GSpace;
private static Rectangle GSpaceBounds;
private static final boolean SUPER_SCREEN = true;
public static final Dimension MaxWidth = new Dimension(Short.MAX_VALUE, 1);
public static final Dimension MaxHeight = new Dimension(1, Short.MAX_VALUE);
public static final Dimension MaxSize = new Dimension(Short.MAX_VALUE, Short.MAX_VALUE);
public static final Dimension ZeroSize = new Dimension(0,0);
private static Window FullScreenWindow;
private static boolean initUnderway = true;
private static boolean isMacAqua;
private static boolean isMacAquaBrushedMetal;
private static boolean isOceanTheme = false;
private static javax.swing.plaf.metal.MetalTheme Theme;
private static Color ToolbarColor = null;
static final Color VueColor = new ColorUIResource(VueResources.getColor("menubarColor", Color.blue));
// private static final Color VueColor = new ColorUIResource(new Color(128,0,0)); // test
private static boolean SKIP_CUSTOM_LAF = false; // test: don't install our L&F customizations
private static boolean SKIP_OCEAN_THEME = false; // test: in java 1.5, use default java Metal theme instead of new Ocean theme
private static boolean FORCE_WINDOWS_LAF = false; // test: on mac, use windows look (java metal), on windows, use native windows L&F
private static boolean SKIP_WIN_NATIVE_LAF = false;
public static void parseArgs(String[] args) {
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-skip_custom_laf")) {
SKIP_CUSTOM_LAF = true;
} else if (args[i].equals("-skip_ocean_theme")) {
SKIP_OCEAN_THEME = true;
} else if (args[i].equals("-win") || args[i].equals("-nativeWindowsLookAndFeel")) {
FORCE_WINDOWS_LAF = true;
} else if (args[i].equals("-nowin")) {
SKIP_WIN_NATIVE_LAF = true;
}
}
}
public static Color getVueColor() {
return VueColor;
}
public static Color getToolbarColor() {
if (initUnderway) throw new InitError();
return ToolbarColor;
}
public static void applyToolbarColor(JComponent c) {
if (isMacAqua || Util.isUnixPlatform())
c.setOpaque(false);
else
c.setBackground(getToolbarColor());
}
public static Color getTextHighlightColor() {
if (initUnderway) throw new InitError();
if (Theme == null)
{
//return Color.yellow;
if (Util.isWindowsPlatform() || Util.isUnixPlatform())
return VueResources.getColor("gui.text.highlightcolor");
else
return SystemColor.textHighlight;
}
else
return Theme.getTextHighlightColor();
}
private static Color initToolbarColor() {
//if (true) return new ColorUIResource(Color.orange); // test
if (isMacAqua) {
//if (true) return new ColorUIResource(Color.red);
if (false && isMacAquaBrushedMetal) {
// FYI/BUG: Mac OS X 10.4+ Java 1.5: applying SystemColor.window is no longer
// working for some components (e.g., palette button menu's (a JPopupMenu))
return new ColorUIResource(SystemColor.window);
} else
return new ColorUIResource(SystemColor.control);
} else
//return new ColorUIResource(VueResources.getColor("toolbar.background"));
return new ColorUIResource(SystemColor.control);
}
public static void init()
{
if (!initUnderway) {
if (DEBUG.INIT) out("init: already run");
return;
}
Log.debug("init");
if (FORCE_WINDOWS_LAF || SKIP_CUSTOM_LAF || SKIP_OCEAN_THEME || SKIP_WIN_NATIVE_LAF)
System.out.println("GUI.init; test parameters:"
+ "\n\tforceWindowsLookAndFeel=" + FORCE_WINDOWS_LAF
+ "\n\tskip_custom_laf=" + SKIP_CUSTOM_LAF
+ "\n\tskip_ocean_theme=" + SKIP_OCEAN_THEME
+ "\n\tskip_win_native_laf=" + SKIP_WIN_NATIVE_LAF
);
if (SKIP_CUSTOM_LAF)
Log.info("INIT: skipping installation of custom VUE Look & Feels");
/* VUE's JIDE open-source license if we end up using this:
tufts.Util.executeIfFound("com.jidesoft.utils.Lm", "verifyLicense",
new Object[] { "Scott Fraize",
"VUE",
"p0HJOS:Y049mQb8BLRr9ntdkv9P6ihW" });
*/
isMacAqua = Util.isMacPlatform() && !FORCE_WINDOWS_LAF && !VUE.isApplet();
isMacAquaBrushedMetal =
isMacAqua && !VUE.isApplet() &&
VUE.isSystemPropertyTrue("apple.awt.brushMetalLook");
ToolbarColor = initToolbarColor();
// Note that it is essential that the theme be set before a single GUI object of
// any kind is created. If, for instance, a static member in any class
// initializes a swing gui object, this will end up having no effect here, and
// the entire theme will be silently ignored. This includes the call below to
// UIManager.setLookAndFeel, or even UIManager.getLookAndFeel, which is also why
// we need to tell the VueTheme about LAF depended variable sinstead of having
// it ask for the LAF itself, as it may not have been set yet. Note that when
// using the Mac Aqua L&F, we don't need to set the theme for Metal (as it's not
// being used and would have no effect), but we still need to initialize the
// theme, as it's still queried througout some of the older code.
if (false)
installUIDefaults();
if (VUE.isApplet() || Util.isUnixPlatform())
{
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e)
{
Log.error("Couldn't load jlooks look and feel");
}
}
else if (Util.isMacPlatform() && !VUE.isApplet()) {
if (!SKIP_CUSTOM_LAF) {
try {
// FileChooser:
Set includes = new HashSet();
includes.add("ColorChooser");
includes.add("FileChooser");
includes.add("Component");
includes.add("Browser");
includes.add("Tree");
includes.add("SplitPane");
// includes.add("Button");
// includes.add("CheckBox");
// includes.add("DesktopPane");
// includes.add("EditorPane");
// includes.add("FormattedTextField");
// includes.add("Label");
// includes.add("MenuBar");
// includes.add("OptionPane");
// includes.add("Panel");
// includes.add("RadioButton");
ch.randelshofer.quaqua.QuaquaManager.setIncludedUIs(includes);
//System.setProperty("Quaqua.design", "panther");
UIManager.setLookAndFeel("ch.randelshofer.quaqua.QuaquaLookAndFeel");
//UIManager.setLookAndFeel(ch.randelshofer.quaqua.QuaquaManager.getLookAndFeel());
//System.setProperty("Quaqua.design", "panther");
}
catch (Throwable t) {
Log.error("Couldn't load quaqua look and feel", t);
}
}
} else {
// We're leaving the default look and feel alone (e.g. Windows)
// if on Windows and forcing windows look, these meants try the native win L&F
//if (FORCE_WINDOWS_LAF && Util.isWindowsPlatform())
if (Util.isWindowsPlatform() && !SKIP_WIN_NATIVE_LAF)
setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
else if (Util.isMacPlatform() && VUE.isApplet())
{
/////Load up some JLooks stuff here.
}
else {
if (!SKIP_CUSTOM_LAF)
installMetalTheme();
}
}
UIManager.put("FileChooser.filesOfTypeLabelText","Format:");
if (!VUE.isApplet())
javax.swing.JPopupMenu.setDefaultLightWeightPopupEnabled(false);
if (Util.getJavaVersion() < 1.5f)
UseAlwaysOnTop = false;
String fontName;
int fontSize;
int fontSize2;
if (isMacAqua) {
fontName = "Lucida Grande";
fontSize = 11;
fontSize2 = fontSize + 2;
} else {
fontName = "SansSerif";
fontSize = 11;
fontSize2 = 11;
}
final String fixedFont = "Lucida Sans Typewriter";
//final String fixedFont = "Courier New";
//final String fixedFont = "Courier";
// Verdana is not fully a fixed font, but it's still much easier to read
// than any of the truly fixed fonts, and it's not nearly as scary
// for users to look at.
final String errorFont = "Verdana";
LabelFace = new GUI.Face(fontName, Font.PLAIN, fontSize, GUI.LabelColor);
ValueFace = new GUI.Face(fontName, Font.PLAIN, fontSize, Color.black);
TitleFace = new GUI.Face(fontName, Font.BOLD, fontSize, GUI.LabelColor);
FixedFace = new GUI.Face(fixedFont, Font.PLAIN, fontSize, GUI.LabelColor);
LabelFaceItalic = new GUI.Face(fontName, Font.ITALIC, fontSize, GUI.LabelColor);
//StatusFace = new GUI.Face(null, 0, 0, Color.darkGray, SystemColor.control);
//StatusFace = new GUI.Face(null, 0, 0, Color.darkGray);
StatusFace = new GUI.Face(fontName, Font.PLAIN, fontSize2, Color.darkGray);
StatusFaceSmall = new GUI.Face(fontName, Font.PLAIN, fontSize, Color.darkGray);
//StatusFace = new GUI.Face(fontName, Font.PLAIN, fontSize2, Color.darkGray, SystemColor.control);
ErrorFace = StatusFace;
//ErrorFace = new GUI.Face(errorFont, Font.PLAIN, fontSize+1, Color.darkGray, SystemColor.control);
DataFace = new GUI.Face("Verdana", Font.PLAIN, fontSize, null);
// Verdana & Trebuchet are designed for the screen sans-serif font
// Georgia a designed for the screen serif font
// Both are especially sensitive to handling bolding, small sizes, and character
// differentiation Studies have not found any actual difference in reading
// speed, though there may be reduced eye strain.
// And of course Arial (Microsoft's virtual Helevetica) is a bit more
// compressed, and reliable in at least that everyone's read it
// countless times.
ContentFace = new GUI.Face("Arial", Font.PLAIN, 14, null); // shows underscores below underline
//ContentFace = new GUI.Face("Verdana", Font.PLAIN, 13, null); // underline hides underscores
//Focus manager seems to be causing a lot of problems on windows 7 and 8 we've received numerous reports about windows not being
//clickable which I've tracked back to having Focus manager enabled going to try without it and see if there are any major
//issues arise on win8
if (!Util.isWindowsPlatform() && !Util.isUnixPlatform())
FocusManager.install();
//tufts.Util.executeIfFound("tufts.vue.gui.WindowManager", "install", null);
org.apache.log4j.Level level = org.apache.log4j.Level.DEBUG;
// if (!skipCustomLAF) level = org.apache.log4j.Level.DEBUG; // always show for the moment
Log.log(level, "LAF name: " + UIManager.getLookAndFeel().getName());
Log.log(level, "LAF descr: " + UIManager.getLookAndFeel().getDescription());
Log.log(level, "LAF class: " + UIManager.getLookAndFeel().getClass());
initUnderway = false;
}
private static void installUIDefaults()
{
// an experiment in overriding ScrollPane control keys
if (DEBUG.Enabled) UIManager.put("ScrollBar.width", new Integer(64)); // just to know we've taken effect
// We override scroll-pane input map to turn off arrow keys, so
// the can be used for VUE actions (e.g., Nudge)
UIManager.put("ScrollPane.ancestorInputMap",
new UIDefaults.LazyInputMap(new Object[] {
/*
"RIGHT", "unitScrollRight",
"KP_RIGHT", "unitScrollRight",
"DOWN", "unitScrollDown",
"KP_DOWN", "unitScrollDown",
"LEFT", "unitScrollLeft",
"KP_LEFT", "unitScrollLeft",
"UP", "unitScrollUp",
"KP_UP", "unitScrollUp",
*/
"PAGE_UP", "scrollUp",
"PAGE_DOWN", "scrollDown",
"ctrl PAGE_UP", "scrollLeft",
"ctrl PAGE_DOWN", "scrollRight",
"HOME", "scrollHome",
"END", "scrollEnd"
}));
}
private static void installMetalTheme() {
CommonMetalTheme common = new CommonMetalTheme();
if (Util.getJavaVersion() >= 1.5f && !SKIP_OCEAN_THEME) {
//Theme = new OceanMetalTheme(common);
String className = "tufts.vue.gui.OceanMetalTheme";
try {
Theme = (javax.swing.plaf.metal.MetalTheme)
Class.forName(className).newInstance();
Util.execute(Theme, className, "setCommonTheme", new Object[] { common });
isOceanTheme = true;
} catch (Exception e) {
Theme = null;
e.printStackTrace();
out("failed to load java 1.5+ Ocean theme: " + className);
}
}
if (Theme == null)
Theme = new DefaultMetalTheme(common);
MetalLookAndFeel.setCurrentTheme(Theme);
Log.debug("installed theme: " + Theme);
}
/** @return true if was successful */
private static boolean setLookAndFeel(String name) {
try {
if (name != null)
javax.swing.UIManager.setLookAndFeel(name);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/*
private static void installAquaLAFforVUE() {
try {
javax.swing.UIManager.setLookAndFeel(new VueAquaLookAndFeel());
} catch (javax.swing.UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
}
*/
private static class InitError extends Error {
InitError(String s) {
super("GUI.init hasn't run; indeterminate result for: " + s);
}
InitError() {
super("GUI.init hasn't run; indeterminate result");
}
}
public static boolean isGUIInited()
{
return !initUnderway;
}
public static boolean isMacAqua() {
if (initUnderway) throw new InitError("isMacAqua");
return isMacAqua;
}
public static boolean isMacBrushedMetal() {
if (initUnderway) throw new InitError("isMacAquaBrushedMetal");
return isMacAquaBrushedMetal;
}
public static boolean isOceanTheme() {
if (initUnderway) throw new InitError("isOceanTheme");
return isOceanTheme;
}
public static void reloadGraphicsInfo() {
loadGraphicsInfo();
}
public static boolean hasMultipleScreens()
{
if (GScreenDevices == null)
loadGraphicsInfo();
return GScreenDevices.length > 1;
}
public static Screen[] getAllScreens() {
return Screen.getAllScreens();
}
public static Rectangle getAllScreenBounds()
{
if (GSpaceBounds == null)
loadGraphicsInfo();
return new Rectangle(GSpaceBounds);
}
private static void loadGraphicsInfo()
{
Toolkit GToolkit = Toolkit.getDefaultToolkit();
GEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GDevice = getActiveDevice();
// Global raw offsets for the screen: in multi-monitor setup, this will be different
// for each display.
GScreen = Screen.create(GDevice);
GScreenDevices = GEnvironment.getScreenDevices();
GSpaceBounds = Screen.getAllDeviceBounds(GScreenDevices);
GSpace = Screen.boundsToInsets(GSpaceBounds);
// todo: the below should really be on some kind of observer of a GUI.java state:
if (tufts.vue.Actions.SuperScreen != null) {
// above could be null during init
tufts.vue.Actions.SuperScreen.update(GUI.class);
}
if (DEBUG.Enabled) dumpGraphicsConfig();
}
private static void dumpGraphicsConfig() {
Log.debug("screen stats:");
System.out.println(
//+ "\n\t toolkit: " + Util.tags(GToolkit)
"\t environment: " + Util.tags(GEnvironment)
+ "\n\tenviroMaxWin: " + GEnvironment.getMaximumWindowBounds()
+ "\n\tactiveScreen: " + GScreen
+ "\n\t allSpace: " + GSpace
+ "\n\t allBounds: " + GSpaceBounds
);
//dumpGraphicsDevice(GDevice, "VUE-ACTIVE");
for (int i = 0; i < GScreenDevices.length; i++) {
if (GScreenDevices[i] == GDevice)
dumpGraphicsDevice(GScreenDevices[i],"VUE-ACTIVE");
else
dumpGraphicsDevice(GScreenDevices[i], null);
}
}
public static void dumpGraphicsDevice(GraphicsDevice d, String key) {
//System.out.println("\tDevice " + key + ": " + d + " ID=" + Util.tags(d.getIDstring())
final GraphicsConfiguration config = d.getDefaultConfiguration();
System.out.println("\t" + Util.tags(d.getIDstring()) + " " + d + " " + (key==null?"":Util.tags(key))
+ "\n\t\t config: " + config
+ "\n\t\t config bounds: " + config.getBounds()
+ "\n\t\t screen insets: " + Toolkit.getDefaultToolkit().getScreenInsets(config)
);
}
/** Return the max window bounds for the given window, for screen device it's currently displayed on */
public static Rectangle getMaximumWindowBounds(Window w) {
return getScreenForWindow(w).getMaxWindowBounds();
}
public static Screen getScreenForWindow(Window w)
{
return Screen.getScreenForWindow(w);
}
public static Screen getScreenForPoint(final Point point) {
return Screen.getScreenForPoint(point);
}
public static GraphicsConfiguration getDeviceConfigForWindow(Window w) {
return Screen.getDeviceForWindow(w).getDefaultConfiguration();
}
/** @return the GraphicsDevice currently determined to be the "active" one for VUE */
public static GraphicsDevice getActiveDevice()
{
return Screen.getDeviceForWindow(VUE.getMainWindow());
}
public static void refreshGraphicsInfo() {
// GraphicsConfiguration changes on DisplayMode change -- update everything.
reloadGraphicsInfo();
// if (GDevice == null) {
// loadGraphicsInfo();
// } else {
// GraphicsConfiguration currentConfig = GDevice.getDefaultConfiguration();
// if (currentConfig != GConfig)
// loadGraphicsInfo();
// else
// GInsets = Toolkit.getDefaultToolkit().getScreenInsets(GConfig); // this may change at any time
// //GInsets = GToolkit.getScreenInsets(GConfig); // this may change at any time
// if (DEBUG.FOCUS) dumpGraphicsConfig();
// }
// //-----------------------------------------------------------------------------
// // Note: all the below code is basically deprecated/unused -- MainDock
// // will be null, as the DockRegion code is not being used.
// //-----------------------------------------------------------------------------
// // if (!VUE.isStartupUnderway() && VUE.getApplicationFrame() != null) {
// // // if (ControlMaxWindow)
// // // VUE.getApplicationFrame().setMaximizedBounds(VueMaxWindowBounds(GMaxWindowBounds));
// // if (DockWindow.MainDock != null) {
// // if (DockWindow.MainDock.mGravity == DockRegion.BOTTOM) {
// // DockWindow.MainDock.moveToY(VUE.getApplicationFrame().getY());
// // } else {
// // Point contentLoc = VUE.mViewerSplit.getLocation();
// // SwingUtilities.convertPointToScreen(contentLoc, VUE.getApplicationFrame().getContentPane());
// // DockWindow.MainDock.moveToY(contentLoc.y);
// // }
// // }
// // }
}
public static int stringLength(Font font, String s) {
return (int) Math.ceil(stringWidth(font, s));
}
public static double stringWidth(Font font, String s) {
if (font == null) {
String msg = "GUI.stringWidth: null font";
if (DEBUG.Enabled)
tufts.Util.printStackTrace(msg);
else
Log.error(msg);
}
return font.getStringBounds(s, GUI.DefaultFontContext).getWidth();
}
// /** VUE specific threshold for configuring UI */
// public static boolean isSmallScreen() {
// refreshGraphicsInfo();
// return GScreenWidth < 1024;
// }
/** center the given window on default physical screen, but never let it go above whatever
* our max window bounds are.
*/
public static void centerOnScreen(java.awt.Window window)
{
refreshGraphicsInfo();
//int x = GScreenWidth/2 - window.getWidth()/2;
//int y = GScreenHeight/2 - window.getHeight()/2;
int x = GScreen.top + GScreen.width/2 - window.getWidth()/2;
int y = GScreen.left + GScreen.height/2 - window.getHeight()/2;
Rectangle wb = getMaximumWindowBounds(window);
if (y < wb.y)
y = wb.y;
if (x < wb.x)
x = wb.x;
window.setLocation(x, y);
}
// private static Rectangle VueMaxWindowBounds(Rectangle systemMax) {
// Rectangle r = new Rectangle(systemMax);
// // force 0 at left, ignoring any left inset
// //r.width += r.x;
// //r.x = 0;
// // MacOSX won't let us override this when we set a frame's state to MAXIMIZED_BOTH,.
// // although the window can still be manually positioned there (user or setLocation)
// int min =
// DockWindow.isTopDockEmpty() ?
// 0 :
// DockWindow.getCollapsedHeight();
// //Util.printStackTrace("min="+min);
// min += DockWindow.ToolbarHeight;
// r.y += min;
// r.height -= min;
// /*
// //int dockHeight = DockWindow.getCollapsedHeight();
// if (!DockWindow.TopDock.isEmpty()) {
// r.y += dockHeight;
// r.height -= dockHeight;
// }
// */
// // At least on mac, it's not allowing us to reduce the max window
// // bounds at the bottom: any reduction in height is taken off the top.
// // In short: the y-value in the bounds is completely ignored.
// //if (!DockWindow.BottomDock.isEmpty())
// //r.height -= dockHeight;
// // ignore dock gap
// /*
// // apparently can't override this on at least mac
// if (r.x <= 4) {
// r.width += r.x;
// r.x = 0;
// }
// */
// // System.out.println(r);
// return r;
// }
public static Image getSystemIconForExtension(String ext) {
return getSystemIconForExtension(ext, 32);
}
// private static final Image UNKNOWN_TYPE;
// static {
// if (Util.isMacPlatform())
// UNKNOWN_TYPE = tufts.macosx.MacOSX.getIconForExtension("", 16);
// else
// UNKNOWN_TYPE = null;
// }
private static final Image NULL_IMAGE = NoImage32; // any image would do
private static class ImageIconCache {
final Map map = new java.util.concurrent.ConcurrentHashMap();
public void put(String key, Object image) {
if (DEBUG.IO||DEBUG.IMAGE) Log.debug(String.format("caching %10s: %s", key, Util.tags(image)));
if (image == null)
map.put(key, NULL_IMAGE);
else
map.put(key, image);
}
public void put(String ext, int size, Object image) {
put(ext + "." + size, image);
}
public Image get(String key) {
return (Image) map.get(key);
}
public Image get(String ext, int size)
{
final String key = ext + "." + size;
final Object entry = map.get(key);
Image image = null;
if (entry instanceof Image) {
image = (Image) entry;
}
else if (entry !=null)
{
try {
if (size == 16)
image = (Image) getIconMethod().invoke(entry, new Object[]{Boolean.FALSE});
else
image = (Image) getIconMethod().invoke(entry, new Object[]{Boolean.TRUE});
// now that we've unpacked the ShellFolder version,
// we can put the real image into the cache:
put(key, image);
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return image;
}
}
private static final ImageIconCache IconCache = new ImageIconCache();
private static final boolean LARGE_ICON = true;
private static final boolean SMALL_ICON = false;
private static final String TmpIconDir = VUE.getSystemProperty("java.io.tmpdir");
private static final Image RSSIcon = VueResources.getImage("dataSourceRSS");
private static Class cShellFolder;
private static Object sf;
static Object large;
static Object small;
public static Image getSystemIconForExtension(String ext, int sizeRequest)
{
if (DEBUG.IO && DEBUG.META) Log.debug("icon request: " + ext + "@" + sizeRequest);
if (ext == null)
return null;
ext = ext.toLowerCase();
if (ext.equals(Resource.EXTENSION_VUE)) {
final String key = "vueIcon" + sizeRequest;
Image image = IconCache.get(key);
if (image == null) {
image = tufts.vue.VueResources.getImage(key);
if (image == null)
image = tufts.vue.VueResources.getImage("vueIcon64");
IconCache.put(key, image);
}
return image;
} else if (ext.equals("rss"))
return RSSIcon;
if (ext == Resource.EXTENSION_HTTP)
ext = "htm";
if (Util.isMacPlatform() && !VUE.isApplet()) {
if (false && Util.isMacLeopard() && Util.getJavaVersion() > 1.5) {
; // ShellFolder/FileSystemView method still doesn't work
} else {
if (Util.isMacCocoaSupported())
return tufts.macosx.MacOSX.getIconForExtension(ext, sizeRequest);
else
return null;
}
// Image image = tufts.macosx.MacOSX.getIconForExtension(ext, sizeRequest);
// // May need an unknown type for each likely sizeRequest
// if ((image == null || image == UNKNOWN_TYPE) && ("readme".equals(ext) || "msg".equals(ext)))
// image = tufts.macosx.MacOSX.getIconForExtension("txt", sizeRequest);
// return image;
}
//-----------------------------------------------------------------------------
// Below happens for non-Mac platforms only: Windows, Linux, (etc?)
//-----------------------------------------------------------------------------
Image image = IconCache.get(ext, sizeRequest);
if (image != null) {
if (DEBUG.IO) Log.debug(String.format("cache hit %8s: %s", ext + "." + sizeRequest, Util.tags(image)));
if (image == NULL_IMAGE) {
return null;
} else {
return image;
}
}
// proceed the SLOW way, but all we can do until we have jdic/jdesktop and they actually handle this for us:
File file = null;
try {
if ("dir".equals(ext)) {
file = new File(TmpIconDir); // a guaranteed vanilla directory we should be able to find
if (DEBUG.IO) Log.debug(" trying " + file);
} else {
file = new File(TmpIconDir + File.separator + "vueIcon." + ext);
if (DEBUG.IO) Log.debug(" trying " + file);
if (file.createNewFile())
if (DEBUG.Enabled) Log.debug("created " + file);
// we deliberately leave the above files behind, so that hopefully
// future runs of VUE on this machine will be faster
}
// You MUST have found or created a real, actual file for ShellFolder
// to work.
if (cShellFolder == null)
cShellFolder = Class.forName("sun.awt.shell.ShellFolder");
if (cShellFolder !=null)
{
if (sizeRequest <= 24) {
//false
sf = getShellFolderMethod().invoke(null, new Object[]{file});
image = (Image) getIconMethod().invoke(sf, new Object[]{Boolean.FALSE});//shellFolder.getIcon(SMALL_ICON);
small = image;
large = sf; //shellFolder; // can be fetched later
} else {
//true
sf = getShellFolderMethod().invoke(null, new Object[]{file});
image = (Image) getIconMethod().invoke(sf, new Object[]{Boolean.TRUE});//shellFolder.getIcon(LARGE_ICON);
small = sf;//shellFolder; // can be fetched later
large = image;
}
}
else
{
//you're not on a Sun JVM and not on a mac at this point.
small = null;
large = null;
}
// We assume here that on Windows platforms, small is always 16x16, and large is always 32x32
// See IconCache code that also depends on this.
IconCache.put(ext, 16, small);
IconCache.put(ext, 32, large);
if (image != null) {
if (image.getHeight(null) != sizeRequest) {
if (DEBUG.IO || DEBUG.IMAGE)
Log.debug(Util.TERM_RED
+ "image size doesn't match request size for key [" + ext + "." + sizeRequest + "]: "
+ Util.tags(image)
+ Util.TERM_CLEAR);
if (DEBUG.IO) Log.debug("caching: " + ext + "@" + sizeRequest + ": " + Util.tags(image));
IconCache.put(ext, sizeRequest, image);
}
} else {
throw new NullPointerException("no Image");
}
} catch (Throwable t) {
//Log.warn("could not get Icon for filetype: " + ext + "." + sizeRequest + "; ", t);
Icon fsIcon = null;
try {
// debug: does this return us anything interesting? java 1.5 default impl delegates to ShellFolder,
// so result should presumably be the same
fsIcon = javax.swing.filechooser.FileSystemView.getFileSystemView().getSystemIcon(file);
} catch (Throwable _) {}
Log.warn("could not get Icon for filetype: " + ext + "." + sizeRequest + "; " + t + "; fsIcon=" + fsIcon);
IconCache.put(ext, sizeRequest, null);
}
return image;
}
private static Method iconMethod = null;
private static Method getShellFolder = null;
private static Method getIconMethod()
{
if (iconMethod == null)
{
try {
iconMethod = cShellFolder.getDeclaredMethod("getIcon", new Class[]{Boolean.TYPE});
} catch (SecurityException e) {
Log.info(e.toString() + "while trying to invoke ShellFolder, possibly non-sun JVM");
} catch (NoSuchMethodException e) {
Log.info(e.toString() + "while trying to invoke ShellFolder, possibly non-sun JVM"); } catch (IllegalArgumentException e) {
}
}
return iconMethod;
}
private static Method getShellFolderMethod()
{
if (getShellFolder == null)
{
try {
getShellFolder = cShellFolder.getDeclaredMethod("getShellFolder", new Class[]{File.class});
} catch (SecurityException e) {
Log.info(e.toString() + "while trying to invoke ShellFolder, possibly non-sun JVM");
} catch (NoSuchMethodException e) {
Log.info(e.toString() + "while trying to invoke ShellFolder, possibly non-sun JVM"); } catch (IllegalArgumentException e) {
}
}
return getShellFolder;
}
private static Image getIconFromReflection(String file)
{
if (cShellFolder == null){
try {
cShellFolder = Class.forName("sun.awt.shell.ShellFolder");
} catch (ClassNotFoundException e1) {
Log.info(e1.toString() + "while trying to load ShellFolder, possibly non-sun JVM");
}
}
if (cShellFolder !=null)
{
Image i = null;
try {
sf = getShellFolderMethod().invoke(null, new Object[]{file});
i = (Image) getIconMethod().invoke(sf, new Object[]{Boolean.FALSE});
} catch (IllegalArgumentException e) {
Log.info(e.toString() + "while trying to load ShellFolder, possibly non-sun JVM");
} catch (IllegalAccessException e) {
Log.info(e.toString() + "while trying to load ShellFolder, possibly non-sun JVM");
} catch (InvocationTargetException e) {
Log.info(e.toString() + "while trying to load ShellFolder, possibly non-sun JVM");
}
return i;
}
else
return null;
}
/** these may change at any time, so we must fetch them newly each time */
public static Insets getScreenInsets() {
refreshGraphicsInfo();
return GScreen.getAsInsets();
}
/**
* Factory method for creating frames in VUE. On PC, this
* is same as new new JFrame(). In Mac Look & Feel it adds a duplicate
* menu-bar to the frame as every frame needs one
* or we lose the mebu-bar (todo: no longer true)
*/
public static JFrame createFrame()
{
return createFrame(null);
}
public static JFrame createFrame(String title)
{
JFrame newFrame = new JFrame(title);
/*
if (isMacAqua()) {
JMenuBar menu = new VueMenuBar();
newFrame.setJMenuBar(menu);
}
*/
return newFrame;
}
/** @return a new VUE application DockWindow */
public static DockWindow createDockWindow(String title, boolean asToolbar,boolean showCloseButton) {
// In Java 1.5 we can set the Window's to be always on top. In this case, we
// can use a hidden parent for the DockWindow, set them to be never focusable, and
// use our FocusManager to force focus to it as needed. We also need our
// WindowManager in this case to hide the DockWindow's when the application goes
// inactive, otherwise they'll stay on top of all other applications.
// Doing this allows the main VUE frame to keep application focus even when
// using tool widges (the title bar stays active), will allow us to create an
// MDI (multiple document interface) because the DockWindow's aren't parented to
// just one window, and allows DockWindows to be used in full-screen mode for
// the same reason: they always stay on top, and don't need to be children of
// the currently active window to be seen. If we could re-parent java Window's
// on the fly we could avoiid all this, but alas, we can't. (Java Window's
// always stay on top of their parent, but not on top of other windows,
// including "grandparents".)
if ((Util.isUnixPlatform() && Util.getJavaVersion() >= 1.5f ) || VUE.isApplet() )
{
//There are a bunch of focus problems on Unix, hopefully this clears it up.
return new DockWindow(title,VUE.getRootWindow(),null,asToolbar,showCloseButton);
}
else if (UseAlwaysOnTop && Util.getJavaVersion() >= 1.5f) {
DockWindow dockWindow
= new DockWindow(title, DockWindow.getHiddenFrame(), null, asToolbar,showCloseButton);
dockWindow.setFocusableWindowState(false);
setAlwaysOnTop(dockWindow.window(), true);
return dockWindow;
} else {
// TODO: create method in VUE for getting DockWindow parent for use elsewhere
return new DockWindow(title, getFullScreenWindow(), null, asToolbar,showCloseButton);
//return new DockWindow(title, VUE.getRootWindow());
}
}
public static DockWindow createDockWindow(String title, boolean asToolbar) {
return createDockWindow(title,asToolbar,true);
}
public static DockWindow createDockWindow(String title) {
return createDockWindow(title, false);
}
/** the given panel must have it's title set via setName */
public static DockWindow createDockWindow(JComponent c) {
return createDockWindow(c.getName(), c);
}
/**
* Convience method.
* @return a new VUE application DockWindow
* @param content - a component to put in the dock window
*/
public static DockWindow createDockWindow(String title, javax.swing.JComponent content) {
DockWindow dw = createDockWindow(title);
dw.setContent(content);
return dw;
}
public static DockWindow createDockWindow(String title, String helpText, javax.swing.JComponent content) {
DockWindow dw = createDockWindow(title);
dw.setHelpText(helpText);
dw.setContent(content);
return dw;
}
public static DockWindow createDockWindow(String title, String helpText) {
DockWindow dw = createDockWindow(title);
dw.setHelpText(helpText);
return dw;
}
/**
* @return a new VUE application Dockable Toolbar
* @param content - a component to use for the roolbar
*/
public static DockWindow createToolbar(String title, javax.swing.JComponent content) {
DockWindow dw = createDockWindow(title, true);
dw.setContent(content);
return dw;
}
public static Frame HiddenDialogParent = null;
// based on SwingUtilities.getSharedOwnerFrame()
public static Frame getHiddenDialogParentFrame() {
if (HiddenDialogParent == null) {
HiddenDialogParent = new Frame() {
public void show() {} // This frame can never be shown
// this will prevent children from going behind other windows?
public boolean isShowing() { return true; }
public String toString() { return name(this); }
};
HiddenDialogParent.setName("*VUE-DIALOG-PARENT*");
}
return HiddenDialogParent;
}
public static Window getFullScreenWindow() {
if (FullScreenWindow == null) {
//FullScreenWindow = new Window(VUE.getRootFrame());
//FullScreenWindow = new JWindow(VUE.getRootFrame());
//FullScreenWindow = new DockWindow("***VUE-FULLSCREEN***", VUE.getRootFrame());
FullScreenWindow = new FullScreen.FSWindow();
}
return FullScreenWindow;
}
// public static Window getCurrentRootWindow() {
// if (FullScreen.inFullScreen())
// return FullScreenWindow;
// else
// return VUE.getRootWindow();
// }
/**
* Find the containing Window of the given Component, and make sure it's displayed
* on screen.
*/
public static void makeVisibleOnScreen(Component c)
{
java.awt.Window w = javax.swing.SwingUtilities.getWindowAncestor(c);
if (w != null && !w.isShowing()) {
if (DEBUG.WIDGET) {
out(GUI.name(c) + " showing containing window " + GUI.name(w));
if (DEBUG.META) tufts.Util.printStackTrace("showing containing window " + GUI.name(w));
}
DockWindow.flickerAnchorDock();
w.setVisible(true);
w.toFront();
}
}
/**
* Find the given class in the AWT hierarchy, and make sure it's containing Window
* is visible on the screen. If the found instance is a Widget, it is expanded.
* If it's parent is a JTabbedPane, it's tab is selected.
*
* @param clazz - a class that must be a subclass of Component (or nothing will be found)
*/
public static void makeVisibleOnScreen(Object requestor, Class clazz)
{
new EventRaiser<java.awt.Component>(requestor, clazz) {
public void dispatch(Component c) {
if (isWidget(c)) {
Widget.setExpanded((JComponent)c, true);
} else {
if (c.getParent() instanceof JTabbedPane)
((JTabbedPane)c.getParent()).setSelectedComponent(c);
makeVisibleOnScreen(c);
}
}
}.raise();
}
public static boolean isWidget(Component c) {
return c instanceof JComponent && Widget.isWidget((JComponent)c);
}
/*
private static Cursor oldRootCursor;
private static Cursor oldViewerCursor;
private static tufts.vue.MapViewer waitedViewer;
private static synchronized void activateWaitCursor(final Component component) {
if (oldRootCursor != null) {
if (DEBUG.FOCUS) out("multiple wait-cursors: already have " + oldRootCursor + "\n");
return;
}
if (VUE.getActiveViewer() != null) {
waitedViewer = VUE.getActiveViewer();
oldViewerCursor = waitedViewer.getCursor();
waitedViewer.setCursor(CURSOR_WAIT);
}
JRootPane root = SwingUtilities.getRootPane(component);
if (root != null) {
if (true||DEBUG.Enabled) out("ACTIVATING WAIT CURSOR: current = " + oldRootCursor
+ "\n\t on JRootPane " + root);
oldRootCursor = root.getCursor();
root.setCursor(CURSOR_WAIT);
}
}
private static void _clearWaitCursor(Component component) {
//out("restoring old cursor " + oldRootCursor + "\n");
if (oldRootCursor == null)
return;
if (waitedViewer != null) {
waitedViewer.setCursor(oldViewerCursor);
waitedViewer = null;
}
SwingUtilities.getRootPane(VUE.ApplicationFrame).setCursor(oldRootCursor);
oldRootCursor = null;
}
*/
private static Map CursorMap = new HashMap();
private static boolean WaitCursorActive = false;
private static Component ViewerWithWaitCursor;
public static synchronized void activateWaitCursor() {
//tufts.Util.printStackTrace("ACTIAVTE WAIT-CURSOR");
if (DEBUG.FOCUS) Log.info("ACTIVATE WAIT CURSOR");
activateWaitCursorInAllWindows();
}
private static synchronized void activateWaitCursorInAllWindows() {
synchronized (CursorMap) {
if (DEBUG.FOCUS) Log.info("ACTIVATE WAIT CURSOR IN ALL WINDOWS");
//if (!CursorMap.isEmpty()) {
if (WaitCursorActive) {
//VUE.Log.error("attempting to activate wait cursor while one is already active");
if (DEBUG.FOCUS) Log.info("FYI, activating wait cursor for all windows while one is already active");
//return;
}
// We must activate on the MapViewer first, as is a child of ApplicationFrame or
// FullScreenWindow, and setting the wait cursor on those first will cause the
// viewer "old" cursor to already think it's the wait cursor. We need to handle
// the view as it's own case as it may have a tool active that has a different
// cursor than the default currently active.
//activateWaitCursor(ViewerWithWaitCursor = VUE.getActiveViewer());
activateWaitCursor(VUE.getApplicationFrame());
activateWaitCursor(getFullScreenWindow()); // not always working?
for (DockWindow dw : DockWindow.AllWindows)
activateWaitCursor(dw.window());
WaitCursorActive = true;
}
}
private static void activateWaitCursor(final Component c) {
if (c == null)
return;
Cursor curCursor = c.getCursor();
if (curCursor != CURSOR_DEFAULT && curCursor != CURSOR_WAIT)
CursorMap.put(c, curCursor);
if (c instanceof JComponent)
{
RootPaneContainer root = (RootPaneContainer) ((JComponent)c).getTopLevelAncestor();
root.getGlassPane().setCursor(CURSOR_WAIT);
root.getGlassPane().setVisible(true);
}
else if (c instanceof JFrame)
{
((JFrame)c).getGlassPane().setCursor(CURSOR_WAIT);
((JFrame)c).getGlassPane().setVisible(true);
}
else
c.setCursor(CURSOR_WAIT);
if (DEBUG.FOCUS) Log.info("set wait cursor on " + name(c) + " (old=" + curCursor + ")");
}
public static synchronized void clearWaitCursor() {
//tufts.Util.printStackTrace("CLEAR WAIT-CURSOR");
if (DEBUG.FOCUS) Log.info("CLEAR WAIT CURSOR SCHEDULED");
VUE.invokeAfterAWT(new Runnable() { public void run() { clearAllWaitCursors(); }});
}
private static synchronized void clearAllWaitCursors() {
synchronized (CursorMap) {
if (DEBUG.FOCUS) Log.info("CLEAR ALL WAIT CURSORS");
if (!WaitCursorActive) {
if (DEBUG.FOCUS) Log.info("\t(wait cursors already cleared)");
return;
}
//clearWaitCursor(ViewerWithWaitCursor);
clearWaitCursor(VUE.getApplicationFrame());
clearWaitCursor(getFullScreenWindow());
for (DockWindow dw : DockWindow.AllWindows)
clearWaitCursor(dw.window());
CursorMap.clear();
WaitCursorActive = false;
}
}
private static void clearWaitCursor(Component c) {
if (c == null)
return;
Object oldCursor = CursorMap.get(c);
if (oldCursor == null || oldCursor == CURSOR_WAIT) {
if (oldCursor == CURSOR_WAIT)
Log.error("old cursor on " + name(c) + " was wait cursor! Restoring to default.");
if (DEBUG.FOCUS) Log.info("cleared wait cursor on " + name(c) + " to default");
if (c instanceof JComponent)
{
RootPaneContainer root = (RootPaneContainer) ((JComponent)c).getTopLevelAncestor();
root.getGlassPane().setCursor(CURSOR_DEFAULT);
root.getGlassPane().setVisible(false);
}
else if (c instanceof JFrame)
{
((JFrame)c).getGlassPane().setCursor(CURSOR_DEFAULT);
((JFrame)c).getGlassPane().setVisible(false);
}
c.setCursor(CURSOR_DEFAULT);
} else {
if (DEBUG.FOCUS) Log.info("cleared wait cursor on " + name(c) + " to old: " + oldCursor);
if (c instanceof JComponent)
{
RootPaneContainer root = (RootPaneContainer) ((JComponent)c).getTopLevelAncestor();
root.getGlassPane().setCursor((Cursor) oldCursor);
root.getGlassPane().setVisible(false);
}
else if (c instanceof JFrame)
{
((JFrame)c).getGlassPane().setCursor((Cursor) oldCursor);
((JFrame)c).getGlassPane().setVisible(false);
}
c.setCursor((Cursor) oldCursor);
}
}
private static Rectangle SpecialWorkingBounds = null; // e.g.,for VISWALL
public static void setSpecialWorkingBounds(Rectangle b) {
SpecialWorkingBounds = b;
}
private static Rectangle getFullScreenBounds()
{
if (tufts.vue.Actions.SuperScreen.getToggleState()) {
if (SpecialWorkingBounds != null && !SpecialWorkingBounds.isEmpty())
return SpecialWorkingBounds;
else
return GSpaceBounds;
} else
return getActiveDevice().getDefaultConfiguration().getBounds();
}
private static Rectangle getFullScreenWorkingBounds()
{
final Rectangle bounds = new Rectangle(getFullScreenBounds());
if (Util.isMacPlatform()) {
// place us under the mac menu bar
bounds.y += GScreen.margin.top;
bounds.height -= GScreen.margin.top;
// we explicitly ignore any left/bottom/right insets (for the dock)
}
if (DEBUG.PRESENT) {
// so we can see underlying diagnostic windows
bounds.x += bounds.width / 4;
//bounds.y += bounds.height / 16;
//bounds.y += 22;
bounds.width /= 2;
bounds.height /= 3;
}
//out("FSBounds: " + bounds);
return bounds;
}
/**
* In case window is off screen, size it, then set visible on screen, then place it.
* This avoids reshape flashing -- when a window is setVisible, it sometimes
* displays for a moment before it's taken it's new size and been re-validated.
*/
public static void setFullScreenVisible(Window window)
{
final Rectangle bounds = getFullScreenWorkingBounds();
window.setSize(bounds.width, bounds.height);
window.setVisible(true);
window.setLocation(bounds.x, bounds.y);
}
public static void setFullScreen(Window window) {
window.setBounds(getFullScreenWorkingBounds());
}
public static int getOffScreenY()
{
if (GSpace == null)
refreshGraphicsInfo();
return GSpace.bottom + 1;
}
public static int getOffScreenX()
{
if (GSpace == null)
refreshGraphicsInfo();
return GSpace.right + 1;
}
/** set window's location such that it will not be visible on any screen */
public static void setOffScreen(java.awt.Window window)
{
window.setLocation(0, getOffScreenY());
// note that we want this *minimally* off-screen, as
// apps like Mac OS X Expose will still find and display
// this window, and if it's far out, it degrades the
// usefulness of that feature.
}
public static void setAlwaysOnTop(Window w, boolean onTop) {
Log.debug("setAlwaysOnTop " + onTop + " " + name(w));
// this was needed for pre Java 1.5 support:
//Util.invoke(w, "setAlwaysOnTop", onTop ? Boolean.TRUE : Boolean.FALSE);
w.setAlwaysOnTop(onTop);
}
public static Icon getIcon(String name) {
return VueResources.getIcon(GUI.class, "icons/" + name);
}
/** @deprecated - use keepRegionOnScreen */
public static void keepLocationOnScreen(Point loc, Dimension size) {
keepRegionOnScreen(loc, loc, size);
}
public static void keepRegionOnScreen(Point onScreen, Point loc, Dimension size) {
keepRegionOnScreen(Screen.getScreenForPoint(onScreen), loc, size);
}
/**
* Given location and size (of presumably a Window) modify location
* such that the resulting bounds are on screen. If the window
* is bigger than the screen, it will keep the upper left corner
* visible.
*
* @param screen - the inset-bounds of the screen to keep the region on
* @param loc - the location of the region to keep entirely on the screen -- may be directly modified by this call
* @param size - the size of the region
*
* E.g., can be used to keep tool-tips on-screen.
*
* Note: a complete solution would need to pass in which screen we want to keep the
* point on (for multi-monitor setups) -- for now we just assume the current VUE
* "active" screen.
*
* Note: this does not refresh the current graphics display stats --
* it references currently cached values only.
*/
public static void keepRegionOnScreen(Screen screen, Point loc, Dimension size)
{
// if would go off bottom, move up
if (loc.y + size.height >= screen.bottom)
loc.y = screen.bottom - (size.height + 1);
// if would go off top, move back down
// [old: avoid top insets as windows don't normally go
// over the menu bar at the top.]
if (loc.y < screen.top)
loc.y = screen.top;
// if would go off right, move back left
if (loc.x + size.width > screen.right)
loc.x = screen.right - size.width;
// if would go off left, just put at left
if (loc.x < screen.left)
loc.x = screen.left;
}
/*
public interface KeyBindingRelayer {
public boolean processKeyBindingUp(KeyStroke ks, KeyEvent e, int condition, boolean pressed);
}
* We need this to hand off command keystrokes to the VUE menu bar. This isn't as
* important given the way do things in java 1.5, but we really need in 1.4 and on
* the PC -- otherwise when a DockWindow has the focus, you can't use all the global
* short-cut keys, such as the ability to hide or show a DockWindow with a shortcut!
*
* Allowing this in java 1.5 means we can access the VueMenuBar
* shortcuts even while editing a text field.
// todo: we could probably have our FocusManager handle this for us, which may be a bit cleaner
// todo: or, we could just get the input map from menu bar and invoke the action ourselves...
public static boolean processKeyBindingToMenuBar(KeyBindingRelayer relayer,
KeyStroke ks,
KeyEvent e,
int condition,
boolean pressed)
{
// We want to ignore vanilla typed text as a quick-reject culling mechanism.
// So only if any modifier bits are on (except SHIFT), do we attempt to
// send the KeyStroke to the JMenuBar to check against any accelerators there.
final int PROCESS_MASK =
InputEvent.CTRL_MASK |
InputEvent.META_MASK |
InputEvent.ALT_MASK |
InputEvent.ALT_GRAPH_MASK;
// On Mac java 1.4.2, and on the PC (TODO: also java 1.5 or only java 1.4.2?)
// we have to manually redirect the key events to the main VueMenuBar.
if ((e.getModifiers() & PROCESS_MASK) == 0 && !e.isActionKey())
return relayer.processKeyBindingUp(ks, e, condition, pressed);
if (DEBUG.TOOL||DEBUG.FOCUS) out(name(relayer) + " processKeyBinding [" + ks + "] condition="+condition);
// processKeyBinding never appears to return true, and this key event never
// gets marked as consumed (I guess we'd have to listen for that in our text fields
// and consume it), so we're always passing the character up the tree...
if (relayer.processKeyBindingUp(ks, e, condition, pressed) || e.isConsumed())
return true;
// Condition usually comes in as WHEN_ANCESTOR_OF_FOCUSED_COMPONENT (1), which doesn't do it for us.
// We need condition (2): WHEN_IN_FOCUSED_WINDOW
int newCondition = JComponent.WHEN_IN_FOCUSED_WINDOW;
if (DEBUG.TOOL||DEBUG.FOCUS)
out(name(relayer) + " processKeyBinding [" + ks + "] handing to VueMenuBar w/condition=" + newCondition);
try {
return false; //return VUE.getJMenuBar().doProcessKeyBinding(ks, e, newCondition, pressed);
} catch (NullPointerException ex) {
out("no menu bar: " + ex);
return false;
}
}
*/
public static void invokeAfterAWT(Runnable runnable) {
java.awt.EventQueue.invokeLater(runnable);
}
public static void invokeOnEDT(Runnable runnable) {
if (java.awt.EventQueue.isDispatchThread())
runnable.run();
else
java.awt.EventQueue.invokeLater(runnable);
}
public static void messageAfterAWT(final String s) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() { System.out.println(s); }
});
}
public static void postEvent(AWTEvent e) {
Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(e);
}
public static void postEventLater(final AWTEvent e) {
invokeAfterAWT(new Runnable() { public void run() { postEvent(e); }});
}
/** make sure there's a colon on the end of the string. E.g., some localized resource
* strings have the colon built in (inappropriately) -- this helps deal with that */
public static String ensureColon(String s) {
return s.endsWith(":") ? s : (s + ":");
}
public static void dumpSizes(Component c) {
dumpSizes(c, "");
}
public static void dumpSizes(Component c, String msg) {
//if (DEBUG.META) tufts.Util.printStackTrace("java 1.5 only");
final boolean prfSet, maxSet , minSet;
final String sp = " ";
final String setMsg = " SET ";
prfSet = c.isPreferredSizeSet();
maxSet = c.isMinimumSizeSet();
minSet = c.isMaximumSizeSet();
out(name(c) + " sizes; " + msg
+ "\n\t Size" + sp + name(c.getSize())
+ "\n\tprefSize" + (prfSet?setMsg:sp) + name(c.getPreferredSize())
+ "\n\t minSize" + (maxSet?setMsg:sp) + name(c.getMinimumSize())
+ "\n\t maxSize" + (minSet?setMsg:sp) + name(c.getMaximumSize())
);
// Pre-Java 1.5
// boolean prfSet = false, maxSet = false, minSet = false;
// String setMsg = " ??? ";
// try {
// prfSet = ((Boolean)Util.invoke(c, "isPreferredSizeSet")).booleanValue();
// maxSet = ((Boolean)Util.invoke(c, "isMinimumSizeSet")).booleanValue();
// minSet = ((Boolean)Util.invoke(c, "isMaximumSizeSet")).booleanValue();
// setMsg = " SET ";
// } catch (Throwable t) {}
// out(name(c) + " sizes; " + msg
// + "\n\t Size" + sp + name(c.getSize())
// + "\n\tprefSize" + (prfSet?setMsg:sp) + name(c.getPreferredSize())
// + "\n\t minSize" + (maxSet?setMsg:sp) + name(c.getMinimumSize())
// + "\n\t maxSize" + (minSet?setMsg:sp) + name(c.getMaximumSize())
// );
}
public static String name(Object c) {
return name(c, false);
}
public static String namex(Object c) {
return name(c, true);
}
/** @return a short name for given object, being smart about it if it's a java.awt.Component */
public static String name(Object c, boolean unique) {
if (c == null)
return "null";
if (c instanceof Boolean)
return "bool:" + ((Boolean)c).booleanValue();
if (c instanceof String)
return c.toString();
if (c instanceof AWTEvent)
return eventName((AWTEvent) c);
String title = null;
String name = null;
if (c instanceof java.awt.Frame) {
title = ((java.awt.Frame)c).getTitle();
if ("".equals(title))
title = null;
}
if (title == null && c instanceof Component) {
title = ((Component)c).getName();
if (OVERRIDE_REDIRECT.equals(title))
title = "###";
if (title == null) {
if (c instanceof javax.swing.JLabel)
title = ((javax.swing.JLabel)c).getText();
else if (c instanceof javax.swing.AbstractButton)
title = ((javax.swing.AbstractButton)c).getText();
}
} else if (c instanceof java.awt.MenuComponent) {
title = ((java.awt.MenuComponent)c).getName();
} else if (c instanceof java.awt.Dimension) {
Dimension d = (Dimension) c;
title = "w="+ d.width + " h=" + d.height;
} else if (c instanceof ComboKey)
title = ((ComboKey)c).key;
name = baseObjectName(c);
if (unique || title == null || title.startsWith("###"))
name += "@" + Integer.toHexString(c.hashCode());
if (title != null)
name += "(" + title + ")";
String text = null;
if (c instanceof javax.swing.text.JTextComponent) {
text = ((javax.swing.text.JTextComponent)c).getText();
int ni = text.indexOf('\n');
if (ni > 1 && !DEBUG.META)
text = text.substring(0,ni) + "...";
} else if (c instanceof ComboKey)
text = ((ComboKey)c).localized;
if (text != null)
name += "[" + text + "]";
return name;
}
public static String name(AWTEvent e) {
return eventName(e);
}
public static String eventName(AWTEvent e) {
return String.format("%-40s %-20s %s",
name(e.getSource()),
e.getClass().getSimpleName(),
eventParamString(e)
);
// return ""
// + VueUtil.pad(37, name(e.getSource()))
// //+ " " + VueUtil.pad(25, baseClassName(e.getClass().getName() + "@" + Integer.toHexString(e.hashCode())))
// + " " + VueUtil.pad(20, baseClassName(e.getClass().getName()))
// + eventParamString(e)
// ;
}
public static String eventParamString(AWTEvent e)
{
String s = "[" + e.paramString().replaceAll("='\r'", "<CR>").replaceAll("\r", "<CR>");
// \r carriage-return (ascii decimal=10) will cause output to overwrite itself
if (e instanceof InputEvent) {
if (e instanceof MouseEvent) {
if (((MouseEvent)e).isPopupTrigger())
s += ",POPUP-TRIGGER";
if (Util.isMacPlatform() && ((InputEvent)e).isControlDown())
s += ",MAC-POPUP";
}
s += "," + Integer.toBinaryString(((InputEvent)e).getModifiers());
s += "," + Integer.toBinaryString(((InputEvent)e).getModifiersEx());
}
return s + "]";
}
private static String baseClassName(Class clazz) {
return clazz.getSimpleName().length() > 1 ? clazz.getSimpleName() : clazz.getName();
//return baseClassName(clazz.getName());
}
private static String baseObjectName(Object o) {
return o == null ? "null" : baseClassName(o.getClass());
//return o == null ? "null" : baseClassName(o.getClass().getName());
}
// protected static String baseClassName(String className) {
// return className.substring(className.lastIndexOf('.') + 1);
// }
//-----------------------------------------------------------------------------
// Event testers
//-----------------------------------------------------------------------------
public static final int ALL_MODIFIER_KEYS_MASK =
java.awt.event.InputEvent.SHIFT_MASK
| java.awt.event.InputEvent.CTRL_MASK
| java.awt.event.InputEvent.META_MASK
| java.awt.event.InputEvent.ALT_MASK
;
private static boolean isPopupTrigger(MouseEvent e) {
if (Util.isMacPlatform()) {
// will detect mac pop-up trigger in any MouseEvent (not just MOUSE_PRESSED)
return e.isPopupTrigger() // does NOT detect in MOUSE_RELEASED
|| e.isControlDown(); // will detect mac pop-up trigger in any MouseEvent
} else {
return e.isPopupTrigger();
}
}
public static boolean isDoubleClick(MouseEvent e) {
// % 2 detects cascading double clicks (reported as a 4 click, 6 click, etc)
// clickCount > 0 prevents action with long mouse down (reports as 0 click)
// we also make sure that BUTTON1_MASK is set -- it's a left click
return e.getClickCount() > 1
&& e.getClickCount() % 2 == 0
&& (e.getModifiers() & InputEvent.BUTTON1_MASK) != 0
//&& (e.getModifiersEx() & InputEvent.BUTTON1_DOWN_MASK) != 0 // will not detect in MOUSE_RELEASED
&& !isPopupTrigger(e)
;
}
// public static boolean isUnmodifiedDoubleClick(MouseEvent e) {
// return isDoubleClick(e)
// && (e.getModifiers() & GUI.ALL_MODIFIER_KEYS_MASK) == 0
// }
// public static boolean isModifiedDoubleClick(MouseEvent e) {
// return isDoubleClick(e)
// && (e.getModifiers() & GUI.ALL_MODIFIER_KEYS_MASK) != 0
// }
public static boolean isSingleClick(MouseEvent e) {
return e.getClickCount() == 1
&& (e.getModifiers() & InputEvent.BUTTON1_MASK) != 0
//&& (e.getModifiersEx() & InputEvent.BUTTON1_DOWN_MASK) != 0 // will not detect in MOUSE_RELEASED
////&& (e.getModifiers() & GUI.ALL_MODIFIER_KEYS_MASK) == 0
;
}
/** @return true if 1 click, button 2 or 3 pressed, button 1 not already down & ctrl not down */
public static boolean isRightClick(MouseEvent e) {
return e.getClickCount() == 1
&& (e.getButton() == MouseEvent.BUTTON3 ||
e.getButton() == MouseEvent.BUTTON2)
&& (e.getModifiersEx() & InputEvent.BUTTON1_DOWN_MASK) == 0
&& !e.isControlDown() // is used for context-menu pop-up on Mac OS X
;
}
// /** single click count and isRightClick is true */
// public static boolean isSingleRightClick(MouseEvent e) {
// return e.getClickCount() == 1 && isRightClick(e);
// }
public static boolean anyModifierKeysDown(InputEvent e) {
return (e.getModifiers() & ALL_MODIFIER_KEYS_MASK) != 0;
}
public static boolean noModifierKeysDown(InputEvent e) {
return !anyModifierKeysDown(e);
}
public static boolean isMenuPopup(ComponentEvent ce) {
if (ce instanceof MouseEvent) {
MouseEvent e = (MouseEvent) ce;
return e.isPopupTrigger() || isRightClick(e);
} else
return false;
}
/**
* This is basically a dynamic enum type with one significant value associated with each enum key (the localized string)
* Arrays/collections of these are useful with JComboBox's.
*/
// would be nice to include a persistance string method and tune class for subclassing for type safety
public static class ComboKey {
// private static final Map<Class,Map> KeyMaps = new HashMap();
// private static final Map<Class,Map> ValueMaps = new HashMap();
private static int count = 1;
public final String key;
/** the VueResources.getString(keY) lookup value -- often GUI text that has been localized */
public final String localized;
/** this is a MASTER ordinal amongst ALL sub-types -- for convenience of a numeric runtime constant only */
public final int ordinal;
// /** stats for each subclass type */ protected class Stats {
// final Map<String,? extends ComboKey> KeyMap = new LinkedHashMap();
// final Map<String,? extends ComboKey> ValueMap = new HashMap();
// int count = 0; // for ordinals
// }
// Can't override a static to get data, to get All, or anything...
public ComboKey(String k, Map keyMap, Map valueMap) {
key = k;
final String s = VueResources.local(key);
localized = (s == null ? key : s);
if (keyMap != null && keyMap.put(key, this) != null)
throw new Error("duplicate key [" + key + "] for dynamic enum type " + Util.quote(getClass().getSimpleName()) + "; " + Util.tags(this));
if (valueMap != null)
valueMap.put(localized, this);
ordinal = count++; // could use keyMap.size()
// todo: force all keys lowercase for lookups
//KeyMaps.put(getClass(), keyMap); // will be called for every instance, but that's okay
}
public ComboKey(String k) { this(k, null, null); }
/** @return localized text -- not good for debugging, but good for a JComboBox */
public String toString() { return localized; }
public String toDebug() { return getClass().getSimpleName() + "<" + key + ">"; }
/** when implementing an All() in subclasses to return all declared instances, throw this error if key-map is empty */
/**/public static class InitError extends Error { public InitError(Class clazz) {
super("no keys yet for: " + clazz.getSimpleName() + "\n\treference instance(s) of " + clazz.getName() + " earlier in calling code to force classloading");
}}
// public static Object[] All(Class clazz) { // is static: must pass in sub-type
// if (data().KeyMap.isEmpty())
// throw new Error("fix your init-order with a declaration change: declared instances haven't been referenced yet");
// return data().KeyMap.values().toArray();
// }
// public ComboKey(String k, Class clazz) {
// }
// public static Map<String,? extends ComboKey> keyMap() {
// return KeyMaps.get(getClass());
// }
// Doesn't make lots of sense as non-static methods:
// public <T extends ComboKey> T decode(String input, Class<T> clazz) {
// return (T) KeyMaps.get(clazz).get(input);
// }
// public <T extends ComboKey> T decode(String input) {
// return (T) decode(input, getClass());
// }
}
// Perhaps a best example:
// /** result-operations, take II. Needs more ComboKey support for ROP.All() to work... */
// public static final class ROP extends ComboKey { private ROP(String s) { super(s /*,ROP.class*/); }
// public static final ROP[] All() { return (ROP[]) ComboKey.getAll(ROP.class); }
// public static final ROP
// SELECT = new ROP("searchgui.select"),
// SHOW = new ROP("searchgui.show"),
// HIDE = new ROP("searchgui.hide"),
// CLUSTER = new ROP("searchgui.cluster"),
// LINK = new ROP("searchgui.link"),
// COPY = new ROP("searchgui.copynewmap");
// // Declaring them in-class ensures a call to All will always report properly, even during init.
// // This is probably the closest we'll ever get to a DSL in Java... I see why Scala.
// }
//-----------------------------------------------------------------------------
private static class LocalData extends DataFlavor {
private Class clazz;
LocalData(Class clazz) throws ClassNotFoundException {
super(DataFlavor.javaJVMLocalObjectMimeType
+ "; class=" + clazz.getName()
+ "; humanPresentableName=" + baseClassName(clazz));
//super(DataFlavor.javaJVMLocalObjectMimeType, baseClassName(clazz));
this.clazz = clazz;
}
/*
* DataFlavor.equals make's all javaJVMLocalObjectMimeType's look the same. As
* we don't support cross VM transfer for these yet (serialization), we override
* equals to do an object compare (otherwise, Transferable's get confused, as
* they can't distinguish between these types).
*
* Okay -- don't need this if we construct above by building up a MimeType
* specification.
*/
/*
public boolean equals(DataFlavor that) {
return this == that;
}
*/
}
public static DataFlavor makeDataFlavor(String mimeType) {
DataFlavor flavor = null;
try {
flavor = new DataFlavor(mimeType);
} catch (Throwable t) {
Util.printStackTrace(t);
}
return flavor;
}
// probably move this & LocalData to something like LWDataTransfer with MapViewer.LWTransfer
public static DataFlavor makeDataFlavor(Class clazz) {
// We don't generally support serialization yet, so we need to make sure to
// tag non-supporting flavors as javaVM local only.
if (java.io.Serializable.class.isAssignableFrom(clazz)) {
return new DataFlavor(clazz, baseClassName(clazz));
} else {
try {
return new LocalData(clazz);
} catch (ClassNotFoundException e) {
// Should never happen, as we already have a live Class object
Util.printStackTrace(e);
}
}
return null;
}
private static final AlphaComposite DisabledIconAlpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f);
public static final class ProxyEnabledIcon implements javax.swing.Icon
{
private final javax.swing.Icon icon;
private final java.awt.Component trackingEnabled;
public ProxyEnabledIcon(javax.swing.Icon icon, java.awt.Component c) {
this.icon = icon;
this.trackingEnabled = c;
}
public void paintIcon(java.awt.Component c, java.awt.Graphics g, int x, int y) {
if (!trackingEnabled.isEnabled()) {
if (DEBUG.TOOL) {
System.out.println("DISABLED " + trackingEnabled.getClass().getName()
+ " disables " + icon
+ "; painting on " + c.getClass().getName());
}
if (DEBUG.BOXES) {
g.setColor(Color.red);
g.drawLine(x,y,x+100,y+100);
}
((Graphics2D)g).setComposite(DisabledIconAlpha);
}
icon.paintIcon(c, g, x, y);
}
public int getIconWidth() { return icon.getIconWidth(); }
public int getIconHeight() { return icon.getIconHeight(); }
}
private static class IconImpl implements Icon {
final int width, height;
public IconImpl(int w, int h) {
width = w;
height = h;
}
public void paintIcon(java.awt.Component c, java.awt.Graphics g, int x, int y) {
if (DEBUG.BOXES) {
g.setColor(Color.red);
g.fillRect(x, y, width, height);
}
// if (DEBUG.BOXES) {
// g.setColor(Color.gray);
// g.drawRect(x, y, width-1, height-1);
// }
}
public int getIconWidth() { return width; }
public int getIconHeight() { return height; }
public String toString() { return getClass().getSimpleName() + " " + width + "x" + height; }
}
public static final class EmptyIcon extends IconImpl {
public EmptyIcon(int w, int h) {
super(w, h);
}
public EmptyIcon(Icon icon) {
this(icon.getIconWidth(), icon.getIconHeight());
}
}
public static final class ResizedIcon extends IconImpl {
final Icon icon;
public ResizedIcon(Icon icon, int w, int h) {
super(w, h);
this.icon = icon;
}
public Icon getOriginal() {
return icon;
}
@Override
public void paintIcon(java.awt.Component c, java.awt.Graphics g, int x, int y) {
// attempt to center into the desired icon size region
final int offx = (width - icon.getIconWidth()) / 2;
final int offy = (height - icon.getIconHeight()) / 2;
icon.paintIcon(c, g, x + offx, y + offy);
}
}
private static class UnicodeIcon extends IconImpl {
final TextRow glyph;
final Color color;
final int xoff, yoff;
UnicodeIcon(int code, int pointSize, Color c, int width, int height, int x, int y) {
super(width, height);
color = c;
xoff = x;
yoff = y;
//glyph = new TextRow(new String(new char[ (char) code ]),
glyph = new TextRow("" + ((char)code),
getUnicodeGlyphFont(pointSize));
}
public void paintIcon(java.awt.Component c, java.awt.Graphics g, int x, int y) {
super.paintIcon(c, g, x, y);
g.setColor(color);
//Log.debug("drawing " + glyph + " at " + x + "," + y + "; +" + xoff + ",+" + yoff);
glyph.draw(g, x + xoff, y + yoff);
}
}
public static Icon makeUnicodeIcon(int code, int pointSize, Color c, int width, int height, int xoff, int yoff) {
return new UnicodeIcon(code, pointSize, c, width, height, xoff, yoff);
}
public static Icon reframeIcon(Icon i, int w, int h) {
return new ResizedIcon(i, w, h);
}
public static final Icon EmptyIcon16 = new EmptyIcon(16,16);
public static final Icon NO_ICON = new EmptyIcon(0,0);
public static final Insets EmptyInsets = new Insets(0,0,0,0);
private static boolean anyIcons(Component[] items) {
for (int i = 0; i < items.length; i++) {
if (items[i] instanceof AbstractButton) {
if (((AbstractButton)items[i]).getIcon() != null)
return true;
}
}
return false;
}
private static void enforceIcons(Component[] items) {
for (int i = 0; i < items.length; i++) {
if (items[i] instanceof AbstractButton) {
AbstractButton menuItem = (AbstractButton) items[i];
if (menuItem.getIcon() == null)
menuItem.setIcon(EmptyIcon16);
}
}
}
public static void adjustMenuIcons(javax.swing.JMenu menu) {
//out("ADJUSTING " + name(menu));
if (anyIcons(menu.getMenuComponents()))
enforceIcons(menu.getMenuComponents());
}
public static void adjustMenuIcons(javax.swing.JPopupMenu menu) {
//out("ADJUSTING " + name(menu));
if (anyIcons(menu.getComponents()))
enforceIcons(menu.getComponents());
}
public static JMenu buildMenu(JMenu menu, Action[] actions) {
addToMenu(menu, actions);
return menu;
}
public static void addToMenu(JPopupMenu menu, Action[] actions) {
for (int i = 0; i < actions.length; i++) {
Action a = actions[i];
if (a == null)
menu.addSeparator();
else
menu.add(a);
}
adjustMenuIcons(menu);
}
public static void addToMenu(JMenu menu, Action[] actions) {
for (int i = 0; i < actions.length; i++) {
Action a = actions[i];
if (a == null)
menu.addSeparator();
else
menu.add(a);
}
adjustMenuIcons(menu);
}
public static JMenu buildMenu(String name, Action[] actions) {
return buildMenu(new JMenu(name), actions);
}
public static JPopupMenu buildMenu(Action[] actions) {
JPopupMenu menu = new JPopupMenu();
menu.setBorder(BorderFactory.createEmptyBorder(5,0,5,0));
for (int i = 0; i < actions.length; i++) {
Action a = actions[i];
if (a == null)
menu.addSeparator();
else
menu.add(a);
}
adjustMenuIcons(menu);
return menu;
}
/**
* Given a trigger component (such as a label), when mouse is
* pressed on it, pop the given menu. Default location is below
* the given trigger.
*/
public static class PopupMenuHandler extends tufts.vue.MouseAdapter
implements javax.swing.event.PopupMenuListener
{
private long mLastHidden;
private JPopupMenu mMenu;
public PopupMenuHandler(Component trigger, JPopupMenu menu)
{
trigger.addMouseListener(this);
menu.addPopupMenuListener(this);
mMenu = menu;
}
public void mousePressed(MouseEvent e) {
long now = System.currentTimeMillis();
if (now - mLastHidden > 100)
showMenu(e.getComponent());
}
/** show the menu relative to the given trigger that activated it */
public void showMenu(Component trigger) {
mMenu.show(trigger, getMenuX(trigger), getMenuY(trigger));
}
/** get menu X location relative to trigger: default is 0 */
public int getMenuX(Component trigger) { return 0; }
/** get menu Y location relative to trigger: default is trigger height (places below trigger) */
public int getMenuY(Component trigger) { return trigger.getHeight(); }
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
mLastHidden = System.currentTimeMillis();
//out("HIDING");
}
public void popupMenuWillBecomeVisible(PopupMenuEvent e) { /*out("SHOWING");*/ }
public void popupMenuCanceled(PopupMenuEvent e) { /*out("CANCELING");*/ }
// One gross thing about a pop-up menu is that there's no way to know that it
// was just hidden by a click on the component that popped it. That is, if you
// click on the menu launcher once, you want to pop it, and if you click again,
// you want to hide it. But the AWT system autmatically cancels the pop-up as
// soon as the mouse-press happens ANYWYERE, and even before we'd get a
// processMouseEvent, so by the time we get this MOUSE_PRESSED, the menu is
// already hidden, and it looks like we should show it again! So we have to use
// a simple timer.
}
public static Font getUnicodeGlyphFont(int pointSize) {
if (Util.isWindowsPlatform())
return new Font("Lucida Sans Unicode", Font.PLAIN, pointSize+4); // this is included in default WinXP installations
// setFont(new Font("Arial Unicode MS", Font.PLAIN, pointSize+4)); // this is not
else if (Util.isMacPlatform())
return new Font("Symbol Regular", Font.PLAIN, pointSize);
else
return new Font("Lucida Grande", Font.PLAIN, pointSize);
}
/**
* A class for a providing a label of fixed size to display the given unicode character.
* A font is selected per-platform for ensuring good results.
* Todo: can we get a font that will handle this on Linux? ("Lucida Grande" is the default).
* The fixed sizes is useful in the case where you want a changeable icon, such as a
* right-arrow / down-arrow for an open/close icon, but the glyphs are actually different
* sizes. This allows you to fix to the size to something that accomodates both (currently
* you must tune and select that size manually, and use two IconicLabels with the same size).
*/
public static class IconicLabel extends JLabel implements Icon {
private final Dimension mSize;
private char icon;
public IconicLabel(char iconChar, int pointSize, Color color, int width, int height) {
super(new String(new char[] { iconChar }), JLabel.CENTER);
icon = iconChar;
setFont(getUnicodeGlyphFont(pointSize));
// todo: this may be a problem for Linux: does it have any decent default fonts that
// include the fancy extended unicode character sets?
//setFont(new Font("Arial Unicode MS", Font.PLAIN, (int) (((float)pointSize) * 1.3)));
// todo: the Mac v.s. PC fonts are too different to get
// this perfect; will need need per platform glyph
// selection (the unicode char). Or: could do something
// crazy like get the bounding box of the character in the
// available font, and keep scaling the point size until
// the bounding box matches what we're looking for...
mSize = new Dimension(width, height);
setPreferredSize(mSize);
setMaximumSize(mSize);
setMinimumSize(mSize);
setAlignmentX(0.5f);
if (color != null)
setForeground(color);
}
public IconicLabel(char iconChar, int width, int height) {
this(iconChar, 16, null, width, height);
}
public int getIconWidth() { return mSize.width; }
public int getIconHeight() { return mSize.height; }
/*
public void addNotify() {
super.addNotify();
if (DEBUG.BOXES) System.out.println(name(this) + " prefSize=" + super.getPreferredSize());
}
*/
@Override
public void paintComponent(Graphics g) {
// for debug: fill box with color so can see size
if (DEBUG.BOXES) {
g.setColor(Color.red);
g.fillRect(0, 0, mSize.width, mSize.height);
}
if (!Util.isMacPlatform())
((java.awt.Graphics2D)g).setRenderingHint
(java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
/**
* On Linux, I couldn't find a single font in font.propeties that had the glyph
* for the arrows so am drawing them instead, and just not using the characters at all.
*/
if ((!Util.isMacPlatform() && (!Util.isWindowsPlatform())))
{
Graphics2D g2d = (Graphics2D)g;
g2d.setColor(this.getForeground());
g2d.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING,java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
Shape shape = makeTriangle();
if (this.getText() == "\u25B8")
g2d.rotate(Math.PI+Math.PI/2,this.getWidth()/2.0,this.getHeight()/2.0);
g2d.fill(shape);
}
else
{
super.paintComponent(g);
}
}
public void paintIcon(Component c, Graphics g, int x, int y) {
if (DEBUG.BOXES) {
g.setColor(Color.red);
g.fillRect(0, 0, mSize.width, mSize.height);
}
//g.drawString(
}
private static Shape makeTriangle() {
GeneralPath p = new GeneralPath();
p.moveTo( 5f, 5f );
p.lineTo( 11f, 5f );
p.lineTo( 7.5f, 11f );
p.closePath();
return p;
}
@Override
public void setBounds(int x, int y, int width, int height) {
super.setBounds(x, y, mSize.width, mSize.height);
}
@Override public Dimension getSize() { return mSize; }
@Override public Dimension getPreferredSize() { return mSize; }
@Override public Dimension getMaximumSize() { return mSize; }
@Override public Dimension getMinimumSize() { return mSize; }
@Override public Rectangle getBounds() { return new Rectangle(getX(), getY(), mSize.width, mSize.height); }
}
// unfinished
public static class PopupMenuButton extends JLabel {
private JPopupMenu popMenu;
private long lastHidden;
private boolean doRollover = false;
//static final char chevron = 0xBB; // unicode "right-pointing double angle quotation mark"
//final Color inactiveColor = isMacAqua ? Color.darkGray : Color.lightGray;
PopupMenuButton(Icon icon, Action[] actions) {
super(icon);
//setForeground(inactiveColor);
//setName(DockWindow.this.getName());
setFont(new Font("Arial", Font.PLAIN, 18));
//Insets borderInsets = new Insets(0,0,1,2); // chevron
Insets borderInsets = new Insets(1,0,0,2);
if (DEBUG.BOXES) {
setBorder(new MatteBorder(borderInsets, Color.orange));
setBackground(Color.red);
setOpaque(true);
} else {
// using an empty border allows mouse over the border gap
// to also activate us for rollover
setBorder(new EmptyBorder(borderInsets));
}
setMenuActions(actions);
}
/*
PopupMenuButton(char unicodeCharIcon, Action[] actions) {
this(new GUI.IconicLabel(unicodeCharIcon, 16, 16), actions);
doRollover = true;
}
*/
public void setMenuActions(Action[] actions)
{
clearMenuActions();
new GUI.PopupMenuHandler(this, GUI.buildMenu(actions)) {
public void mouseEntered(MouseEvent e) {
if (doRollover)
setForeground(isMacAqua ? Color.black : Color.white);
}
public void mouseExited(MouseEvent e) {
if (doRollover)
setForeground(isMacAqua ? Color.gray : SystemColor.activeCaption.brighter());
//setForeground(inactiveColor);
}
public int getMenuX(Component c) { return c.getWidth(); }
public int getMenuY(Component c) { return -getY(); } // 0 in parent
};
repaint();
}
private void clearMenuActions() {
MouseListener[] ml = getMouseListeners();
for (int i = 0; i < ml.length; i++) {
if (ml[i] instanceof GUI.PopupMenuHandler)
removeMouseListener(ml[i]);
}
}
}
// add a color to a Font
public static class Face extends Font {
public final Color color;
public final Color bgColor;
public Face(Font f, Color c) {
super(f.getName(), f.getStyle(), f.getSize());
color = c;
bgColor = null;
}
public Face(Font f) {
this(f, Color.black);
}
public Face(String name, int size, Color c) {
super(name, Font.PLAIN, size);
color = c;
bgColor = null;
}
public Face(String name, int style, int size, Color c) {
super(name, style, size);
color = c;
bgColor = null;
}
public Face(String name, int style, int size, Color c, Color bg) {
super(name, style, size);
color = c;
bgColor = bg;
}
}
public static Border makeSpace(int inset) {
return makeSpace(inset, inset, inset, inset);
}
public static Border makeSpace(Insets i) {
return makeSpace(i.top, i.left, i.bottom, i.right);
}
public static Border makeSpace(int t, int l, int b, int r) {
if (DEBUG.BOXES)
return new MatteBorder(t,l,b,r, Color.orange);
else
return new EmptyBorder(t,l,b,r);
}
public static void installBorder(javax.swing.text.JTextComponent c) {
if (isMacAqua) {
if (c.isEditable())
c.setBorder(getAquaTextBorder());
}
}
private static Border AquaTextBorder = null;
private static Border getAquaTextBorder() {
if (AquaTextBorder != null)
return AquaTextBorder;
try {
Class abc = null;
try {
abc = Class.forName("apple.laf.AquaTextFieldBorder");
} catch (ClassNotFoundException e) {
// java 6 update as of June 2009 (1.6.0_13) will fail on above, but should find this:
abc = Class.forName("com.apple.laf.AquaTextFieldBorder");
}
AquaTextBorder = (javax.swing.border.Border) abc.newInstance();
if (DEBUG.Enabled) Log.debug("found Mac Aqua text border: " + abc);
} catch (Throwable t) {
Log.error("Mac Aqua GUI init problem:", t);
AquaTextBorder = new LineBorder(Color.blue); // backup debug
}
return AquaTextBorder;
}
/**
* Potentially set the foreground/background color & font on the given
* Component, if there are any resource entries for key ending in .font,
* .foreground or .background
*/
public static void init(Component c, String key) {
Color fg = VueResources.getColor(key+".foreground");
Color bg = VueResources.getColor(key+".background");
Font font = VueResources.getFont(key+".font");
if (fg != null)
c.setForeground(fg);
if (bg != null)
c.setBackground(bg);
if (font != null)
c.setFont(font);
}
public static void apply(Font f, JComponent c) {
if (f instanceof Face)
apply((Face)f, c);
else if (f.getSize() > 0)
c.setFont(f);
}
public static void apply(Face face, JComponent c) {
if (face.getSize() > 0)
c.setFont(face);
if (face.color != null)
c.setForeground(face.color);
if (face.bgColor != null) {
c.setBackground(face.bgColor);
c.setOpaque(true);
} else {
c.setOpaque(false);
}
}
public static void paintNow(JComponent c) {
c.paintImmediately(0, 0, c.getWidth(), c.getHeight());
}
public static void flashBackground(JComponent c, Color color) {
final Color old = c.getBackground();
c.setBackground(color);
paintNow(c);
c.setBackground(old);
}
public static void setDocumentFont(javax.swing.JTextPane tp, Font f)
{
SimpleAttributeSet a = new SimpleAttributeSet();
setFontAttributes(a, f);
StyledDocument doc = tp.getStyledDocument();
doc.setParagraphAttributes(0, doc.getEndPosition().getOffset(), a, false);
}
private static void setFontAttributes(javax.swing.text.MutableAttributeSet a, Font f)
{
StyleConstants.setFontFamily(a, f.getFamily());
StyleConstants.setFontSize(a, f.getSize());
StyleConstants.setItalic(a, f.isItalic());
StyleConstants.setBold(a, f.isBold());
}
// private void setDocumentColor(Color c)
// {
// StyleConstants.setForeground(mAttributeSet, c);
// javax.swing.text.StyledDocument doc = getStyledDocument();
// doc.setParagraphAttributes(0, doc.getEndPosition().getOffset(), mAttributeSet, true);
// }
public static void setRootPaneNames(RootPaneContainer r, String name) {
r.getRootPane().setName(name + ".root");
r.getContentPane().setName(name + ".content");
r.getLayeredPane().setName(name + ".layer");
r.getGlassPane().setName(name + ".glass");
}
public static String dropName(int dropAction) {
String name = "";
if ((dropAction & DnDConstants.ACTION_COPY) != 0) name += "COPY";
if ((dropAction & DnDConstants.ACTION_MOVE) != 0) {
if (name.length() > 0)
name += "+";
name += "MOVE";
}
if ((dropAction & DnDConstants.ACTION_LINK) != 0) {
if (name.length() > 0)
name += "+";
name += "LINK";
}
if (name.length() < 1)
name = "NONE";
//name += "(0x" + Integer.toHexString(dropAction) + ")";
return name;
}
public static String dragName(DragSourceDragEvent e) {
return VueUtil.pad(20, baseObjectName(e))
+ "; dropAction=" + dropName(e.getDropAction())
+ "; userAction=" + dropName(e.getUserAction())
+ "; targetActions=" + dropName(e.getTargetActions());
}
public static String dragName(DropTargetDragEvent e) {
return VueUtil.pad(20, baseObjectName(e))
+ "; dropAction=" + dropName(e.getDropAction())
+ "; sourceActions=" + dropName(e.getSourceActions())
;
}
public static String dropName(DropTargetDropEvent e) {
return baseObjectName(e)
+ "; dropAction=" + dropName(e.getDropAction())
+ "; sourceActions=" + dropName(e.getSourceActions())
+ "; isLocal=" + e.isLocalTransfer()
;
}
public static String dragName(DragSourceDropEvent e) {
return VueUtil.pad(20, baseObjectName(e)) + "; drop=" + dropName(e.getDropAction()) + "; success=" + e.getDropSuccess();
}
public static String dragSource(DragSourceEvent e) {
return "source=" + name(e.getDragSourceContext().getComponent());
}
// todo: move to DELAYED-init block for GUI called at end of main during caching runs
private static final int DragSize = 128;
private static BufferedImage DragImage = new BufferedImage(DragSize, DragSize, BufferedImage.TYPE_INT_ARGB);
private static Graphics2D DragGraphicsBuf = (Graphics2D) DragImage.getGraphics();
private static AlphaComposite DragAlpha = AlphaComposite.getInstance(AlphaComposite.SRC, .667f);
public static final Color TransparentColor = new Color(0,0,0,0);
public static boolean dragImagesSupported()
{
// The windows platform, at least as of XP, doesn't support dragging an image,
// and doing image creation noticably slows down the start of the drag, so
// we skip it. (Don't know about Linux)
// 2008-04-14: Windows Vista appears capable of drag images (at least Safari on Vista does it),
// tho don't know if java 6 is wired up to use it yet (first test was not successful).
return Util.isMacPlatform();
}
// public static void startLWCDrag(Component source,
// MouseEvent mouseEvent,
// tufts.vue.LWComponent c)
// {
// // TODO: move LWTransfer (or most of it) out of MapViewer to GUI
// //startLWCDrag(source, mouseEvent, c, new LWTransfer(c));
// throw new UnsupportedOperationException();
// }
public static void startRecognizedDrag(DragGestureEvent e, tufts.vue.LWComponent c)
{
// e.startDrag(DragSource.DefaultCopyDrop,
// tufts.vue.MapViewer.getTransferableHelper(c));
e.startDrag(DragSource.DefaultCopyDrop,
c.getAsImage(0.5, new Dimension(256,256)), // as of Java 6, Mac default is to scale to max of 128 in any dimension
// tho above can be changed via apple.awt.dnd.defaultDragImageSize System Property
//new Point(-(int)c.getWidth()/2, -(int)c.getHeight()/2), // image offset
new Point(0, 0),
new tufts.vue.LWTransfer(c),
new GUI.DragSourceAdapter());
//null); // drag source listener
}
public static void startLWCDrag(Component source,
MouseEvent mouseEvent,
tufts.vue.LWComponent c)
{
startLWCDrag(source, mouseEvent, c, new tufts.vue.LWTransfer(c));
}
public static void startLWCDrag(Component source,
MouseEvent mouseEvent,
tufts.vue.LWComponent c,
Transferable transfer)
{
if (true) {
Image dragImage = null;
if (dragImagesSupported()) {
Dimension maxSize = new Dimension(256,256); // bigger for LW drags
// This can be slow on Window's, and we can't see it anyway
dragImage = c.getAsImage(0.5, maxSize);
}
startDrag(source,
mouseEvent,
dragImage,
null,
transfer);
} else {
// todo: can optimize with a single buffer for all LW
// drags, but to fully opt, need to do in conjunction with
// LWTransfer, which because of Sun's DND code, always
// generates it's image.
DragGraphicsBuf.setColor(TransparentColor);
DragGraphicsBuf.fillRect(0,0, DragSize,DragSize);
c.drawImage(DragGraphicsBuf, 0.667, new Dimension(DragSize,DragSize), (Color) null, 1.0);
startSystemDrag(source, mouseEvent, DragImage, transfer);
}
}
/**
* This allows any Component to initiate a system drag without having to be a drag gesture recognizer -- it
* can simply be initiated from a MouseMotionListener.mouseDragged
*
* @param image, if non-null, is scaled to fit a 128x128 size and rendered with a transparency for dragging
*/
public static void startSystemDrag(Component source, MouseEvent mouseEvent, Image image, Transferable transfer)
{
if (DEBUG.DND) out("startSystemDrag: " + transfer);
Point imageOffset = null;
if (!dragImagesSupported())
image = null;
if (image != null) {
int w = image.getWidth(null);
int h = image.getHeight(null);
int max = 0;
// Allow zoom of up to 2x in case of a very small icon
final int maxZoom = (w <= 16 ? 2 : 1);
if (w > h) {
if (DragSize > h * maxZoom)
max = h * maxZoom;
} else {
if (DragSize > w * maxZoom)
max = w * maxZoom;
}
if (max == 0)
max = DragSize;
final int nw, nh;
if (w > h) {
nw = max;
nh = h * max / w;
} else {
nh = max;
nw = w * max / h;
}
// todo opt: could just fill the rect below or to right of what we're about to draw
// FYI, this is a zillion times faster than use Image.getScaledInstance
DragGraphicsBuf.setClip(null);
DragGraphicsBuf.setColor(TransparentColor);
DragGraphicsBuf.fillRect(0,0, DragSize,DragSize);
DragGraphicsBuf.setComposite(DragAlpha);
DragGraphicsBuf.drawImage(image, 0, 0, nw, nh, null);
image = DragImage;
w = nw;
h = nh;
imageOffset = new Point(w / -2, h / -2);
}
startDrag(source, mouseEvent, image, imageOffset, transfer);
}
public static void startRecognizedDrag(DragGestureEvent e, Resource resource, DragSourceListener dsl)
{
final Image dragImage = resource.getDragImage();
int offX = 0, offY = 0;
if (dragImage != null) {
offX = -dragImage.getWidth(null) / 2;
offY = -dragImage.getHeight(null) / 2;
}
e.startDrag(DragSource.DefaultCopyDrop, // cursor
dragImage, // drag image
new Point(offX,offY), // drag image offset
new GUI.ResourceTransfer(resource),
dsl); // drag source listener
}
private static void startDrag(Component source,
MouseEvent mouseEvent,
Image rawImage,
Point dragOffset,
Transferable transfer)
{
if (dragOffset == null) {
if (rawImage == null) {
dragOffset = new Point(0,0);
} else {
int w = rawImage.getWidth(null);
int h = rawImage.getHeight(null);
dragOffset = new Point(w / -2, h / -2);
}
}
// this is a coordinate within the component named in DragStub
Point dragStart = mouseEvent.getPoint();
// the cursor in the DragStub is determining the actual
// cursor: the action in the DragGestureEvent and the
// cursror arg to startDrag: don't know what they're doing...
DragGestureEvent trigger = new DragGestureEvent(new DragStub(mouseEvent, source),
DnDConstants.ACTION_COPY, // preferred action (1 only)
dragStart, // start point
Collections.singletonList(mouseEvent));
trigger
.startDrag(DragSource.DefaultCopyDrop, // cursor
rawImage,
dragOffset,
transfer,
//null, // drag source listener
//MapViewer.this // drag source listener
new GUI.DragSourceAdapter()
// is optional when startDrag from DragGestureEvent, but not dragSource.startDrag
);
}
/** A relatively empty DragGestureRecognizer just so we can kick off our
* own drag event. */
private static class DragStub extends DragGestureRecognizer {
public DragStub(InputEvent triggerEvent, Component startFrom) {
super(DragSource.getDefaultDragSource(),
startFrom, // component (drag start coordinates local to here)
DnDConstants.ACTION_COPY | // alone, prevents drop while command is down, tho shows w/copy cursor better
DnDConstants.ACTION_MOVE |
DnDConstants.ACTION_LINK
,
null); // DragGestureListener (can be null)
super.events.add(triggerEvent);
//out("NEW DRAGGER");
}
protected void registerListeners() { /*out("DRAGGER REGISTER");*/ }
protected void unregisterListeners() { /*out("DRAGGER UN-REGISTER");*/ }
}
public static class DragSourceAdapter implements DragSourceListener {
public void dragOver(DragSourceDragEvent dsde) {
if (DEBUG.DND & DEBUG.META) out("dragOver: " + dragName(dsde) + " " + dragSource(dsde));
}
public void dragEnter(DragSourceDragEvent dsde) {
if (DEBUG.Enabled||DEBUG.DND) out(" dragEnter: " + dragName(dsde) + " " + dragSource(dsde));
tufts.vue.LWTransfer.markAsLocal(true);
}
public void dropActionChanged(DragSourceDragEvent dsde) {
if (DEBUG.DND) out(" dropActionChanged: " + dragName(dsde) + " " + dragSource(dsde));
}
public void dragExit(DragSourceEvent dse) {
if (DEBUG.Enabled||DEBUG.DND) out(" dragExit: " + dragSource(dse));
tufts.vue.LWTransfer.markAsLocal(false);
}
public void dragDropEnd(DragSourceDropEvent dsde) {
if (DEBUG.DND) out(" dragDropEnd: " + dragName(dsde) + " " + dragSource(dsde));
}
}
public static class ResourceTransfer implements Transferable
{
private final java.util.List flavors = new java.util.ArrayList(3);
private final java.util.List content;
public ResourceTransfer(tufts.vue.Resource r) {
content = Collections.singletonList(r);
flavors.add(DataFlavor.stringFlavor);
flavors.add(Resource.DataFlavor);
}
public ResourceTransfer(java.io.File file) {
content = Collections.singletonList(file);
flavors.add(DataFlavor.stringFlavor);
flavors.add(DataFlavor.javaFileListFlavor);
}
/** For a list of Files or URLResource's */
public ResourceTransfer(java.util.List list)
{
flavors.add(DataFlavor.stringFlavor);
final Object first = list.get(0);
if (first instanceof Resource) {
flavors.add(Resource.DataFlavor);
if (first instanceof tufts.vue.URLResource)
flavors.add(DataFlavor.javaFileListFlavor);
} else if (first instanceof java.io.File) {
flavors.add(DataFlavor.javaFileListFlavor);
}
content = list;
}
/** Returns the array of flavors in which it can provide the data. */
public synchronized java.awt.datatransfer.DataFlavor[] getTransferDataFlavors() {
return (DataFlavor[]) flavors.toArray(new DataFlavor[flavors.size()]);
}
/** Returns whether the requested flavor is supported by this object. */
public boolean isDataFlavorSupported(DataFlavor flavor) {
if (flavor == null)
return false;
for (int i = 0; i < flavors.size(); i++)
if (flavor.equals(flavors.get(i)))
return true;
return false;
}
/**
* String flavor: return single Resource as text or File as text
* Resource flavor: return a list of Resource's
* Java file list flavor: return list of either Resources or Files
*/
public synchronized Object getTransferData(DataFlavor flavor)
throws UnsupportedFlavorException, java.io.IOException
{
if (DEBUG.WORK || (DEBUG.DND && DEBUG.META)) System.out.println("ResourceTransfer: getTransferData, flavor=" + flavor);
Object result = null;
if (DataFlavor.stringFlavor.equals(flavor)) {
// Always support something for the string flavor, or
// we get an exception thrown (even tho I think that
// may be against the published API).
Object o = content.get(0);
if (o instanceof java.io.File) {
result = ((java.io.File)o).toString();
} else if (o instanceof Resource) {
// todo: Resource.getReference? Returns URL if there is one, local package content otherwise
java.io.File file = ((Resource)o).getActiveDataFile();
if (file != null)
result = file.toString();
else
result = ((Resource)o).getSpec();
} else {
Log.error("getTransferData: unhandled stringFlavor type: " + Util.tags(o));
result = o;
}
} else if (Resource.DataFlavor.equals(flavor)) {
result = content;
} else if (DataFlavor.javaFileListFlavor.equals(flavor)) {
result = content;
} else {
throw new UnsupportedFlavorException(flavor);
}
if (DEBUG.WORK || (DEBUG.DND && DEBUG.META)) System.out.println("\treturning " + Util.tags(result));
return result;
}
}
public static class MouseWheelRelay implements MouseWheelListener {
private final MouseWheelListener head, tail;
/**
* Create's a MouseWheelListener that forwards events to "head", and if the event hasn't
* been consumed there (@see AWTEvent.consume()), forwards the event on to "tail".
*/
public MouseWheelRelay(MouseWheelListener head, MouseWheelListener tail) {
if (head == null || tail == null)
throw new NullPointerException("MouseWheelRelay: neither head or tail can be null");
this.head = head;
this.tail = tail;
}
/**
* Intercept MouseWheelEvents going to the nearest JScrollPane ancestor of
* Component "overriding" by sending them to "intercept" first. The intercepting
* component should consume the event for those it wishes to override, otherwise the event
* will be passed on to the orignal target "overriding".
*
* If no JScrollPane is found to intercept events on, the intercepting component will
* simply be added as a standard MouseWheelListener.
*
* @return true if an intercept was installed, false if a standard MouseWheelListener was installed
*
* BUG: note that the event delivered to the intercepting listener will still
* have its x,y coordinates in the coordinate space of the original target, so
* the interceptor will currently have to check the event source and adjust for
* any differing coordinate systems manually.
*/
public static boolean addScrollPaneIntercept(final MouseWheelListener intercepting, java.awt.Component overriding)
{
if (overriding instanceof JScrollPane) {
; // overriding is already fully discovered
} else {
Component nearestScrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, overriding);
if (nearestScrollPane != null)
overriding = nearestScrollPane;
}
final MouseWheelListener[] currentListeners = overriding.getMouseWheelListeners();
if (DEBUG.MOUSE || DEBUG.INIT || DEBUG.FOCUS || currentListeners.length > 1)
out("MouseWheelRelay: " + GUI.name(overriding) + ": currentMouseWheelListeners: "
+ java.util.Arrays.asList(currentListeners)
+ "; intercepting=" + GUI.name(intercepting));
if (currentListeners == null || currentListeners.length == 0 || currentListeners[0] == null) {
overriding.addMouseWheelListener(intercepting);
return false;
} else {
overriding.removeMouseWheelListener(currentListeners[0]);
overriding.addMouseWheelListener(new MouseWheelRelay(intercepting, currentListeners[0]));
return true;
}
}
public void mouseWheelMoved(MouseWheelEvent e) {
if (DEBUG.FOCUS) Log.debug("MW-HEAD->" + GUI.eventName(e) +
"\n\t" + getClass().getName() + ": INTERCEPTING dispatch: " + Util.tags(head));
// first, send to the intercept to see if it wants it
// TODO: can use SwingUtilities.convertMouseEvent to patch coordinate system of intercepted mouse event
head.mouseWheelMoved(e);
// if unconsumed by the intercept, send on to the original target we're overriding (usually, a JScrollPane)
if (!e.isConsumed()) {
if (DEBUG.FOCUS) Log.debug("MW-TAIL->" + GUI.eventName(e) +
"\n\t" + getClass().getName() + ": ORIGINAL dispatch: " + Util.tags(tail));
tail.mouseWheelMoved(e);
}
}
}
private static class DefaultMetalTheme extends javax.swing.plaf.metal.DefaultMetalTheme
{
private CommonMetalTheme common;
DefaultMetalTheme(CommonMetalTheme common) { this.common = common; }
public FontUIResource getMenuTextFont() { return common.fontMedium; }
public FontUIResource getUserTextFont() { return common.fontSmall; }
public FontUIResource getControlTextFont() { return common.fontControl; }
protected ColorUIResource getSecondary1() { return common.VueSecondary1; }
protected ColorUIResource getSecondary2() { return common.VueSecondary2; }
protected ColorUIResource getSecondary3() { return common.VueSecondary3; }
public void addCustomEntriesToTable(UIDefaults table) {
super.addCustomEntriesToTable(table);
common.addCustomEntriesToTable(table);
}
public String getName() { return super.getName() + " (VUE)"; }
}
static class CommonMetalTheme
{
// these are gray in Metal Default Theme
final ColorUIResource VueSecondary1;
final ColorUIResource VueSecondary2;
final ColorUIResource VueSecondary3;
protected FontUIResource fontSmall = new FontUIResource("SansSerif", Font.PLAIN, 11);
protected FontUIResource fontMedium = new FontUIResource("SansSerif", Font.PLAIN, 12);
// controls: labels, buttons, tabs, tables, etc.
protected FontUIResource fontControl = new FontUIResource("SansSerif", Font.PLAIN, 12);
CommonMetalTheme() {
Color VueColor = getVueColor();
//Color VueColor = new ColorUIResource(200, 221, 242); // #c8ddf2 from OceanTheme
VueSecondary3 = new ColorUIResource(VueColor);
VueSecondary2 = new ColorUIResource(VueColor.darker());
VueSecondary1 = new ColorUIResource(VueColor.darker().darker());
TabbedPaneUI.toolbarColor = GUI.ToolbarColor;
TabbedPaneUI.vueSecondary2Color = VueSecondary2;
}
public void addCustomEntriesToTable(UIDefaults table)
{
//table.put("ComboBox.background", Color.white);
table.put("Button.font", fontSmall);
table.put("Label.font", fontSmall);
table.put("TitledBorder.font", fontMedium.deriveFont(Font.BOLD));
table.put("TabbedPaneUI", "tufts.vue.gui.GUI$TabbedPaneUI");
}
}
/**
* This tweaks the background color of unselected tabs in the tabbed pane,
* and completely turns off painting any kind of focus indicator.
*/
public static class TabbedPaneUI extends javax.swing.plaf.metal.MetalTabbedPaneUI {
static Color toolbarColor;
static Color vueSecondary2Color;
public static ComponentUI createUI( JComponent x ) {
if (DEBUG.INIT) System.out.println("Creating GUI.TabbedPaneUI");
return new TabbedPaneUI();
/*
return new TabbedPaneUI(new ColorUIResource(Color.blue),
new ColorUIResource(Color.green));
*/
}
protected void XinstallDefaults() {
super.installDefaults();
super.tabAreaBackground = Color.blue;
super.selectColor = Color.green;
super.selectHighlight = Color.red;
super.highlight = Color.green;
super.lightHighlight = Color.magenta;
super.shadow = Color.black;
super.darkShadow = Color.black;
super.focus = Color.orange;
}
protected void paintFocusIndicator(Graphics g, int tabPlacement,
Rectangle[] rects, int tabIndex,
Rectangle iconRect, Rectangle textRect,
boolean isSelected) {}
protected void paintTabBackground( Graphics g, int tabPlacement,
int tabIndex, int x, int y, int w, int h, boolean isSelected ) {
int slantWidth = h / 2;
if (isSelected) {
// in Ocean Theme, this is a ligher blue, so we've overriden it in defaults.
g.setColor(selectColor);
} else {
//g.setColor( tabPane.getBackgroundAt( tabIndex ) );
Color c = tabPane.getBackgroundAt( tabIndex );
// for now, allow toolbar color as optional tabbed bg, but all others override
if (toolbarColor.equals(c))
g.setColor(c);
else
g.setColor(vueSecondary2Color);
}
switch ( tabPlacement ) {
case LEFT:
g.fillRect( x + 5, y + 1, w - 5, h - 1);
g.fillRect( x + 2, y + 4, 3, h - 4 );
break;
case BOTTOM:
g.fillRect( x + 2, y, w - 2, h - 4 );
g.fillRect( x + 5, y + (h - 1) - 3, w - 5, 3 );
break;
case RIGHT:
g.fillRect( x + 1, y + 1, w - 5, h - 1);
g.fillRect( x + (w - 1) - 3, y + 5, 3, h - 5 );
break;
case TOP:
default:
g.fillRect( x + 4, y + 2, (w - 1) - 3, (h - 1) - 1 );
g.fillRect( x + 2, y + 5, 2, h - 5 );
}
}
}
// From: https://developer.apple.com/library/mac/#technotes/tn2007/tn2196.html
// Create a Layout component that will ensure the buttons abut each other
public static JComponent createLayoutComponent(java.util.List<JButton> segmentButtons)
{
Box layoutBox = Box.createHorizontalBox();
for(JButton button : segmentButtons) {
layoutBox.add(button);
}
return layoutBox;
}
public static AbstractButton createSegmentButton(String style, String position, ButtonGroup buttonGrp, String label)
{
final AbstractButton button = new JToggleButton();
button.putClientProperty("JButton.buttonType", style);
button.putClientProperty("JButton.segmentPosition", position);
button.setLabel(label);
buttonGrp.add(button);
return button;
}
// public static class ButtonBox extends Box {
// final ButtonGroup group;
// ButtonBox(ButtonGroup group) {
// }
// }
/**
* Style is one of: "segmented", "segmentedCapsule", "segmentedTextured", "segmentedRoundRect"
* @see https://developer.apple.com/library/mac/#technotes/tn2007/tn2196.html
*
* For each button, the clientProperty with key "segment.value" will hold the original object.
*
* It saves created ButtonGroup in the Box using a JComponent client property with key ButtonGroup.class.
*/
public static Box createButtonBox(final String style, final Object[] labelStringables) {
final Box box = Box.createHorizontalBox();
final ButtonGroup buttonGroup = new ButtonGroup();
box.putClientProperty(ButtonGroup.class, buttonGroup);
String position = "first";
int count = 1;
for (Object o : labelStringables) {
final AbstractButton b = createSegmentButton(style, position, buttonGroup, Util.upCaseInitial(o.toString()));
if (o instanceof ComboKey) {
b.setName(((ComboKey)o).key);
b.putClientProperty(ComboKey.class, o);
}
b.putClientProperty("segment.value", o);
b.setFocusPainted(false);
// b.setMargin(EmptyInsets); // no effect
b.setFont(GUI.LabelFace);
//b.setFont(GUI.TitleFace);
//b.setForeground(Color.white);
if (count == 1)
b.setSelected(true);
box.add(b);
if (++count == labelStringables.length)
position = "last";
else
position = "middle";
}
return box;
}
// Bottleneck for creating the buttons for the button group
public static java.util.List<AbstractButton> createSegmentButtonsWithStyle(int numButtons, ButtonGroup buttonGrp, String style)
{
// Allocate a list of JButtons
java.util.List<AbstractButton> buttons = new ArrayList<AbstractButton>();
if (numButtons == 1) {
// If 1 button is requested, then it gets the "only" segment position
buttons.add(createSegmentButton(style, "only", buttonGrp, ""));
} else {
// If more than 1 button is requested, then
// the first one gets "first" the last one gets "last" and the rest get "middle"
buttons.add(createSegmentButton(style, "first", buttonGrp, ""));
for(int i = 0; i < buttons.size() - 2; ++i) {
buttons.add(createSegmentButton(style, "middle", buttonGrp, ""));
}
buttons.add(createSegmentButton(style, "last", buttonGrp, ""));
}
return buttons;
}
/*
public void propertyChange(java.beans.PropertyChangeEvent e) {
out(e);
}
*/
// static { Toolkit.getDefaultToolkit().addPropertyChangeListener(new GUI()); }
private static void out(String s) {
Log.info(s);
}
/*
private static PropertyChangeHandler PropertyChangeHandler = new PropertyChangeHandler();
static java.beans.PropertyChangeListener getPropertyChangeHandler() {
if (PropertyChangeHandler == null)
PropertyChangeHandler = new PropertyChangeHandler();
return PropertyChangeHandler;
}
static class PropertyChangeHandler implements java.beans.PropertyChangeListener
{
private PropertyChangeHandler() {}
private boolean mIgnoreActionEvents = false;
public void propertyChange(java.beans.PropertyChangeEvent e) {
if (DEBUG.Enabled) out("propertyChange: " + e);
}
public void actionPerformed(java.awt.event.ActionEvent e) {
if (mIgnoreActionEvents) {
if (DEBUG.TOOL) System.out.println(this + " ActionEvent ignored: " + e);
} else {
if (DEBUG.TOOL) System.out.println(this + " actionPerformed " + e);
//fireFontChanged(null, makeFont());
}
}
}
*/
// /**
// * Size a normal Window to the maximum size usable in the current
// * platform & desktop configuration, <i>without</i> using the java
// * special full-screen mode, which can't have any other windows
// * on top of it, and changes the way user input events are handled.
// * On the PC, this will just be the whole screen (bug: probably
// * not good enough if they have non-hiding menu bar set to always-
// * on-top). On the Mac, it will be adjusted for the top menu
// * bar and the dock if it's visible.
// */
// // todo: test in multi-screen environment
// private static void XsetFullScreen(Window window)
// {
// refreshGraphicsInfo();
// Dimension screen = window.getToolkit().getScreenSize();
// if (Util.isMacPlatform()) {
// // mac won't layer a regular window over the menu bar, so
// // we need to limit the size
// Rectangle desktop = GEnvironment.getMaximumWindowBounds();
// out("setFullScreen: mac maximum bounds " + Util.out(desktop));
// if (desktop.x > 0 && desktop.x <= 4) {
// // hack for smidge of space it attempts to leave if the dock is
// // at left and auto-hiding
// desktop.width += desktop.x;
// desktop.x = 0;
// } else {
// // dock at bottom & auto-hiding
// int botgap = screen.height - (desktop.y + desktop.height);
// if (botgap > 0 && botgap <= 4) {
// desktop.height += botgap;
// } else {
// // dock at right & auto-hiding
// int rtgap = screen.width - desktop.width;
// if (rtgap > 0 && rtgap <= 4)
// desktop.width += rtgap;
// }
// }
// if (DEBUG.FOCUS) desktop.height /= 5;
// out("setFullScreen: mac adjusted bounds " + Util.out(desktop));
// window.setLocation(desktop.x, desktop.y);
// window.setSize(desktop.width, desktop.height);
// } else {
// if (DEBUG.FOCUS) screen.height /= 5;
// window.setLocation(0, 0);
// window.setSize(screen.width, screen.height);
// }
// out("setFullScreen: set to " + window);
// }
private GUI() {}
public static void main(String args[]) {
// Can't find any property names on Mac OS X -- are there any?
// Is it possible to get an update when the screen DisplayMode changes?
String propName = null;
if (args.length > 0)
propName = args[0];
if (propName == null)
propName = "win.propNames";
//new java.awt.Frame("A Frame").show();
Object propValue = Toolkit.getDefaultToolkit().getDesktopProperty(propName);
System.out.println("Property " + propName + " = " + propValue);
if (propValue != null && propValue instanceof String[]) {
System.out.println("Supported windows property names:");
String[] propNames = (String[]) propValue;
for (int i = 0; i < propNames.length; i++) {
Object value = Toolkit.getDefaultToolkit().getDesktopProperty(propNames[i]);
System.out.println(propNames[i] + " = " + value);
}
}
}
}