[Aprog]
[aprog.git] / Aprog / src / net / sourceforge / aprog / swing / SwingTools.java
blobec5c816eab5e9ce062e505977f4ad92277afd355
1 /*
2 * The MIT License
3 *
4 * Copyright 2010 Codist Monk.
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
25 package net.sourceforge.aprog.swing;
27 import static net.sourceforge.aprog.i18n.Messages.translate;
28 import static net.sourceforge.aprog.tools.Tools.getCallerClass;
29 import static net.sourceforge.aprog.tools.Tools.getCallerMethodName;
30 import static net.sourceforge.aprog.tools.Tools.getLoggerForThisMethod;
31 import static net.sourceforge.aprog.tools.Tools.getResourceAsStream;
32 import static net.sourceforge.aprog.tools.Tools.ignore;
33 import static net.sourceforge.aprog.tools.Tools.invoke;
34 import static net.sourceforge.aprog.tools.Tools.unchecked;
36 import java.awt.Color;
37 import java.awt.Component;
38 import java.awt.Container;
39 import java.awt.Dimension;
40 import java.awt.GridBagConstraints;
41 import java.awt.GridBagLayout;
42 import java.awt.Window;
43 import java.awt.datatransfer.DataFlavor;
44 import java.awt.dnd.DnDConstants;
45 import java.awt.dnd.DropTargetDropEvent;
46 import java.awt.event.MouseAdapter;
47 import java.awt.event.MouseEvent;
48 import java.awt.image.BufferedImage;
49 import java.io.File;
50 import java.io.IOException;
51 import java.io.UnsupportedEncodingException;
52 import java.lang.reflect.InvocationTargetException;
53 import java.net.URLDecoder;
54 import java.util.ArrayList;
55 import java.util.Arrays;
56 import java.util.Collections;
57 import java.util.HashMap;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Scanner;
61 import java.util.logging.Level;
63 import javax.imageio.ImageIO;
64 import javax.swing.AbstractButton;
65 import javax.swing.Box;
66 import javax.swing.ImageIcon;
67 import javax.swing.JDialog;
68 import javax.swing.JFrame;
69 import javax.swing.JLabel;
70 import javax.swing.JMenu;
71 import javax.swing.JMenuBar;
72 import javax.swing.JMenuItem;
73 import javax.swing.JScrollPane;
74 import javax.swing.JSplitPane;
75 import javax.swing.KeyStroke;
76 import javax.swing.SwingUtilities;
77 import javax.swing.UIManager;
78 import javax.swing.WindowConstants;
80 import net.sourceforge.aprog.af.MacOSXTools;
81 import net.sourceforge.aprog.i18n.Messages;
82 import net.sourceforge.aprog.tools.IllegalInstantiationException;
84 /**
85 * This class provides utility static methods to help build Swing GUIs.
86 * <br>According to the JDK, accessing and modifying AWT components should only be done
87 * in the AWT Event Dispatching Thread.
88 * <br>The methods in this class enforce this rule by calling {@link #checkAWT()} to make sure
89 * that they are used in the proper thread.
91 * @author codistmonk (creation 2010-06-26)
93 public final class SwingTools {
95 /**
96 * @throws IllegalInstantiationException To prevent instantiation
98 private SwingTools() {
99 throw new IllegalInstantiationException();
102 private static DataFlavor uriListAsStringFlavor = null;
105 * {@value}.
107 public static final String META = MacOSXTools.MAC_OS_X ? "meta" : "control";
110 * {@value}.
112 public static final String DEFAULT_IMAGES_BASE = "images/";
115 * {@value}.
117 public static final String ICON_FORMAT = "png";
120 * {@value}.
122 public static final String ROLLOVER_DISABLED_ICON_SUFFIX = "_disabled." + ICON_FORMAT;
125 * {@value}.
127 public static final String ROLLOVER_NORMAL_ICON_SUFFIX = "." + ICON_FORMAT;
130 * {@value}.
132 public static final String ROLLOVER_SELECTED_ICON_SUFFIX = "_selected." + ICON_FORMAT;
135 * {@value}.
137 public static final String ROLLOVER_ROLLOVER_ICON_SUFFIX = "_rollover." + ICON_FORMAT;
140 * {@value}.
142 public static final String ROLLOVER_ROLLOVER_SELECTED_ICON_SUFFIX = "_rollover_selected." + ICON_FORMAT;
144 private static final Map<String, ImageIcon> iconCache = new HashMap<String, ImageIcon>();
146 private static String imagesBase = DEFAULT_IMAGES_BASE;
150 * @return
151 * <br>Not null
152 * <br>Shared
154 public static final String getImagesBase() {
155 return imagesBase;
160 * @param imagesBase
161 * <br>Not null
162 * <br>Shared
164 public static final void setImagesBase(final String imagesBase) {
165 SwingTools.imagesBase = imagesBase;
169 * Returns the icon located at {@code getImageBase() + resourceName}.
170 * <br>Icons are cached using {@code resourceName} as a key.
172 * @param resourceName
173 * <br>Not null
174 * @return
175 * <br>Not null
176 * <br>New
177 * @throws RuntimeException if the resource cannot be loaded
179 public static final ImageIcon getIcon(final String resourceName) {
180 try {
181 final ImageIcon cachedIcon = iconCache.get(resourceName);
183 if (cachedIcon != null) {
184 return cachedIcon;
187 final ImageIcon icon = new ImageIcon(ImageIO.read(getResourceAsStream(getImagesBase() + resourceName)));
189 iconCache.put(resourceName, icon);
191 return icon;
192 } catch (final IOException exception) {
193 throw unchecked(exception);
199 * @param resourceName
200 * <br>Not null
201 * @return
202 * <br>Maybe null
203 * <br>New
205 public static final ImageIcon getIconOrNull(final String resourceName) {
206 try {
207 return getIcon(resourceName);
208 } catch (final Exception exception) {
209 ignore(exception);
211 return null;
216 * @param image
217 * <br>Not null
218 * @param title
219 * <br>Not null
220 * @param modal
221 * <br>Range: any boolean
223 public static final void show(final BufferedImage image, final String title, final boolean modal) {
224 final JLabel imageLabel = new JLabel(new ImageIcon(image));
226 imageLabel.addMouseMotionListener(new MouseAdapter() {
228 @Override
229 public final void mouseMoved(final MouseEvent event) {
230 final int x = event.getX();
231 final int y = event.getY();
233 if (0 <= x && x < image.getWidth() && 0 <= y && y < image.getHeight()) {
234 final Color color = new Color(image.getRGB(x, y));
236 invoke(imageLabel.getRootPane().getParent(), "setTitle",
237 title + " (x: " + x + ") (y: " + y + ") (r: " + color.getRed() +
238 ") (g: " + color.getGreen() + ") (b: " + color.getBlue() + ") (a: " + color.getAlpha() + ")");
244 show(new JScrollPane(imageLabel), title, modal);
248 * @param component
249 * <br>Not null
250 * @param title
251 * <br>Not null
252 * @param modal
253 * <br>Range: any boolean
255 public static final void show(final Component component, final String title, final boolean modal) {
256 final Runnable runnable = new Runnable() {
258 @Override
259 public final void run() {
260 final JDialog frame = new JDialog((JFrame) null, title, true);
262 frame.add(component);
263 frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
265 packAndCenter(frame).setVisible(true);
270 if (modal) {
271 try {
272 SwingUtilities.invokeAndWait(runnable);
273 } catch (final Exception exception) {
274 throw unchecked(exception);
276 } else {
277 SwingUtilities.invokeLater(runnable);
283 * @param container
284 * <br>Not null
285 * <br>Input-output
286 * @param component
287 * <br>Not null
288 * <br>Input-output
289 * <br>Shared
290 * @param constraints
291 * <br>Not null
293 public static final void add(final Container container, final Component component, final GridBagConstraints constraints) {
294 checkAWT();
296 if (!(container.getLayout() instanceof GridBagLayout)) {
297 container.setLayout(new GridBagLayout());
300 final GridBagLayout layout = (GridBagLayout) container.getLayout();
302 layout.setConstraints(component, constraints);
304 container.add(component);
309 * @param <T> the actual type of {@code button}
310 * @param button
311 * <br>Not null
312 * <br>Input-output
313 * @param imageName
314 * <br>Not null
315 * @param borderPainted if {@code false}, then the preferred size is set to the size of the image,
316 * and the background and border are not drawn; if {@code true}, then {@code button} is left in its current state
317 * @return {@code button}
318 * <br>Not null
320 public static final <T extends AbstractButton> T rollover(final T button, final String imageName, final boolean borderPainted) {
321 checkAWT();
323 button.setRolloverEnabled(true);
324 button.setDisabledIcon(getIconOrNull(imageName + ROLLOVER_DISABLED_ICON_SUFFIX));
325 button.setIcon(getIconOrNull(imageName + ROLLOVER_NORMAL_ICON_SUFFIX));
326 button.setSelectedIcon(getIconOrNull(imageName + ROLLOVER_SELECTED_ICON_SUFFIX));
327 button.setRolloverIcon(getIconOrNull(imageName + ROLLOVER_ROLLOVER_ICON_SUFFIX));
328 button.setRolloverSelectedIcon(getIconOrNull(imageName + ROLLOVER_ROLLOVER_SELECTED_ICON_SUFFIX));
330 if (!borderPainted) {
331 if (button.getIcon() != null) {
332 button.setPreferredSize(new Dimension(button.getIcon().getIconWidth(), button.getIcon().getIconHeight()));
335 button.setBorderPainted(false);
338 return button;
342 * Encloses {@code component} in a scroll pane.
344 * @param component
345 * <br>Not null
346 * <br>Input-output
347 * @return
348 * <br>Not null
349 * <br>New
351 public static final JScrollPane scrollable(final Component component) {
352 checkAWT();
354 return new JScrollPane(component);
358 * Packs and updates {@code window}'s minimum size so that it cannot be resized to be smaller than its packed size.
360 * @param <W> The actual type of {@code window}
361 * @param window
362 * <br>Not null
363 * <br>input-output
364 * @return {@code window}
365 * <br>Not null
367 public static final <W extends Window> W packAndUpdateMinimumSize(final W window) {
368 checkAWT();
370 window.setMinimumSize(null);
371 window.pack();
372 window.setMinimumSize(window.getSize());
374 return window;
378 * Packs and centers {@code window} on the screen.
380 * @param <W> The actual type of {@code window}
381 * @param window
382 * <br>Not null
383 * <br>input-output
384 * @return {@code window}
385 * <br>Not null
387 public static final <W extends Window> W packAndCenter(final W window) {
388 checkAWT();
390 window.pack();
392 return center(window);
396 * Centers {@code window} on the screen.
398 * @param <W> the actual type of {@code window}
399 * @param window
400 * <br>Not null
401 * <br>input-output
402 * <br>Shared
403 * @return {@code window}
404 * <br>Not null
405 * <br>Shared
407 public static final <W extends Window> W center(final W window) {
408 checkAWT();
410 window.setLocationRelativeTo(null);
412 return window;
416 * Creates a menu bar from the nonnull elements of {@code menus}.
418 * @param menus
419 * <br>Not null
420 * @return
421 * <br>Not null
422 * <br>New
424 public static final JMenuBar menuBar(final JMenu... menus) {
425 checkAWT();
427 final JMenuBar result = new JMenuBar();
429 for (final JMenu menu : menus) {
430 if (menu != null) {
431 boolean menuHasNonnullItems = false;
433 for (int i = 0; i < menu.getItemCount(); ++i) {
434 menuHasNonnullItems |= menu.getItem(i) != null;
437 if (menuHasNonnullItems) {
438 result.add(menu);
443 return result;
447 * Creates a menu from the elements in {@code items}.
448 * <br>A null element generates a separator.
449 * <br>Consecutive null elements are coalesced into only one null element.
450 * <br>If all the elements are null, then the generated menu is empty
451 * (it doesn't even contain a separator).
453 * @param title
454 * <br>Not null
455 * @param items
456 * <br>Not null
457 * @return
458 * <br>Not null
459 * <br>New
461 public static final JMenu menu(final String title, final JMenuItem... items) {
462 checkAWT();
464 final JMenu result = new JMenu(title);
466 boolean lastItemWasNull = true;
468 for (final JMenuItem item : items) {
469 if (item != null) {
470 result.add(item);
471 } else if (!lastItemWasNull) {
472 result.addSeparator();
475 lastItemWasNull = item == null;
478 return result;
482 * @return
483 * <br>Not null
485 public static final DataFlavor getURIListAsStringFlavor() {
486 if (uriListAsStringFlavor == null) {
487 try {
488 uriListAsStringFlavor = new DataFlavor("text/uri-list; class=java.lang.String");
489 } catch (final ClassNotFoundException exception) {
490 getLoggerForThisMethod().log(Level.SEVERE, "", exception);
494 return uriListAsStringFlavor;
499 * @param event
500 * <br>Not null
501 * <br>Input-output
502 * @return a list of files, or an empty list if {@code event} cannot provide a list of files or a string
503 * <br>Not null
504 * <br>Maybe new
505 * @throws RuntimeException if an error occurs
507 @SuppressWarnings("unchecked")
508 public static final List<File> getFiles(final DropTargetDropEvent event) {
509 event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
511 try {
512 if (event.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
513 return (List<File>) event.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
516 final String transferDataString = (String) event.getTransferable().getTransferData(DataFlavor.stringFlavor);
518 if (event.isDataFlavorSupported(getURIListAsStringFlavor())) {
519 final Scanner scanner = new Scanner(transferDataString);
520 final List<File> result = new ArrayList<File>();
522 while (scanner.hasNext()) {
523 final String fileURL = URLDecoder.decode(scanner.nextLine(), "UTF-8");
524 final String protocol = "file:";
526 result.add(new File(fileURL.startsWith(protocol) ? fileURL.substring(protocol.length()) : fileURL));
529 return result;
532 if (event.isDataFlavorSupported(DataFlavor.stringFlavor)) {
533 return Arrays.asList(new File(transferDataString));
536 return Collections.emptyList();
537 } catch (final Exception exception) {
538 throw unchecked(exception);
542 public static final void useSystemLookAndFeel() {
543 try {
544 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
545 } catch (final Exception exception) {
546 getLoggerForThisMethod().log(Level.WARNING, "", exception);
551 * Executes in the AWT Event Dispatching Thread a runnable invoking
552 * the caller method with the specified arguments,
553 * or does nothing if the method is called in that thread.
554 * <br>This method can be used to simplify code that needs to be executed in AWT
555 * by taking care of generating an anonymous inner class implementing {@link Runnable}.
556 * <br>Example:
557 * <pre>
558 * public final void f() {
559 * // Warning: this section might get executed 2 times in different threads
561 * if (SwingTools.canInvokeThisMethodInAWT(this)) {
562 * // This section is executed only once in the AWT Event Dispatching Thread
563 * // For instance, the following instruction doesn't throw
564 * SwingTools.checkAWT();
567 * // Warning: this section might get executed 2 times in different threads
569 * </pre>
571 * @param object The caller object or {@code null} if the caller is static
572 * <br>Maybe null
573 * <br>Shared
574 * @param arguments
575 * <br>Not null
576 * <br>Shared
577 * @return {@code true} if and only if the method is called in the AWT Event Dispatching Thread
578 * @throws RuntimeException if an error occurs
580 public static final boolean canInvokeThisMethodInAWT(final Object object, final Object... arguments) {
581 if (SwingUtilities.isEventDispatchThread()) {
582 return true;
585 final Class<?> callerClass = getCallerClass();
586 final String callerMethodName = getCallerMethodName();
588 try {
589 SwingUtilities.invokeAndWait(createInvoker(object, callerClass, callerMethodName, arguments));
590 } catch (final InterruptedException exception) {
591 getLoggerForThisMethod().log(Level.WARNING, null, exception);
592 } catch (final InvocationTargetException exception) {
593 throw unchecked(exception.getCause());
596 return false;
600 * Non-blocking version of {@link #canInvokeThisMethodInAWT(java.lang.Object, java.lang.Object[])}.
602 * @param object
603 * <br>Maybe null
604 * <br>Shared
605 * @param arguments
606 * <br>Not null
607 * <br>Shared
608 * @return {@code true} if and only if the method is called in the AWT Event Dispatching Thread
610 public static final boolean canInvokeLaterThisMethodInAWT(final Object object, final Object... arguments) {
611 if (SwingUtilities.isEventDispatchThread()) {
612 return true;
615 final Class<?> callerClass = getCallerClass();
616 final String callerMethodName = getCallerMethodName();
618 SwingUtilities.invokeLater(createInvoker(object, callerClass, callerMethodName, arguments));
620 return false;
624 * Creates an action that will invoke the specified method with
625 * the specified arguments when it is performed.
627 * @param objectOrClass
628 * <br>Not null
629 * <br>Shared
630 * @param methodName
631 * <br>Not null
632 * <br>Shared
633 * @param arguments
634 * <br>Not null
635 * <br>Shared
636 * @return
637 * <br>Not null
638 * <br>New
640 public static final InvokerAction action(final Object objectOrClass,
641 final String methodName, final Object... arguments) {
642 return new InvokerAction(objectOrClass, methodName, arguments);
647 * @throws IllegalStateException if the current thread is not the AWT Event Dispatching Thread
649 public static final void checkAWT() {
650 if (!SwingUtilities.isEventDispatchThread()) {
651 throw new IllegalStateException("This section must be executed in the AWT Event Dispatching Thread");
657 * @throws IllegalStateException if the current thread is the AWT Event Dispatching Thread
659 public static final void checkNotAWT() {
660 if (SwingUtilities.isEventDispatchThread()) {
661 throw new IllegalStateException("This section must not be executed in the AWT Event Dispatching Thread");
667 * @param object
668 * <br>Maybe null
669 * <br>Shared
670 * @param callerClass
671 * <br>Not null
672 * <br>Shared
673 * @param callerMethodName
674 * <br>Not null
675 * <br>Shared
676 * @param arguments
677 * <br>Not null
678 * <br>Shared
679 * @return
680 * <br>Not null
681 * <br>New
683 private static final Runnable createInvoker(final Object object, final Class<?> callerClass, final String callerMethodName, final Object... arguments) {
684 return new Runnable() {
686 @Override
687 public final void run() {
688 invoke(object == null ? callerClass : object, callerMethodName, arguments);
695 * @param components
696 * <br>Not null
697 * @return
698 * <br>Not null
699 * <br>New
701 public static final Box horizontalBox(final Component... components) {
702 checkAWT();
704 final Box result = Box.createHorizontalBox();
706 for (final Component component : components) {
707 result.add(component);
710 return result;
714 * @param components
715 * <br>Not null
716 * @return
717 * <br>Not null
718 * <br>New
720 public static final Box verticalBox(final Component... components) {
721 checkAWT();
723 final Box result = Box.createVerticalBox();
725 for (final Component component : components) {
726 result.add(component);
729 return result;
733 * @param leftComponent
734 * <br>Maybe null
735 * <br>Will become reference
736 * @param rightComponent
737 * <br>Maybe null
738 * <br>Will become reference
739 * @return
740 * <br>Not null
741 * <br>New
743 public static final JSplitPane horizontalSplit(final Component leftComponent, final Component rightComponent) {
744 checkAWT();
746 return new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftComponent, rightComponent);
750 * @param leftComponent
751 * <br>Maybe null
752 * <br>Will become reference
753 * @param rightComponent
754 * <br>Maybe null
755 * <br>Will become reference
756 * @return
757 * <br>Not null
758 * <br>New
760 public static final JSplitPane verticalSplit(final Component leftComponent, final Component rightComponent) {
761 checkAWT();
763 return new JSplitPane(JSplitPane.VERTICAL_SPLIT, leftComponent, rightComponent);
767 * The methods in this class create localized menu elements using {@link Messages#translate(java.lang.Object, java.lang.Object[])}.
769 * @author codistmonk (creation 2012-04-15)
771 public static final class I18N {
774 * @throws IllegalInstantiationException To prevent instantiation
776 private I18N() {
777 throw new IllegalInstantiationException();
781 * @param translationKey
782 * <br>Not null
783 * <br>Will become reference
784 * @param items
785 * <br>Not null
786 * @return
787 * <br>Not null
788 * <br>New
790 public static final JMenu menu(final String translationKey, final JMenuItem... items) {
791 return translate(SwingTools.menu(translationKey, items));
795 * @param translationKey
796 * <br>Not null
797 * <br>Will become reference
798 * @param objectOrClass
799 * <br>Not null
800 * <br>Will become reference
801 * @param methodName
802 * <br>Not null
803 * <br>Will become reference
804 * @param arguments
805 * <br>Not null
806 * <br>Will become reference
807 * @return
808 * <br>Not null
809 * <br>New
811 public static final JMenuItem item(final String translationKey,
812 final Object objectOrClass, final String methodName, final Object... arguments) {
813 return translate(new JMenuItem(
814 action(objectOrClass, methodName, arguments)
815 .setName(translationKey)));
819 * @param translationKey
820 * <br>Not null
821 * <br>Will become reference
822 * @param shortcut
823 * <br>Not null
824 * <br>Will become reference
825 * @param objectOrClass
826 * <br>Not null
827 * <br>Will become reference
828 * @param methodName
829 * <br>Not null
830 * <br>Will become reference
831 * @param arguments
832 * <br>Not null
833 * <br>Will become reference
834 * @return
835 * <br>Not null
836 * <br>New
838 public static final JMenuItem item(final String translationKey, final KeyStroke shortcut,
839 final Object objectOrClass, final String methodName, final Object... arguments) {
840 return translate(new JMenuItem(
841 action(objectOrClass, methodName, arguments)
842 .setName(translationKey)
843 .setShortcut(shortcut)));