001: /*
002: * Swing Explorer. Tool for developers exploring Java/Swing-based application internals.
003: * Copyright (C) 2008, Maxim Zakharenkov
004: *
005: * This program is free software; you can redistribute it and/or modify
006: * it under the terms of the GNU General Public License as published by
007: * the Free Software Foundation; either version 2 of the License, or
008: * (at your option) any later version.
009: *
010: * This program is distributed in the hope that it will be useful,
011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013: * GNU General Public License for more details.
014: *
015: * You should have received a copy of the GNU General Public License along
016: * with this program; if not, write to the Free Software Foundation, Inc.,
017: * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
018: *
019: * $Header: /cvs/swingexplorer/src/org/swingexplorer/GuiUtils.java,v 1.3 2008/03/07 11:46:50 maxz1 Exp $
020: */
021: package org.swingexplorer;
022:
023: import java.awt.BorderLayout;
024: import java.awt.Color;
025: import java.awt.Component;
026: import java.awt.Dialog;
027: import java.awt.Dimension;
028: import java.awt.Frame;
029: import java.awt.GraphicsConfiguration;
030: import java.awt.Image;
031: import java.awt.Insets;
032: import java.awt.Point;
033: import java.awt.Toolkit;
034: import java.awt.Window;
035: import java.awt.event.ComponentEvent;
036: import java.awt.event.WindowAdapter;
037: import java.awt.event.WindowEvent;
038: import java.lang.reflect.Constructor;
039: import java.net.URL;
040: import java.text.DateFormat;
041: import java.text.MessageFormat;
042: import java.text.SimpleDateFormat;
043: import java.util.Enumeration;
044:
045: import javax.swing.Action;
046: import javax.swing.ImageIcon;
047: import javax.swing.JFrame;
048: import javax.swing.JInternalFrame;
049: import javax.swing.JMenuItem;
050: import javax.swing.JPopupMenu;
051: import javax.swing.JTree;
052: import javax.swing.SwingUtilities;
053: import javax.swing.tree.DefaultMutableTreeNode;
054: import javax.swing.tree.DefaultTreeModel;
055: import javax.swing.tree.TreeNode;
056: import javax.swing.tree.TreePath;
057:
058: import org.swingexplorer.PNLComponentTree.TreeNodeObject;
059:
060: /**
061: * Helper GUI utility methods
062: * @author Maxim Zakharenkov
063: */
064: final public class GuiUtils {
065:
066: /** Logger used by this class. */
067:
068: /**
069: * Constant which may be used as background error color for any editor.
070: * This constant define light red color.
071: */
072: public static final Color EDITOR_ERROR_COLOR = new Color(255, 150,
073: 150);
074:
075: /**
076: * Constant used for tool-tip text generation. Defines error
077: * font color in HTML format.
078: */
079: public static final String EDITOR_ERROR_COLOR_HTML = "#f30000";
080:
081: /**
082: * Private constructor means class cannot be instantiated.
083: */
084: private GuiUtils() {
085: }
086:
087: /**
088: * Used to get instance of <i>Image</i> object using <i>ClassLoader</i> as
089: * resource provider.
090: * @param resourceName Name of the resource where image stored.
091: * @return Instance of <i>Image</i> object or <code>null</code> if
092: * resource is not found.
093: */
094: public static Image getImage(String resourceName) {
095: return getImage(null, resourceName);
096: }
097:
098: /**
099: * Used to get instance of <i>Image</i> object. Specified class or
100: * <i>ClassLoader</i> if class is null are used to get access to the
101: * resource where image is stored.
102: * @param _class Class used to get the image resource.
103: * May be <code>null</code>.
104: * @param resourceName Name of the resource where image is stored.
105: * @return Instance of <i>Image</i> object or <code>null</code> if
106: * resource is not found.
107: */
108: public static Image getImage(Class _class, String resourceName) {
109: URL resource;
110: if (_class != null) {
111: // Strange things happens here:
112: // The following code must work in web start,
113: // but is not working in local Java application.
114: resource = _class.getClassLoader()
115: .getResource(resourceName);
116: // that's why following code used:
117: if (resource == null) {
118: resource = _class.getResource(resourceName);
119: }
120:
121: } else {
122: resource = Thread.currentThread().getContextClassLoader()
123: .getResource(resourceName);
124: }
125: if (resource != null) {
126: return Toolkit.getDefaultToolkit().getImage(resource);
127: } else {
128: return null;
129: }
130: }
131:
132: /**
133: * Used to get instance of <i>ImageIcon</i> object. Invoke
134: * {@link #getImageIcon(Class, String)} method with <code>null</code> class.
135: * @param resourceName Name of the resource where image stored.
136: * @return Instance of <i>ImageIcon</i> object or <code>null</code> if
137: * resource is not found.
138: */
139: public static ImageIcon getImageIcon(String resourceName) {
140: return getImageIcon(null, resourceName);
141: }
142:
143: /**
144: * Used to get instance of <i>ImageIcon</i> object. Specified class or
145: * <i>ClassLoader</i> if class is null are used to get access to the
146: * resource where image is stored.
147: * @param _class Class used to get the image resource.
148: * May be <code>null</code>.
149: * @param resourceName Name of the resource where image is stored.
150: * @return Instance of <i>Image</i> object or <code>null</code> if
151: * resource is not found.
152: */
153: public static ImageIcon getImageIcon(Class _class,
154: String resourceName) {
155: Image image = getImage(_class, resourceName);
156: if (image != null) {
157: return new ImageIcon(image);
158: } else {
159: return null;
160: }
161: }
162:
163: /**
164: * Used to get an icon with specified width and height. If the width or
165: * height are less or equal with 0 icon will not be scaled.
166: * @param resourceName Name of resource where icon is stored.
167: * @param width Width for the returned icon.
168: * @param height Height for the returned icon.
169: * @return The created and scaled icon or <code>null</code> if resource
170: * is not available.
171: */
172: public static ImageIcon getScaledIcon(String resourceName,
173: int width, int height) {
174: ImageIcon result = getImageIcon(resourceName);
175: if (result != null) {
176: if (width > 0 && height > 0) {
177: Image artImage = result.getImage().getScaledInstance(
178: width, height, Image.SCALE_SMOOTH);
179: result.setImage(artImage);
180: }
181: }
182: return result;
183: }
184:
185: /**
186: * Used to get parent window for specified component.
187: * @param component The component for which parent window should be found.
188: * @return The window where specified component is placed or the component
189: * itself if it is instance of <i>Window</i>.
190: */
191: public static Window getParentWindow(Component component) {
192: Window result;
193: if (component instanceof Window) {
194: result = (Window) component;
195: } else if (component != null) {
196: result = SwingUtilities.getWindowAncestor(component);
197: } else {
198: throw new NullPointerException(MessageFormat.format(
199: "Cannot find parent window for component \"{0}\"",
200: new Object[] { component }));
201: }
202: return result;
203: }
204:
205: /**
206: * Centers specified child component on the specified container.
207: * @param parent Container used to center on, if <code>null</code>
208: * then child is centered relatively to the whole screen.
209: * @param child The component to be centered.
210: */
211: public static void center(Component parent, Component child) {
212: if (parent == null) {
213: center(child);
214: } else {
215: center(parent.getLocationOnScreen(), parent.getSize(),
216: child, false);
217: }
218: }
219:
220: /**
221: * Centers specified component on a screen.
222: * @param component The component to be centered.
223: */
224: public static void center(Component component) {
225: // size will be restricted against screen size anyway
226: // that's why boolean argument is false.
227: // anyway this argument should not be removed - it is simply reserved
228: // for future if MDI restriction will be required.
229: center(new Point(0, 0), null, component, true);
230:
231: }
232:
233: private static void center(Point parentLocation,
234: Dimension parentSize, Component component,
235: boolean restrictSize) {
236:
237: // this dimension is used to fit into screen after positioning performed
238: Dimension screenSize = Toolkit.getDefaultToolkit()
239: .getScreenSize();
240:
241: if (parentSize == null) {
242: parentSize = screenSize;
243: }
244:
245: Dimension childSize = component.getSize();
246:
247: // Disallows child GUI component have greater size than parent GUI
248: // container have.
249: // This method is reserved to restrict area when MDI component used.
250: if (restrictSize) {
251: if (childSize.height > parentSize.height) {
252: childSize.height = parentSize.height;
253: }
254: if (childSize.width > parentSize.width) {
255: childSize.width = parentSize.width;
256: }
257: }
258:
259: // check child size against screen size anyway:
260: if (childSize.height > screenSize.height) {
261: childSize.height = screenSize.height;
262: }
263: if (childSize.width > screenSize.width) {
264: childSize.width = screenSize.width;
265: }
266: component.setSize(childSize.width, childSize.height);
267:
268: int childLocationX = (parentSize.width - childSize.width) / 2
269: + parentLocation.x;
270: int childLocationY = (parentSize.height - childSize.height) / 2
271: + parentLocation.y;
272:
273: // check location and size to fit into screen:
274: if (childSize.width + childLocationX > screenSize.width) {
275: childLocationX = screenSize.width - childSize.width;
276: }
277: if (childSize.height + childLocationY > screenSize.height) {
278: childLocationY = screenSize.height - childSize.height;
279: }
280: if (childLocationX < 0) {
281: childLocationX = 0;
282: }
283: if (childLocationY < 0) {
284: childLocationY = 0;
285: }
286:
287: // Centers child GUI component on the parent GUI container.
288: component.setLocation(childLocationX, childLocationY);
289: }
290:
291: /**
292: * Character <code>&</code> used as prefix for a mnemonic symbol.
293: */
294: public static final char AMP = '&' /**/;
295:
296: private static final char NO_MNEMONIC = (char) 0;
297:
298: /**
299: * Used to return string where single <code>&</code> symbols are
300: * extracted and double are replaced with single one.
301: * @param text Text from where the symbols has to be extracted.
302: * @return String where the symbols are extracted.
303: */
304: public static String getTextWithoutMnemonic(String text) {
305: if (text == null) {
306: return null;
307: }
308: StringBuffer result = new StringBuffer();
309: int idx = 0;
310: while (idx < text.length()) {
311: char c = text.charAt(idx++);
312: if (c != AMP) {
313: result.append(c);
314: } else {
315: if (idx < text.length()) {
316: result.append(text.charAt(idx++));
317: } else {
318: result.append(AMP);
319: }
320: }
321: }
322: return result.toString();
323: }
324:
325: /**
326: * Used to get from a text a mnemonic defined by a single
327: * <code>&</code> symbol.
328: * @param text Text where mnemonic may be defined.
329: * @return Extracted mnemonic or character that is zero if
330: * mnemonic is not specified.
331: */
332: public static char getMnemonicFromText(String text) {
333: if (text == null) {
334: return NO_MNEMONIC;
335:
336: } else if (text.length() < 1) {
337: return NO_MNEMONIC;
338: }
339:
340: int idx = text.indexOf(AMP);
341: while (idx >= 0) {
342: if (text.length() > (++idx)) {
343: if (text.charAt(idx) != AMP) {
344: break;
345: }
346: idx = text.indexOf(AMP, idx + 1);
347: } else {
348: idx = -1;
349: }
350: }
351: if (idx < 0) {
352: return NO_MNEMONIC;
353: } else {
354: return text.charAt(idx);
355: }
356: }
357:
358: /**
359: * Used to create popup menu with specified actions.
360: * @param actions Array of actions used for the popup menu creation. If
361: * array contains <i>null</i> element separator will be created in the
362: * corresponding position.
363: * @return New instance of popup menu.
364: */
365: public static JPopupMenu createPopupMenu(Action[] actions) {
366: JPopupMenu result = new JPopupMenu();
367: for (int i = 0; i < actions.length; i++) {
368: if (actions[i] != null) {
369:
370: JMenuItem item = new JMenuItem(actions[i]) {
371: public void setText(String text) {
372: super .setText(GuiUtils
373: .getTextWithoutMnemonic(text));
374: }
375: };
376: result.add(item);
377:
378: } else {
379:
380: result.addSeparator();
381: }
382: }
383: return result;
384: }
385:
386: /**
387: * Adds listener to window that ensures that
388: * the window is not resized less then specified size.
389: * In case if it is resized anyway it restores the
390: * minimal size.
391: *
392: * @param wnd Window which size should be restricted.
393: * @param minSize The minimum allowed size for the window.
394: */
395: public static void restrictWindowMinimumSize(final Window wnd,
396: final Dimension minSize) {
397: restrictMinimumSizeImpl(wnd, minSize);
398: }
399:
400: /**
401: * Adds listener to internal frame that ensures that
402: * the frame is not resized less then specified size.
403: * In case if it is resized anyway it restores the
404: * minimal size.
405: *
406: * @param frame Frame which size should be restricted.
407: * @param minSize The minimum allowed size for the window.
408: */
409: public static void restrictWindowMinimumSize(
410: final JInternalFrame frame, final Dimension minSize) {
411: restrictMinimumSizeImpl(frame, minSize);
412: }
413:
414: private static void restrictMinimumSizeImpl(
415: final Component component, final Dimension minSize) {
416: // Ensure that window has normal size before resizing event
417: Dimension curSize = component.getSize();
418: if (curSize.width < minSize.width
419: || curSize.height < minSize.height) {
420: component.setSize(minSize);
421: }
422: // Adding listener. If added after size settings - will not be added if
423: // one of arguments is NULL.
424: component
425: .addComponentListener(new java.awt.event.ComponentAdapter() {
426: public void componentResized(ComponentEvent e) {
427: Dimension curSize = component.getSize();
428: if (curSize.width < minSize.width
429: || curSize.height < minSize.height) {
430: Dimension newSize = new Dimension(Math.max(
431: minSize.width, curSize.width),
432: Math.max(minSize.height,
433: curSize.height));
434: component.setSize(newSize);
435: }
436: }
437: });
438: }
439:
440: /**
441: * Ensures that specified window do not exceed screen size, including
442: * possible task bar.
443: * @param window The window to be placed exactly on screen.
444: */
445: public static void ensureWindowOnScreen(Window window) {
446: Toolkit kit = Toolkit.getDefaultToolkit();
447: GraphicsConfiguration gc = window.getGraphicsConfiguration();
448: Insets ins = kit.getScreenInsets(gc);
449: Dimension totalSize = kit.getScreenSize();
450:
451: int availWidth = totalSize.width - ins.left - ins.right;
452: int availHeight = totalSize.height - ins.top - ins.bottom;
453:
454: // modify location and size only if window is out of screen:
455: Point loc = window.getLocation();
456: Dimension size = window.getSize();
457: if (loc.x < ins.left || loc.y < ins.top
458: || loc.x + size.width > availWidth
459: || loc.y + size.height > availHeight) {
460:
461: // modify size to be inside the screen:
462: if (size.width > availWidth) {
463: size.width = availWidth;
464: }
465: if (size.height > availHeight) {
466: size.height = availHeight;
467: }
468: window.setSize(size);
469:
470: // modify locaiton to be on screen:
471: if (loc.x < ins.left) {
472: loc.x = ins.left;
473: }
474: if (loc.y < ins.top) {
475: loc.y = ins.top;
476: }
477:
478: if (loc.x + size.width > totalSize.width - ins.right) {
479: loc.x = totalSize.width - ins.right - size.width;
480: }
481: if (loc.y + size.height > totalSize.height - ins.bottom) {
482: loc.y = totalSize.height - ins.bottom - size.height;
483: }
484: window.setLocation(loc);
485: }
486: }
487:
488: /**
489: * Method for demo purposes.
490: * Shows specified frame with 1/1.5 size
491: * of screen. When frame is closed the System.exit(0) is executed;
492: * @param frame Frame to show
493: */
494: public static void showDemoFrame(JFrame frame) {
495: // Making good size and center the frame
496: Dimension screenSize = Toolkit.getDefaultToolkit()
497: .getScreenSize();
498: frame.setSize((int) (screenSize.width / 1.5),
499: (int) (screenSize.height / 1.5));
500: center(frame);
501:
502: // Adding exit listener
503: frame.addWindowListener(new WindowAdapter() {
504: public void windowClosing(WindowEvent e) {
505: System.exit(0);
506: }
507: });
508: frame.setVisible(true);
509: }
510:
511: /**
512: * Method for demo purposes.
513: * Shows specified panel in frame with 1/1.5 size
514: * of screen. When frame is closed the System.exit(0) is executed
515: * @param panel Panel to show
516: */
517: public static void showDemoPanel(Component panel) {
518: JFrame frame = new JFrame();
519: frame.getContentPane().setLayout(new BorderLayout());
520: frame.getContentPane().add(panel, BorderLayout.CENTER);
521: showDemoFrame(frame);
522: }
523:
524: /**
525: * Constructs instance of Dialog that has Frame or Dialog
526: * parameter in constructor.
527: *
528: * @param dialogClass Class of dialog to construct.
529: * @param parent Dialog owner component.
530: * @return New dialog instance.
531: * @throws IllegalArgumentException If invalid class specified.
532: */
533: public static Dialog createDialogInstance(Class dialogClass,
534: Component parent) throws IllegalArgumentException {
535: // get parent window...
536: Window owner;
537: if (parent instanceof Window) {
538: owner = (Window) parent;
539: } else if (parent != null) {
540: owner = SwingUtilities.getWindowAncestor(parent);
541: } else {
542: owner = null;
543: }
544: // create instance...
545: Dialog result = null;
546: Constructor ctor = null;
547: try {
548: if (owner instanceof Dialog) {
549: ctor = dialogClass
550: .getConstructor(new Class[] { Dialog.class });
551: result = (Dialog) ctor
552: .newInstance(new Object[] { owner });
553:
554: } else if (owner instanceof Frame) {
555: ctor = dialogClass
556: .getConstructor(new Class[] { Frame.class });
557: result = (Dialog) ctor
558: .newInstance(new Object[] { owner });
559:
560: } else {
561: ctor = dialogClass.getConstructor(new Class[] {});
562: result = (Dialog) ctor.newInstance(new Object[] {});
563: }
564:
565: } catch (Exception ex) {
566: throw new IllegalArgumentException(
567: "Invalid dialog class. It must be public and have public constructor with Frame or Dialog parameter");
568: }
569: return result;
570: }
571:
572: private static int timeZone = 0;
573: private static String timeFormat = null;
574: private static String dateFormat = null;
575: private static String dateTimeFormat = null;
576:
577: /**
578: * Gets time representing format.
579: * @return Modified locale time format.
580: */
581: public static String getTimeFormat() {
582: if (timeFormat == null) {
583: timeFormat = modifyFormat(((SimpleDateFormat) DateFormat
584: .getTimeInstance(DateFormat.MEDIUM)).toPattern());
585: }
586: return timeFormat;
587: }
588:
589: /**
590: * Gets date-time representing format. This format depends on locale
591: * setting but is modified to ensure that every position has length of
592: * 2, and year position has length of 4.
593: * @return Modified locale date-time format.
594: */
595: public static String getDateTimeFormat() {
596: if (dateTimeFormat == null) {
597: dateTimeFormat = modifyFormat(((SimpleDateFormat) DateFormat
598: .getDateTimeInstance(DateFormat.SHORT,
599: DateFormat.MEDIUM)).toPattern());
600: }
601: return dateTimeFormat;
602: }
603:
604: /**
605: * Gets date representing format.
606: * @return Modified locale date format.
607: */
608: public static String getDateFormat() {
609: if (dateFormat == null) {
610: dateFormat = modifyFormat(((SimpleDateFormat) DateFormat
611: .getDateInstance(DateFormat.SHORT)).toPattern());
612: }
613: return dateFormat;
614: }
615:
616: private static String modifyFormat(String format) {
617: StringBuffer buffer = new StringBuffer();
618: int index = 0;
619: while (index < format.length()) {
620:
621: char ch = format.charAt(index);
622:
623: if ("dMyHhmsSa".indexOf(ch) < 0) {
624: // this is not pattern character:
625: buffer.append(ch);
626: index++;
627: } else {
628: // add pattern character twice (or 4 times for year)
629: buffer.append(ch);
630: buffer.append(ch);
631: if (ch == 'S') {
632: // milliseconds are 3-digits
633: buffer.append(ch);
634: }
635: if (ch == 'y') {
636: // year is 4-digit number
637: buffer.append(ch);
638: buffer.append(ch);
639: }
640: // and skip all same character following:
641: do {
642: index++;
643: } while (index < format.length()
644: && format.charAt(index) == ch);
645: }
646: }
647:
648: return buffer.toString();
649: }
650:
651: /**
652: * Gets time shift (in minutes) used by UI
653: * elements to display date/time value.
654: * @return 0 (by default) meaning no shift used, positive value meaning
655: * displayed date/time is greater than value set to control, negative value
656: * means displayed one is less than set to control.
657: * @see #setTimeZoneOffset(int)
658: */
659: public static int getTimeZoneOffset() {
660: return timeZone;
661: }
662:
663: /**
664: * Sets time zone used by UI elements displaying date/time values.
665: * @param minutes The time zone to be used.
666: * @see #getTimeZoneOffset()
667: */
668: public static void setTimeZoneOffset(int minutes) {
669: timeZone = minutes;
670: }
671:
672: /**
673: * Gets string description of the currently used Java Virtual Machine.
674: * @return String containing Java VM name and version.
675: */
676: public static String getJavaVersion() {
677: String vmName = System.getProperty("java.vm.name");
678: String vmVersion = System.getProperty("java.vm.version");
679:
680: return MessageFormat.format("{0} ({1})", vmName, vmVersion);
681: }
682:
683: /**
684: * Method to be used in conjunction with {@link #expandTreePaths(JTree, Enumeration)}
685: * @param tree
686: * @return
687: */
688: public static Enumeration<TreePath> getExpatnedTreePaths(JTree tree) {
689: TreePath pathToRoot = new TreePath(tree.getModel().getRoot());
690: Enumeration<TreePath> expandPaths = tree
691: .getExpandedDescendants(pathToRoot);
692: return expandPaths;
693: }
694:
695: /**
696: * Used to restore expansion state of the tree like:
697: * <pre>
698: * Enumeration<TreePath> expandPaths = tree.getExpandedDescendants(pathToRoot);
699: *
700: * // do some changes in the tree model
701: * ...
702: *
703: * // fire change event
704: * DefaultTreeModel model = (DefaultTreeModel)treProblems.getModel();
705: * model.nodeStructureChanged(root);
706: *
707: * // restore expanded state
708: * GuiUtils.expandTreePaths(tree, expandPaths);
709: * </pre>
710: * It is useful only when DefaultMutableTreeNode elements or their successors
711: * are used inside tree.
712: * @param tree
713: * @param expandPaths
714: */
715: public static void expandTreePaths(JTree tree,
716: Enumeration<TreePath> expandPaths) {
717: if (expandPaths != null) {
718:
719: DefaultMutableTreeNode root = (DefaultMutableTreeNode) tree
720: .getModel().getRoot();
721:
722: // iterator through paths to expand
723: while (expandPaths.hasMoreElements()) {
724: // obtaion last path user object
725: TreePath curPath = expandPaths.nextElement();
726: DefaultMutableTreeNode castedNode = (DefaultMutableTreeNode) curPath
727: .getLastPathComponent();
728: Object obj = castedNode.getUserObject();
729:
730: // go through all elements in the tree and search for elements to expand
731: // matching them by user object
732: Enumeration enumAll = root.breadthFirstEnumeration();
733: while (enumAll.hasMoreElements()) {
734: DefaultMutableTreeNode curNode = (DefaultMutableTreeNode) enumAll
735: .nextElement();
736: if (obj.equals(curNode.getUserObject())) {
737: // expand
738: tree
739: .expandPath(new TreePath(curNode
740: .getPath()));
741: break;
742: }
743: }
744: }
745: }
746: }
747:
748: /**
749: * Utility method to notify tree about changes without
750: * loosing expansion state
751: * @param treProblems
752: */
753: public static void notifyTreeChanged(JTree treProblems) {
754: Enumeration<TreePath> expandedState = GuiUtils
755: .getExpatnedTreePaths(treProblems);
756: DefaultTreeModel model = (DefaultTreeModel) treProblems
757: .getModel();
758: model.nodeStructureChanged((TreeNode) model.getRoot());
759: GuiUtils.expandTreePaths(treProblems, expandedState);
760: }
761: }
762:
763: /*
764: * $Log: GuiUtils.java,v $
765: * Revision 1.3 2008/03/07 11:46:50 maxz1
766: * Added thread violation monitor and EDT hang monitor. Changed L & F.
767: *
768: * Revision 1.2 2008/02/06 08:36:08 maxz1
769: * Changed license header
770: *
771: * Revision 1.1 2007/06/27 19:41:38 maxz1
772: * new
773: *
774: */
|