/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-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.test.gui;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import java.util.Locale;
import java.util.prefs.Preferences;
import java.util.concurrent.CountDownLatch;
import java.awt.Desktop;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.HeadlessException;
import java.awt.event.ActionEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowAdapter;
import java.awt.image.BufferedImage;
import java.beans.PropertyVetoException;
import javax.swing.*;
import javax.swing.event.InternalFrameEvent;
import javax.swing.event.InternalFrameAdapter;
import static java.lang.StrictMath.*;
import static org.junit.Assert.*;
/**
* The desktop pane where to put the widgets to be tested.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.20
*
* @since 3.05
*/
@SuppressWarnings("serial")
final strictfp class DesktopPane extends JDesktopPane {
/**
* The key for screenshot directory in the user preferences.
*/
private static final String SCREENSHOT_DIRECTORY_PREFS = "Screenshots";
/**
* The desktop which contain the internal frame for each widget. Will be created only if
* the "{@code org.geotoolkit.showWidgetTests}" system property is set to {@code true}.
*/
private static DesktopPane desktop;
/**
* The menu for creating new windows.
*/
private final JMenu newMenu;
/**
* A lock used for waiting that the {@linkplain #desktop} has been closed.
*/
private final CountDownLatch lock;
/**
* The last active component.
*/
private JComponent active;
/**
* Creates the desktop.
*/
private DesktopPane() {
newMenu = new JMenu("New");
lock = new CountDownLatch(1);
}
/**
* If the widgets are to be show, prepares the desktop pane which will contain them.
* This method is invoked from JUnit test cases by methods annotated with {@code @BeforeTest}.
*
* @throws HeadlessException If the current environment does not allow the display of widgets.
*/
static synchronized void prepareDesktop() throws HeadlessException {
desktop = new DesktopPane();
desktop.createFrame().setVisible(true);
}
/**
* If a frame has been created, wait for its disposal.
* This method is invoked from JUnit test cases by methods annotated with {@code @AfterTest}.
*
* @throws InterruptedException If the current thread has been interrupted while
* we were waiting for the frame disposal.
*/
static void waitForFrameDisposal() throws InterruptedException {
final DesktopPane desktop;
synchronized (SwingTestBase.class) {
desktop = DesktopPane.desktop;
}
if (desktop != null) {
desktop.lock.await();
synchronized (SwingTestBase.class) {
DesktopPane.desktop = null;
}
}
}
/**
* Shows the given component, if the test is allowed to display widgets and
* the given component is not null.
*
* @param component The component to show, or {@code null} if none.
* @return {@code true} if the component has been shown.
*/
static synchronized boolean show(final JComponent component) {
boolean added = false;
if (desktop != null && component != null) {
desktop.show(component, 0, 1);
added = true;
}
return added;
}
/**
* Shows the given components, if the test is allowed to display widgets and
* the given component is not null.
*
* @param testCase The test case for which the component is added.
* @param components The components to show, or {@code null} if none.
* @return {@code true} if the component has been shown.
*/
static synchronized boolean show(final SwingTestBase<?> testCase, final JComponent... components) {
boolean added = false;
if (desktop != null) {
desktop.addTestCase(testCase);
for (int i=0; i<components.length; i++) {
final JComponent component = components[i];
if (component != null) {
desktop.show(component, i, components.length);
added = true;
}
}
}
return added;
}
/**
* Adds a test case to be show in the "New" menu.
* A {@code null} argument cause the addition of a separator.
*/
private void addTestCase(final SwingTestBase<?> testCase) {
if (testCase != null) {
newMenu.add(new AbstractAction(getTitle(testCase.testing)) {
@Override public void actionPerformed(final ActionEvent event) {
show(testCase);
}
});
} else {
newMenu.addSeparator();
}
}
/**
* Creates the frame for this desktop. This frame is initially invisible;
* the {@link JFrame#setVisible(boolean)} method must be invoked by the caller.
* This method shall be invoked only once.
*
* @return A new frame in which the desktop will be shown.
*/
private JFrame createFrame() {
final JMenuBar menuBar = new JMenuBar();
menuBar.add(newMenu);
if (true) {
final JMenu menu = new JMenu("View");
if (true) {
final JMenu sub = new JMenu("L&F");
final ButtonGroup group = new ButtonGroup();
final String current = UIManager.getLookAndFeel().getName();
for (final UIManager.LookAndFeelInfo lf : UIManager.getInstalledLookAndFeels()) {
final String cn = lf.getClassName();
final String name = lf.getName();
final JRadioButtonMenuItem item = new JRadioButtonMenuItem(new AbstractAction(name) {
@Override public void actionPerformed(final ActionEvent event) {
try {
UIManager.setLookAndFeel(cn);
SwingUtilities.updateComponentTreeUI(DesktopPane.this);
} catch (ReflectiveOperationException | UnsupportedLookAndFeelException e) {
warning(e);
}
}
});
item.setSelected(name.equals(current));
group.add(item);
sub.add(item);
}
menu.add(sub);
}
if (false) { // Doesn't seem to work...
final JMenu sub = new JMenu("Language");
final Locale[] locales = new Locale[] {
Locale.CANADA,
Locale.FRANCE
};
final String current = Locale.getDefault(Locale.Category.DISPLAY).getDisplayLanguage();
final ButtonGroup group = new ButtonGroup();
for (final Locale locale : locales) {
final String name = locale.getDisplayLanguage();
final JRadioButtonMenuItem item = new JRadioButtonMenuItem(new AbstractAction(name) {
@Override public void actionPerformed(final ActionEvent event) {
setLocale(locale);
}
});
item.setSelected(name.equals(current));
group.add(item);
sub.add(item);
}
menu.add(sub);
}
menuBar.add(menu);
}
if (true) {
final JMenu menu = new JMenu("Tools");
menu.add(new AbstractAction("Screenshot") {
@Override public void actionPerformed(final ActionEvent event) {
screenshot();
}
});
menu.add(new AbstractAction("Preferences") {
@Override public void actionPerformed(final ActionEvent event) {
preferences();
}
});
menuBar.add(menu);
}
if (true) {
final JMenu menu = new JMenu("Windows");
menu.add(new AbstractAction("List") {
@Override public void actionPerformed(final ActionEvent event) {
listWindows();
}
});
menuBar.add(menu);
}
final JFrame frame = new JFrame("Geotoolkit.org widget tests");
frame.addWindowListener(new WindowAdapter() {
@Override public void windowClosed(final WindowEvent event) {
frame.removeWindowListener(this);
lock.countDown();
frame.dispose();
}
});
frame.setJMenuBar(menuBar);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setContentPane(this);
frame.setSize(1000, 600);
frame.setLocationRelativeTo(null); // Put at screen center.
return frame;
}
/**
* Returns the title to use for the widget frame.
* This is also the filename of the screenshot.
*
* @param type The widget class.
* @return The frame title, or screenshot filename (without extension).
*/
static String getTitle(Class<?> type) {
while (type.isAnonymousClass()) {
type = type.getSuperclass();
}
return type.getSimpleName();
}
/**
* Shows the widget created by the given test case.
*/
private void show(final SwingTestBase<?> testCase) {
try {
for (int i=0; i<testCase.numTests; i++) {
show(testCase.create(i), i, testCase.numTests);
}
} catch (Exception e) {
warning(e);
}
}
/**
* Show a warning dialog box for the given exception.
*/
private void warning(final Exception e) {
JOptionPane.showInternalMessageDialog(this, e.getLocalizedMessage(),
e.getClass().getSimpleName(), JOptionPane.ERROR_MESSAGE);
}
/**
* Shows the given component in a frame.
*
* @param component The component to show.
*/
private void show(final JComponent component, final int index, final int numTests) {
String title = getTitle(component.getClass());
if (numTests != 1) {
title = title + " (" + index + ')';
}
final JInternalFrame frame = new JInternalFrame(title, true, true, true, true);
frame.addInternalFrameListener(new InternalFrameAdapter() {
@Override public void internalFrameActivated(final InternalFrameEvent event) {
active = component;
}
@Override public void internalFrameClosed(final InternalFrameEvent event) {
if (active == component) {
active = null;
}
}
});
frame.add(component);
frame.pack();
final Dimension size = frame.getMinimumSize();
if (size != null) {
frame.setSize(max(frame.getWidth(), size.width),
max(frame.getHeight(), size.height));
}
final int numCols = (int) ceil(sqrt(numTests));
final int numRows = (numTests + numCols - 1) / numCols;
final int deltaX = getWidth() / numCols;
final int deltaY = getHeight() / numRows;
frame.setLocation(deltaX * (index % numRows) + (deltaX - frame.getWidth()) / 2,
deltaY * (index / numRows) + (deltaY - frame.getHeight()) / 2);
frame.setVisible(true);
add(frame);
try {
frame.setSelected(true);
} catch (PropertyVetoException e) {
warning(e); // Should never happen, but is not critical anyway.
}
System.out.println("Showing " + title);
}
/**
* List windows known to this desktop.
*/
private void listWindows() {
final Component[] components = getComponents();
final String[] titles = new String[components.length];
for (int i=0; i<components.length; i++) {
Component c = components[i];
String title = String.valueOf(c.getName());
if (c instanceof JInternalFrame) {
final JInternalFrame ci = (JInternalFrame) c;
title = String.valueOf(ci.getTitle());
c = ci.getRootPane().getComponent(0);
}
final Dimension size = c.getSize();
titles[i] = title + " : " + c.getClass().getSimpleName() +
'[' + size.width + " \u00D7 " + size.height + ']';
}
final JInternalFrame frame = new JInternalFrame("Windows", true, true, true, true);
frame.add(new JScrollPane(new JList<>(titles)));
frame.pack();
frame.setVisible(true);
add(frame);
}
/**
* Popups a dialog box for setting the preferences.
*/
private void preferences() {
final Preferences prefs = Preferences.userNodeForPackage(DesktopPane.class);
final JFileChooser chooser = new JFileChooser(prefs.get(SCREENSHOT_DIRECTORY_PREFS, null));
chooser.setDialogTitle("Output directory for screenshots");
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
switch (chooser.showOpenDialog(this)) {
case JFileChooser.APPROVE_OPTION: {
final File directory = chooser.getSelectedFile();
if (directory != null) {
prefs.put(SCREENSHOT_DIRECTORY_PREFS, directory.getPath());
}
break;
}
}
}
/**
* Takes a screenshot of the currently active component.
*/
private void screenshot() {
final JComponent active = this.active;
if (active != null && active.isValid()) {
final BufferedImage image = new BufferedImage(active.getWidth(), active.getHeight(), BufferedImage.TYPE_INT_RGB);
final Graphics2D handler = image.createGraphics();
active.print(handler);
handler.dispose();
File file = new File(Preferences.userNodeForPackage(DesktopPane.class).get(SCREENSHOT_DIRECTORY_PREFS, "."));
file = new File(file, getTitle(active.getClass()) + ".png");
try {
assertTrue(ImageIO.write(image, "png", file));
file = file.getParentFile();
Desktop.getDesktop().open(file);
} catch (IOException e) {
JOptionPane.showInternalMessageDialog(active, e.getLocalizedMessage(),
e.getClass().getSimpleName(), JOptionPane.ERROR_MESSAGE);
}
} else {
JOptionPane.showInternalMessageDialog(this, "No active window.", "Screenshot", JOptionPane.WARNING_MESSAGE);
}
}
}