/* * 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); } } } }