View Javadoc

1   /*
2    *  XNap Commons
3    *
4    *  Copyright (C) 2005  Felix Berger
5    *  Copyright (C) 2005  Steffen Pingel
6    *
7    *  This library is free software; you can redistribute it and/or
8    *  modify it under the terms of the GNU Lesser General Public
9    *  License as published by the Free Software Foundation; either
10   *  version 2.1 of the License, or (at your option) any later version.
11   *
12   *  This library is distributed in the hope that it will be useful,
13   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15   *  Lesser General Public License for more details.
16   *
17   *  You should have received a copy of the GNU Lesser General Public
18   *  License along with this library; if not, write to the Free Software
19   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20   */
21  package org.xnap.commons.gui.util;
22  
23  import java.awt.Color;
24  import java.awt.Component;
25  import java.awt.Container;
26  import java.awt.Dimension;
27  import java.awt.Font;
28  import java.awt.Point;
29  import java.awt.Toolkit;
30  import java.awt.event.ActionEvent;
31  import java.awt.event.ActionListener;
32  import java.awt.event.ComponentAdapter;
33  import java.awt.event.ComponentEvent;
34  import java.awt.event.InputEvent;
35  import java.awt.event.KeyEvent;
36  import java.io.IOException;
37  import java.io.InputStream;
38  import java.util.Enumeration;
39  import java.util.HashSet;
40  import java.util.StringTokenizer;
41  import javax.swing.AbstractAction;
42  import javax.swing.AbstractButton;
43  import javax.swing.Action;
44  import javax.swing.ActionMap;
45  import javax.swing.BorderFactory;
46  import javax.swing.InputMap;
47  import javax.swing.JComponent;
48  import javax.swing.JLabel;
49  import javax.swing.JMenu;
50  import javax.swing.JMenuItem;
51  import javax.swing.JPopupMenu;
52  import javax.swing.JScrollBar;
53  import javax.swing.JTabbedPane;
54  import javax.swing.JTable;
55  import javax.swing.JTree;
56  import javax.swing.KeyStroke;
57  import javax.swing.UIManager;
58  import javax.swing.border.Border;
59  import javax.swing.text.JTextComponent;
60  import javax.swing.tree.TreeNode;
61  import javax.swing.tree.TreePath;
62  import org.xnap.commons.gui.ThinBevelBorder;
63  import org.xnap.commons.i18n.I18n;
64  import org.xnap.commons.i18n.I18nFactory;
65  import org.xnap.commons.util.FileHelper;
66  import org.xnap.commons.util.SystemHelper;
67  
68  /***
69   * Helps with gui related tasks.
70   */
71  public class GUIHelper
72  {
73  	static final I18n I18N = I18nFactory.getI18n(GUIHelper.class);
74  
75  	/***
76  	 * Kicker offset.
77  	 */
78  	public static final int POPUP_MENU_HEIGHT_INSET = 50;
79  
80  	/***
81  	 * Adds a mapping between the enter key and <code>action</code> to the
82  	 * input map of <code>c</code>.
83  	 * 
84  	 * @return true, if successful; false, otherwise
85  	 */
86  	public static boolean bindEnterKey(JComponent c, Action action)
87  	{
88  		KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
89  		return bindKey(c, ks, action, true);
90  	}
91  
92  	/***
93  	 * Does the same as {@link #bindEnterKey(JComponent, Action)}but uses the
94  	 * default input map and not the window input map.
95  	 * 
96  	 * @return true, if successful; false, otherwise
97  	 */
98  	public static boolean bindEnterKeyLocally(JComponent c, Action action)
99  	{
100 		KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
101 		return bindKey(c, ks, action, false);
102 	}
103 
104 	/***
105 	 * Adds a mapping between the escape key and <code>action</code> to the
106 	 * input map of <code>c</code>.
107 	 * 
108 	 * @return true, if successful; false, otherwise
109 	 */
110 	public static boolean bindEscapeKey(JComponent c, Action action)
111 	{
112 		KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
113 		return bindKey(c, ks, action, true);
114 	}
115 
116 	/***
117 	 * Adds a mapping between <code>ks</code> and <code>action</code> to the
118 	 * input map of <code>c</code>.
119 	 * 
120 	 * @return true, if successful; false, otherwise
121 	 */
122 	public static boolean bindKey(JComponent c, KeyStroke ks, Action action,
123 			boolean whenInFocusedWindow)
124 	{
125 		InputMap inputMap = (whenInFocusedWindow) ? c
126 				.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) : c
127 				.getInputMap();
128 		ActionMap actionMap = c.getActionMap();
129 		if (inputMap != null && actionMap != null) {
130 			inputMap.put(ks, action);
131 			actionMap.put(action, action);
132 			return true;
133 		}
134 		return false;
135 	}
136 
137 	/***
138 	 * Returns an etched default border.
139 	 * TODO move this to builder?
140 	 */
141 	public static Border createDefaultBorder(String title)
142 	{
143 		return BorderFactory.createTitledBorder(BorderFactory
144 				.createEtchedBorder(), " " + title + " ");
145 	}
146 
147 	/***
148 	 * Returns an empty border.
149 	 */
150 	public static Border createEmptyBorder(int inset)
151 	{
152 		return BorderFactory.createEmptyBorder(inset, inset, inset, inset);
153 	}
154 
155 	/***
156 	 * Returns an empty border.
157 	 */
158 	public static Border createEmptyBorder()
159 	{
160 		return createEmptyBorder(0);
161 	}
162 
163 	/***
164 	 * Returns an empty border.
165 	 */
166 	public static Border createEtchedBorder()
167 	{
168 		return BorderFactory.createEtchedBorder();
169 	}
170 
171 	public static Border createLoweredBorder()
172 	{
173 		return new ThinBevelBorder(ThinBevelBorder.LOWERED);
174 	}
175 
176 	public static Border createRaisedBorder()
177 	{
178 		return new ThinBevelBorder(ThinBevelBorder.RAISED);
179 	}
180 
181 	/***
182 	 * Creates and answers a label with separator; useful to separate paragraphs
183 	 * in a panel. This is often a better choice than a
184 	 * <code>TitledBorder</code>.
185 	 * <p>
186 	 * The current implementation doesn't support component alignments.
187 	 * 
188 	 * <p>
189 	 * Copyright (c) 2003 JGoodies Karsten Lentzsch. All Rights Reserved.
190 	 * Modified by Steffen Pingel for XNap.
191 	 * 
192 	 * @param text
193 	 *            the title's text
194 	 * @param alignment
195 	 *            text alignment: left, center, right
196 	 * @return a separator with title label
197 	 */
198 	//      public JComponent createSeparator(String text, int alignment)
199 	//  	{
200 	//          JPanel header = new JPanel(new GridBagLayout());
201 	//          GridBagConstraints gbc = new GridBagConstraints();
202 	//          gbc.weightx = 0.0;
203 	//          gbc.weighty = 1.0;
204 	//          gbc.anchor = GridBagConstraints.SOUTHWEST;
205 	//          gbc.fill = GridBagConstraints.BOTH;
206 	//          gbc.gridwidth = 1;
207 	//          gbc.gridheight = 3;
208 	//          if (text != null && text.length() > 0) {
209 	//  			JLabel label = new TitleLabel();
210 	//          setTextAndMnemonic(label, textWithMnemonic);
211 	//          label.setVerticalAlignment(SwingConstants.CENTER);
212 	//          label.setBorder(BorderFactory.createEmptyBorder(1, 0, 1, gap));
213 	//          return label;
214 	//              header.add(createTitle(text, 4), gbc);
215 	//          }
216 	//          gbc.weightx = 1.0;
217 	//          gbc.weighty = 1.0;
218 	//          gbc.gridwidth = GridBagConstraints.REMAINDER;
219 	//          gbc.gridheight = 1;
220 	//          JSeparator separator = new JSeparator();
221 	//          header.add(Box.createGlue(), gbc);
222 	//          gbc.weighty = 0.0;
223 	//          header.add(separator, gbc);
224 	//          gbc.weighty = 1.0;
225 	//          header.add(Box.createGlue(), gbc);
226 	//          return header;
227 	//      }
228 
229 	/***
230 	 * Returns a component that displays <code>title</code> in a bold font.
231 	 * 
232 	 */
233 	public static JComponent createHeader(String title)
234 	{
235 		JLabel label = new JLabel(title);
236 		Font font = UIManager.getFont("TitledBorder.font");
237 		if (font != null) {
238 			label.setFont(font.deriveFont(Font.BOLD));
239 		}
240 		Color foreground = UIManager.getColor("TitledBorder.titleColor");
241 		if (foreground != null) {
242 			label.setForeground(foreground);
243 		}
244 		return label;
245 	}
246 
247 	/***
248 	 * Returns a titled border.
249 	 */
250 	public static Border createTitledBorder(String title, int inset)
251 	{
252 		return BorderFactory.createCompoundBorder(createDefaultBorder(title),
253 				createEmptyBorder(inset));
254 	}
255 
256     public static void expandAllNodes(JTree tree, boolean expand) 
257     {
258         TreeNode root = (TreeNode)tree.getModel().getRoot();
259         expandAllNodes(tree, new TreePath(root), expand);
260     }
261     
262     public static void expandAllNodes(JTree tree, TreePath path, boolean expand) {
263         // do a depth-first search recursively as expand or callapse must be
264     	// done bottom-up
265         TreeNode node = (TreeNode)path.getLastPathComponent();
266         if (node.getChildCount() > 0) {
267             for (Enumeration e = node.children(); e.hasMoreElements(); ) {
268                 TreeNode child = (TreeNode)e.nextElement();
269                 TreePath childPath = path.pathByAddingChild(child);
270                 expandAllNodes(tree, childPath, expand);
271             }
272         }
273     
274         if (expand) {
275             tree.expandPath(path);
276         } else {
277             tree.collapsePath(path);
278         }
279     }	
280 	
281 	public static void restrictWidth(JComponent jc)
282 	{
283 		jc.setMaximumSize(new Dimension(jc.getPreferredSize().width, jc
284 				.getMaximumSize().height));
285 	}
286 
287 	public static void scrollToEnd(JTextComponent jt)
288 	{
289 		//          Element map = jt.getDocument().getDefaultRootElement();
290 		//  		Element lastLine = map.getElement(map.getElementCount() - 1);
291 		//  		jt.setCaretPosition(lastLine.getEndOffset() - 1);
292 		jt.setCaretPosition(jt.getDocument().getEndPosition().getOffset() - 1);
293 	}
294 
295 	/***
296 	 * Returns true, if <code>jsb</code> is at the maximum value.
297 	 */
298 	public static boolean shouldScroll(JScrollBar jsb)
299 	{
300 		int pos = jsb.getValue() + jsb.getVisibleAmount();
301 		return (pos == jsb.getMaximum());
302 	}
303 
304 	public static KeyStroke getMenuKeyStroke(int keyCode)
305 	{
306 		int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
307 		return KeyStroke.getKeyStroke(keyCode, mask);
308 	}
309 
310 	public static void setAccelerator(JMenuItem jmi, int keyCode)
311 	{
312 		int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
313 		jmi.setAccelerator(KeyStroke.getKeyStroke(keyCode, mask));
314 	}
315 
316 	public static void setMnemonics(JTabbedPane pane)
317 	{
318 		HashSet<Integer> letters = new HashSet<Integer>();
319 		for (int i = 0; i < pane.getTabCount(); i++) {
320 			if (pane.getMnemonicAt(i) == 0) {
321 				pane.setMnemonicAt(i, getMnemonicForText(pane.getTitleAt(i),
322 						letters));
323 			}
324 		}
325 	}
326 
327 	// TODO assign mnemonics for container itself
328 	public static void setMnemonics(Container c)
329 	{
330 		setMnemonics(c, null);
331 	}
332 
333 	public static void setMnemonics(Container c, HashSet<Integer> l)
334 	{
335 		HashSet<Integer> letters = (l != null) ? l : new HashSet<Integer>();
336 		for (int i = 0; i < c.getComponentCount(); i++) {
337 			Component component = c.getComponent(i);
338 			if (component instanceof AbstractButton) {
339 				AbstractButton ab = (AbstractButton)component;
340 				if (ab.getMnemonic() == 0) {
341 					setMnemonics(ab, letters);
342 				}
343 				else {
344 					letters.add(new Integer(ab.getMnemonic()));
345 				}
346 			}
347 			if (component instanceof JLabel) {
348 				JLabel label = (JLabel)component;
349 				if (label.getDisplayedMnemonic() != 0) {
350 					letters.add(new Integer(label.getDisplayedMnemonic()));
351 				}
352 				if (label.getLabelFor() != null) {
353 					setMnemonics(label, letters);
354 				}
355 			}
356 			if (component instanceof JMenu) {
357 				setMnemonics(((JMenu)component).getPopupMenu());
358 			}
359 			// recurse
360 			if (component instanceof Container) {
361 				setMnemonics((Container)component, letters);
362 			}
363 		}
364 	}
365 
366 	private static boolean setMnemonics(JLabel label, HashSet<Integer> letters)
367 	{
368 		if (label.getText() == null) {
369 			return true;
370 		}
371 		String text = label.getText();
372 		// try first letters of words first
373 		StringTokenizer t = new StringTokenizer(text);
374 		while (t.hasMoreTokens()) {
375 			int character = (int)t.nextToken().charAt(0);
376 			if (!letters.contains(character)) {
377 				letters.add(character);
378 				label.setDisplayedMnemonic(character);
379 				return true;
380 			}
381 		}
382 		// pick any character, start with the second one
383 		// the first one has already been checked
384 		for (int i = 1; i < text.length(); i++) {
385 			int character =  (int)text.charAt(i);
386 			if (text.charAt(i) != ' ' && !letters.contains(character)) {
387 				letters.add(character);
388 				label.setDisplayedMnemonic(character);
389 				return true;
390 			}
391 		}
392 		return false;
393 	}
394 
395 	private static int getMnemonicForText(String text, HashSet<Integer> letters)
396 	{
397 		// try first letters of words first
398 		StringTokenizer t = new StringTokenizer(text);
399 		while (t.hasMoreTokens()) {
400 			int character = (int)t.nextToken().charAt(0);
401 			if (!letters.contains(character)) {
402 				letters.add(character);
403 				return character;
404 			}
405 		}
406 		// pick any character, start with the second one
407 		// the first one has already been checked
408 		for (int i = 1; i < text.length(); i++) {
409 			int character = (int)text.charAt(i);
410 			if (text.charAt(i) != ' ' && !letters.contains(character)) {
411 				letters.add(character);
412 				return character;
413 			}
414 		}
415 		return 0;
416 	}
417 
418 	private static boolean setMnemonics(AbstractButton ab, HashSet<Integer> letters)
419 	{
420 		if (ab.getText() == null) {
421 			return true;
422 		}
423 		String text = ab.getText().toUpperCase();
424 		// try first letters of words first
425 		StringTokenizer t = new StringTokenizer(text);
426 		while (t.hasMoreTokens()) {
427 			int character = (int)t.nextToken().charAt(0);
428 			if (!letters.contains(character)) {
429 				letters.add(character);
430 				ab.setMnemonic(character);
431 				return true;
432 			}
433 		}
434 		// pick any character, start with the second one
435 		// the first one has already been checked
436 		for (int i = 1; i < text.length(); i++) {
437 			int character = (int)text.charAt(i);
438 			if (text.charAt(i) != ' ' && !letters.contains(character)) {
439 				letters.add(character);
440 				ab.setMnemonic(character);
441 				return true;
442 			}
443 		}
444 		return false;
445 	}
446 
447 	/***
448 	 * Loads text from file and sets it to <code>jtc</code>.
449 	 * 
450 	 * <p>
451 	 * If file is not found or could not be read, sets <code>altText</code>
452 	 * instead.
453 	 */
454 	public static void showFile(JTextComponent jtc, String filename,
455 			String altText)
456 	{
457 		InputStream s = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename);
458 		if (s != null) {
459 			try {
460 				jtc.setText(FileHelper.readText(s));
461 				jtc.setCaretPosition(0);
462 				return;
463 			}
464 			catch (IOException e) {}
465 			finally {
466 				try {
467 					s.close();
468 				}
469 				catch (IOException e) {}
470 			}
471 		}
472 		// file reading failed for some reason, fallback to altText
473 		jtc.setText(altText);
474 	}
475 
476 	public static void showPopupMenu(JPopupMenu jpm, Component source, int x,
477 			int y, int yOffset)
478 	{
479 		Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
480 		screen.height -= POPUP_MENU_HEIGHT_INSET;
481 		Point origin = source.getLocationOnScreen();
482 		origin.translate(x, y);
483 		int height = jpm.getHeight();
484 		if (height == 0) {
485 			jpm.addComponentListener(new SizeListener(source, x, y));
486 		}
487 		int width = jpm.getWidth();
488 		if (origin.x + width > screen.width) {
489 			//x -= (origin.x + width) - screen.width;
490 			// we prefer kde behaviour
491 			x -= width;
492 		}
493 		if (origin.y + height > screen.height) {
494 			// we prefer kde behaviour
495 			//y -= (origin.y + height) - screen.height;
496 			y -= height;
497 			y += yOffset;
498 		}
499 		jpm.show(source, x, y);
500 	}
501 
502 	public static void showPopupMenu(JPopupMenu jpm, Component source, int x,
503 			int y)
504 	{
505 		showPopupMenu(jpm, source, x, y, 0);
506 	}
507 
508 	/***
509 	 * Wraps HTML tags around <code>text</code> so the maximum width is
510 	 * limited to a senseful value.
511 	 * 
512 	 * @return text, enclosed in table html tags
513 	 */
514 	public static String label(String text)
515 	{
516 		return tt(text, 500);
517 	}
518 
519 	/***
520 	 * Wraps HTML tags around <code>text</code> so the maximum width is
521 	 * limited to a senseful value.
522 	 * 
523 	 * @return text, enclosed in table html tags
524 	 */
525 	public static String tt(String text, int width)
526 	{
527 		StringBuilder sb = new StringBuilder(33 + text.length() + 25);
528 		sb.append("<html>");
529 		sb.append("<table><tr><td width=\"" + width + "\">");
530 		sb.append(text);
531 		sb.append("</td></tr></table>");
532 		sb.append("</html>");
533 		return sb.toString();
534 	}
535 
536 	public static KeyStroke getMenuShortcut(int keyCode)
537 	{
538 		int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
539 		return KeyStroke.getKeyStroke(keyCode, mask);
540 	}
541 
542 	/***
543 	 * Wraps HTML tags around <code>text</code> so the maximum width is
544 	 * limited to a sensible value.
545 	 * 
546 	 * @return text, enclosed in table html tags
547 	 */
548 	public static String tt(String text)
549 	{
550 		return tt(text, 300);
551 	}
552 
553 	/***
554 	 * Formats key, value as a HTML table row, the key is highlighted as bold.
555 	 */
556 	public static String tableRow(String key, String value)
557 	{
558 		StringBuilder sb = new StringBuilder();
559 		sb.append("<tr><td><b>");
560 		sb.append(key);
561 		sb.append("</b></td><td> ");
562 		sb.append((value != null) ? value : I18N.tr("Unknown"));
563 		sb.append("</td></tr>");
564 		return sb.toString();
565 	}
566 
567 	public static void limitSize(JComponent c)
568 	{
569 		c.setMaximumSize(new Dimension(c.getPreferredSize().width, c
570 				.getMaximumSize().height));
571 	}
572 
573 	public static void bindExpandCollapseKeysToTree(final JTree tree)
574 	{
575 		Action treeExpandAction = new AbstractAction() {
576 			public void actionPerformed(ActionEvent event)
577 			{
578 				TreePath path = tree.getSelectionPath();
579 				if (path != null) {
580 					GUIHelper.expandAllNodes(tree, path, true);
581 				}
582 			}
583 		};
584 		tree.getActionMap().put(treeExpandAction, treeExpandAction);
585 		tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_MULTIPLY, 0), treeExpandAction);
586 		tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ASTERISK, KeyEvent.SHIFT_MASK), treeExpandAction);
587 
588 		Action treeCollapseAction = new AbstractAction() {
589 			public void actionPerformed(ActionEvent event)
590 			{
591 				TreePath path = tree.getSelectionPath();
592 				if (path != null) {
593 					GUIHelper.expandAllNodes(tree, path, false);
594 				}
595 			}
596 		};
597 		tree.getActionMap().put(treeCollapseAction, treeCollapseAction);
598 		tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DIVIDE, 0), treeCollapseAction);
599 	}
600 	
601 	/***
602 	 * Adds some Emacs like keybindings to a table for moving between rows.
603 	 */
604 	public static void bindEmacsKeysToTable(JTable jta)
605 	{
606 		ActionListener al = jta.getActionForKeyStroke(KeyStroke.getKeyStroke(
607 				KeyEvent.VK_DOWN, 0));
608 		jta.registerKeyboardAction(al, KeyStroke.getKeyStroke(KeyEvent.VK_N,
609 				InputEvent.CTRL_MASK), JComponent.WHEN_FOCUSED);
610 		al = jta.getActionForKeyStroke(KeyStroke
611 				.getKeyStroke(KeyEvent.VK_UP, 0));
612 		jta.registerKeyboardAction(al, KeyStroke.getKeyStroke(KeyEvent.VK_P,
613 				InputEvent.CTRL_MASK), JComponent.WHEN_FOCUSED);
614 		al = jta.getActionForKeyStroke(KeyStroke.getKeyStroke(
615 				KeyEvent.VK_PAGE_DOWN, 0));
616 		jta.registerKeyboardAction(al, KeyStroke.getKeyStroke(KeyEvent.VK_V,
617 				InputEvent.CTRL_MASK), JComponent.WHEN_FOCUSED);
618 		al = jta.getActionForKeyStroke(KeyStroke.getKeyStroke(
619 				KeyEvent.VK_PAGE_UP, 0));
620 		jta.registerKeyboardAction(al, KeyStroke.getKeyStroke(KeyEvent.VK_V,
621 				InputEvent.ALT_MASK), JComponent.WHEN_FOCUSED);
622 		al = jta.getActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_HOME,
623 				InputEvent.CTRL_MASK));
624 		jta.registerKeyboardAction(al, KeyStroke.getKeyStroke(KeyEvent.VK_LESS,
625 				InputEvent.ALT_MASK), JComponent.WHEN_FOCUSED);
626 		al = jta.getActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_END,
627 				InputEvent.CTRL_MASK));
628 		jta.registerKeyboardAction(al, KeyStroke.getKeyStroke(KeyEvent.VK_LESS,
629 				InputEvent.ALT_MASK + InputEvent.SHIFT_MASK),
630 				JComponent.WHEN_FOCUSED);
631 	}
632 
633 	private static class SizeListener extends ComponentAdapter
634 	{
635 
636 		private Component source;
637 		private int x;
638 		private int y;
639 
640 		public SizeListener(Component source, int x, int y)
641 		{
642 			this.source = source;
643 			this.x = x;
644 			this.y = y;
645 		}
646 
647 		public void componentResized(ComponentEvent e)
648 		{
649 			e.getComponent().removeComponentListener(this);
650 			GUIHelper.showPopupMenu((JPopupMenu)e.getComponent(), source, x, y);
651 		}
652 	}
653 
654 }