/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2001-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.internal.swing; import java.awt.*; import javax.swing.*; import java.util.Arrays; import java.util.Locale; import java.awt.event.ActionListener; import java.awt.event.WindowListener; import javax.swing.table.TableColumn; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.DefaultTableCellRenderer; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.UndeclaredThrowableException; import org.geotoolkit.lang.Debug; import org.geotoolkit.lang.Static; import org.geotoolkit.internal.Threads; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.gui.swing.WindowCreator; /** * A collection of utility methods for Swing. Every {@code show*} methods delegate * their work to the corresponding method in {@link JOptionPane}, with two differences: * <p> * <ul> * <li>{@code SwingUtilities}'s method may be invoked from any thread. If they * are invoked from a non-Swing thread, execution will be delegate to the Swing * thread and the calling thread will block until completion.</li> * <li>If a parent component is a {@link JDesktopPane}, dialogs will be rendered as * internal frames instead of frames.</li> * </ul> * * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.12 * * @since 3.12 (derived from 2.0) * @module */ public final class SwingUtilities extends Static { /** * The thread group for Swing background tasks. * * @since 3.19 */ public static final ThreadGroup THREAD_GROUP = new ThreadGroup(Threads.GEOTOOLKIT, "Swing"); /** * Do not allow any instance of this class to be created. */ private SwingUtilities() { } /** * Shows the given component in a {@link JFrame}. * This is used (indirectly) mostly for debugging purpose. * * @param panel The panel to show. * @param title The frame title. */ @Debug public static void show(final JComponent panel, final String title) { final JFrame frame = new JFrame(title); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.add(panel); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); } /** * Brings up a "Ok/Cancel" dialog with no icon, using the installed window handler. * In the default configuration, this will result in a call to the * {@link #showOptionDialog(Component, Object, String) method just below. * * @param owner The parent component. Dialog will apears on top of this owner. * @param dialog The dialog content to show. * @param title The title string for the dialog. * @return {@code true} if user clicked "Ok", {@code false} otherwise. */ public static boolean showDialog(final Component owner, final Component dialog, final String title) { final WindowCreator.Handler handler; if (owner instanceof WindowCreator) { handler = ((WindowCreator) owner).getWindowHandler(); } else { handler = WindowCreator.getDefaultWindowHandler(); } return handler.showDialog(owner, dialog, title); } /** * Brings up a "Ok/Cancel" dialog with no icon. This method can be invoked * from any thread and blocks until the user click on "Ok" or "Cancel". * * @param owner The parent component. Dialog will apears on top of this owner. * @param dialog The dialog content to show. * @param title The title string for the dialog. * @return {@code true} if user clicked "Ok", {@code false} otherwise. */ public static boolean showOptionDialog(final Component owner, final Object dialog, final String title) { return showOptionDialog(owner, dialog, title, null); } /** * Brings up a "Ok/Cancel/Reset" dialog with no icon. This method can be invoked * from any thread and blocks until the user click on "Ok" or "Cancel". * * @param owner The parent component. Dialog will apears on top of this owner. * @param dialog The dialog content to show. * @param title The title string for the dialog. * @param reset Action to execute when user press "Reset", or {@code null} * if there is no "Reset" button. If {@code reset} is an * instance of {@link Action}, the button label will be set * according the action's properties. * @return {@code true} if user clicked "Ok", {@code false} otherwise. */ public static boolean showOptionDialog(final Component owner, final Object dialog, final String title, final ActionListener reset) { /* * Delegates to Swing thread if this method is invoked from an other thread. */ if (!EventQueue.isDispatchThread()) { final boolean[] result = new boolean[1]; invokeAndWait(new Runnable() { @Override public void run() { result[0] = showOptionDialog(owner, dialog, title, reset); } }); return result[0]; } /* * Constructs the buttons bar. */ Object[] options = null; Object initialValue = null; int okChoice = JOptionPane.OK_OPTION; if (reset != null) { final Vocabulary resources = Vocabulary.getResources(owner!=null ? owner.getLocale() : null); final JButton button; if (reset instanceof Action) { button = new JButton((Action)reset); } else { button = new JButton(resources.getString(Vocabulary.Keys.Reset)); button.addActionListener(reset); } options = new Object[] { resources.getString(Vocabulary.Keys.Ok), resources.getString(Vocabulary.Keys.Cancel), button }; initialValue = options[okChoice=0]; } /* * Brings ups the dialog box. */ final int choice; if (JOptionPane.getDesktopPaneForComponent(owner)!=null) { choice = JOptionPane.showInternalOptionDialog( owner, // Composante parente dialog, // Message title, // Titre de la boîte de dialogue JOptionPane.OK_CANCEL_OPTION, // Boutons à placer JOptionPane.PLAIN_MESSAGE, // Type du message null, // Icone options, // Liste des boutons initialValue); // Bouton par défaut } else { choice = JOptionPane.showOptionDialog( owner, // Composante parente dialog, // Message title, // Titre de la boîte de dialogue JOptionPane.OK_CANCEL_OPTION, // Boutons à placer JOptionPane.PLAIN_MESSAGE, // Type du message null, // Icone options, // Liste des boutons initialValue); // Bouton par défaut } return choice == okChoice; } /** * Brings up a message dialog with a "Ok" button. This method can be invoked * from any thread and blocks until the user click on "Ok". * * @param owner The parent component. Dialog will apears on top of this owner. * @param message The dialog content to show. * @param title The title string for the dialog. * @param type The message type * ({@link JOptionPane#ERROR_MESSAGE}, * {@link JOptionPane#INFORMATION_MESSAGE}, * {@link JOptionPane#WARNING_MESSAGE}, * {@link JOptionPane#QUESTION_MESSAGE} or * {@link JOptionPane#PLAIN_MESSAGE}). */ public static void showMessageDialog(final Component owner, final Object message, final String title, final int type) { if (!EventQueue.isDispatchThread()) { invokeAndWait(new Runnable() { @Override public void run() { showMessageDialog(owner, message, title, type); } }); return; } if (JOptionPane.getDesktopPaneForComponent(owner)!=null) { JOptionPane.showInternalMessageDialog( owner, // Composante parente message, // Message title, // Titre de la boîte de dialogue type); // Type du message } else { JOptionPane.showMessageDialog( owner, // Composante parente message, // Message title, // Titre de la boîte de dialogue type); // Type du message } } /** * Brings up a confirmation dialog with "Yes/No" buttons. This method can be * invoked from any thread and blocks until the user click on "Yes" or "No". * * @param owner The parent component. Dialog will apears on top of this owner. * @param message The dialog content to show. * @param title The title string for the dialog. * @param type The message type * ({@link JOptionPane#ERROR_MESSAGE}, * {@link JOptionPane#INFORMATION_MESSAGE}, * {@link JOptionPane#WARNING_MESSAGE}, * {@link JOptionPane#QUESTION_MESSAGE} or * {@link JOptionPane#PLAIN_MESSAGE}). * @return {@code true} if user clicked on "Yes", {@code false} otherwise. */ public static boolean showConfirmDialog(final Component owner, final Object message, final String title, final int type) { if (!EventQueue.isDispatchThread()) { final boolean[] result = new boolean[1]; invokeAndWait(new Runnable() { @Override public void run() { result[0] = showConfirmDialog(owner, message, title, type); } }); return result[0]; } final int choice; if (JOptionPane.getDesktopPaneForComponent(owner)!=null) { choice = JOptionPane.showInternalConfirmDialog( owner, // Composante parente message, // Message title, // Titre de la boîte de dialogue JOptionPane.YES_NO_OPTION, // Boutons à faire apparaître type); // Type du message } else { choice = JOptionPane.showConfirmDialog( owner, // Composante parente message, // Message title, // Titre de la boîte de dialogue JOptionPane.YES_NO_OPTION, // Boutons à faire apparaître type); // Type du message } return choice == JOptionPane.YES_OPTION; } /** * Setups the given table for usage as row-header. This method setups the background color to * the same one than the column headers. * * {@note In a previous version, we were assigning to the row headers the same cell renderer than * the one created by <cite>Swing</cite> for the column headers. But it produced strange * effects when the L&F uses a vertical grandiant instead than a uniform color.} * * @param table The table to setup as row headers. * @return The renderer which has been assigned to the table. */ public static TableCellRenderer setupAsRowHeader(final JTable table) { final JTableHeader header = table.getTableHeader(); Color background = header.getBackground(); Color foreground = header.getForeground(); if (background == null || background.equals(table.getBackground())) { if (!SystemColor.control.equals(background)) { background = SystemColor.control; foreground = SystemColor.controlText; } else { final Locale locale = table.getLocale(); background = UIManager.getColor("Label.background", locale); foreground = UIManager.getColor("Label.foreground", locale); } } final DefaultTableCellRenderer renderer = new DefaultTableCellRenderer(); renderer.setBackground(background); renderer.setForeground(foreground); renderer.setHorizontalAlignment(DefaultTableCellRenderer.RIGHT); final TableColumn column = table.getColumnModel().getColumn(0); column.setCellRenderer(renderer); column.setPreferredWidth(60); table.setPreferredScrollableViewportSize(table.getPreferredSize()); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setCellSelectionEnabled(false); return renderer; } /** * Removes the given elements from the given list. This method tries to use * {@link DefaultListModel#removeRange} when possible in order to group events * together. * <p> * <strong>Warning:</strong> This method override the given {@code indices} array. * * @param list The list from which to remove elements. * @param indices The index of elements to remove. */ public static void remove(final DefaultListModel<?> list, final int[] indices) { // We must iterate in reverse order, because the // index after the removed elements will change. int i = indices.length; if (i != 0) { Arrays.sort(indices); int upper = indices[--i]; int lower = upper; while (i != 0) { int previous = indices[--i]; if (previous != lower - 1) { if (lower == upper) { list.remove(lower); } else { list.removeRange(lower, upper); } upper = previous; } lower = previous; } if (lower == upper) { list.remove(lower); } else { list.removeRange(lower, upper); } } } /** * Adds the given listener to the first window ancestor found in the hierarchy. * If an {@link JInternalFrame} is found in the hierarchy, it the listener will * be wrapped in an adapter before to be given to that internal frame. * * @param component The component for which to search for a window ancestor. * @param listener The listener to register. */ public static void addWindowListener(Component component, final WindowListener listener) { while (component != null) { if (component instanceof org.geotoolkit.gui.swing.Window) { ((org.geotoolkit.gui.swing.Window) component).addWindowListener(listener); break; } if (component instanceof Window) { ((Window) component).addWindowListener(listener); break; } if (component instanceof JInternalFrame) { ((JInternalFrame) component).addInternalFrameListener(InternalWindowListener.wrap(listener)); break; } component = component.getParent(); } } /** * Removes the given listener from the first window ancestor found in the hierarchy. * This method is the converse of {@code addWindowListener(listener, component)}. * * @param component The component for which to search for a window ancestor. * @param listener The listener to unregister. */ public static void removeWindowListener(Component component, final WindowListener listener) { while (component != null) { if (component instanceof org.geotoolkit.gui.swing.Window) { ((org.geotoolkit.gui.swing.Window) component).removeWindowListener(listener); break; } if (component instanceof Window) { ((Window) component).removeWindowListener(listener); break; } if (component instanceof JInternalFrame) { InternalWindowListener.removeWindowListener((JInternalFrame) component, listener); break; } component = component.getParent(); } } /** * Causes runnable to have its run method called in the dispatch thread of * the event queue. This will happen after all pending events are processed. * The call blocks until this has happened. * * @param runnable The task to run in the dispath thread. */ public static void invokeAndWait(final Runnable runnable) { if (EventQueue.isDispatchThread()) { runnable.run(); } else { try { EventQueue.invokeAndWait(runnable); } catch (InterruptedException exception) { // Someone don't want to let us sleep. Go back to work. } catch (InvocationTargetException target) { final Throwable exception = target.getTargetException(); if (exception instanceof RuntimeException) { throw (RuntimeException) exception; } if (exception instanceof Error) { throw (Error) exception; } // Should not happen, since {@link Runnable#run} do not allow checked exception. throw new UndeclaredThrowableException(exception, exception.getLocalizedMessage()); } } } }