/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 1999-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.gui.swing; import java.util.Objects; import java.awt.*; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.*; import org.jdesktop.swingx.JXLabel; import org.jdesktop.swingx.JXTitledSeparator; import org.opengis.util.InternationalString; import org.geotoolkit.process.ProgressController; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.internal.swing.SwingUtilities; import org.geotoolkit.internal.swing.ExceptionMonitor; import org.apache.sis.util.Disposable; /** * Reports progress of a lengthly operation in a window. This implementation can also formats * warnings. Its method can be invoked from any thread (it doesn't need to be the <cite>Swing</cite> * thread), which make it easier to use it from some background thread. Such background thread * should have a low priority in order to avoid delaying Swing repaint events. * * <p> </p> * <p align="center"><img src="doc-files/ProgressWindow.png"></p> * <p> </p> * * @author Martin Desruisseaux (MPO, IRD, Geomatys) * @author Guilhem Legal (Geomatys) * @version 3.20 * * @since 1.0 * @module */ public class ProgressWindow extends ProgressController implements Disposable { /** * Initial width for the progress window, in pixels. */ private static final int WIDTH = 360; /** * Initial height for the progress window, in pixels. * Increase this value if some component (e.g. the "Cancel" button) seems truncated. * The current value has been tested for Metal look and feel. */ private static final int HEIGHT = 140; /** * The height of the text area containing the warning messages (if any). */ private static final int WARNING_HEIGHT = 120; /** * Horizontal margin width, in pixels. */ private static final int HMARGIN = 12; /** * Vertical margin height, in pixels. */ private static final int VMARGIN = 9; /** * The progress window as a {@link JDialog} or a {@link JInternalFrame}, * depending of the parent component. */ private final Component window; /** * The container where to add components like the progress bar. */ private final JComponent content; /** * The progress bar. Values ranges from 0 to 100. */ private final JProgressBar progressBar; /** * A description of the undergoing operation. Examples: "Reading header", * "Reading data", <i>etc.</i> */ private final JLabel description; /** * The cancel button. */ private final JButton cancel; /** * Component where to display warnings. */ private JComponent warningArea; /** * The row number where to insert the next warning message. */ private int nextWarningRow; /** * The source of the last warning message. Used in order to avoid to repeat the source * for all subsequent warning messages, if the source didn't changed. */ private String lastSource; /** * Creates a window for reporting progress. The window will not appears immediately. * It will appears only when the {@link #started} method will be invoked. * * @param parent The parent component, or {@code null} if none. */ public ProgressWindow(final Component parent) { /* * Creates the window containing the components. */ Dimension parentSize; final Vocabulary resources = Vocabulary.getResources(parent!=null ? parent.getLocale() : null); final String title = resources.getString(Vocabulary.Keys.Progression); final JDesktopPane desktop = JOptionPane.getDesktopPaneForComponent(parent); if (desktop != null) { final JInternalFrame frame; frame = new JInternalFrame(title); window = frame; content = new JPanel(); // For having an opaque background. parentSize = desktop.getSize(); frame.setContentPane(content); frame.setDefaultCloseOperation(JInternalFrame.HIDE_ON_CLOSE); desktop.add(frame, JLayeredPane.PALETTE_LAYER); } else { final Frame frame; final JDialog dialog; frame = JOptionPane.getFrameForComponent(parent); dialog = new JDialog(frame, title); window = dialog; content = (JComponent) dialog.getContentPane(); parentSize = frame.getSize(); if (parentSize.width == 0 || parentSize.height == 0) { parentSize = Toolkit.getDefaultToolkit().getScreenSize(); } dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); dialog.setResizable(false); dialog.setModal(true); } window.setBounds((parentSize.width-WIDTH)/2, (parentSize.height-HEIGHT)/2, WIDTH, HEIGHT); /* * Creates the label that is going to display the undergoing operation. * This label is initially empty. */ description = new JLabel(); description.setHorizontalAlignment(JLabel.CENTER); /* * Creates the progress bar. */ progressBar = new JProgressBar(); progressBar.setIndeterminate(true); progressBar.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(6,9,6,9), progressBar.getBorder())); /* * Creates the cancel button. */ cancel = new JButton(resources.getString(Vocabulary.Keys.Cancel)); cancel.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { cancel(); } }); final Box cancelBox = Box.createHorizontalBox(); cancelBox.add(Box.createGlue()); cancelBox.add(cancel); cancelBox.add(Box.createGlue()); cancelBox.setBorder(BorderFactory.createEmptyBorder(0, 0, 6, 0)); /* * Layout the elements inside the window. An empty border is created in * order to put some space between the window content and the window border. */ final JPanel panel = new JPanel(new GridLayout(2,1)); panel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEmptyBorder(VMARGIN, HMARGIN, VMARGIN, HMARGIN), BorderFactory.createEtchedBorder())); panel.add(description); panel.add(progressBar); content.setLayout(new BorderLayout()); content.add(panel, BorderLayout.NORTH); content.add(cancelBox, BorderLayout.SOUTH); } /** * Returns a localized string for the specified key. */ private String getString(final short key) { return Vocabulary.getResources(window.getLocale()).getString(key); } /** * Sets the window title. A {@code null} value resets the default title. * * @param name The new window title. */ public void setTitle(String name) { if (name == null) { name = getString(Vocabulary.Keys.Progression); } set(Caller.TITLE, name); } /** * Returns the window title. The default title is "Progress" localized in current locale. * * @return The current window title. */ public String getTitle() { return (String) get(Caller.TITLE); } /** * {@inheritDoc} * * @since 2.3 */ @Override public void setTask(CharSequence task) { super.setTask(task); if (task instanceof InternationalString) { task = ((InternationalString) task).toString(getLocale()); } set(Caller.LABEL, task); } /** * Notifies that the operation begins. This method display the windows if it was * not already visible. */ @Override public void started() { call(Caller.STARTED); } /** * Notifies that the operation is suspended. This method sets the progress bar in * an {@linkplain JProgressBar#setIndeterminate(boolean) indeterminated} state. */ @Override public void paused() { call(Caller.PAUSED); } /** * Notifies that the operation has been resumed. This method stops the progress bar * {@linkplain JProgressBar#setIndeterminate(boolean) indeterminated} state. */ @Override public void resumed() { call(Caller.RESUMED); } /** * {@inheritDoc} */ @Override public void setProgress(final float percent) { super.setProgress(percent); final int p = Math.max(0, Math.min(100, (int) percent)); set(Caller.PROGRESS, Integer.valueOf(p)); } /** * Displays a warning message under the progress bar. The text area for warning messages * will appears only the first time this method is invoked. * * @param source * Name of the warning source, or {@code null} if none. This is typically the * filename in process of being parsed or the URL of the data being processed. * @param location * Text to write on the left side of the warning message, or {@code null} if none. * This is typically the line number where the error occurred in the {@code source} * file or the feature ID of the feature that produced the message. * @param warning * The warning message. */ @Override public synchronized void warningOccurred(final String source, String location, final String warning) { set(Caller.WARNING, new String[] {source, location, warning}); } /** * Displays an exception stack trace. * * @param exception The exception to report. */ @Override public void exceptionOccurred(final Throwable exception) { ExceptionMonitor.show(window, exception); } /** * Notifies that the operation has finished. The window will disappears, except * if it contains warning or exception stack traces. */ @Override public void completed() { call(Caller.COMPLETE); } /** * Releases any resource holds by this window. Invoking this method destroy the window. */ @Override public void dispose() { call(Caller.DISPOSE); } /** * Returns the string {@code margin} without the parenthesis (if any). */ private static String trim(String margin) { margin = margin.trim(); int lower = 0; int upper = margin.length(); while (lower<upper && margin.charAt(lower+0) == '(') lower++; while (lower<upper && margin.charAt(upper-1) == ')') upper--; return margin.substring(lower, upper); } /** * Queries one of the components in the progress window. This method * doesn't need to be invoked from the <cite>Swing</cite> thread. * * @param task The desired value as one of the {@link Caller#TITLE} * or {@link Caller#LABEL} constants. * @return The value. */ private Object get(final int task) { final Caller caller = new Caller(-task); SwingUtilities.invokeAndWait(caller); return caller.value; } /** * Sets the state of one of the components in the progress window. * This method doesn't need to be invoked from the <cite>Swing</cite> thread. * * @param task The value to change as one of the {@link Caller#TITLE} * or {@link Caller#LABEL} constants. * @param value The new value. */ private void set(final int task, final Object value) { final Caller caller = new Caller(task); caller.value = value; EventQueue.invokeLater(caller); } /** * Invokes a <cite>Swing</cite> method without arguments. * * @param task The method to invoke: {@link Caller#STARTED} or {@link Caller#DISPOSE}. */ private void call(final int task) { EventQueue.invokeLater(new Caller(task)); } /** * Task to run in the <cite>Swing</cite> thread. Tasks are identified by a numeric * constant. The {@code get} operations have negative identifiers and are executed * by the {@link EventQueue#invokeAndWait} method. The {@code set} operations have * positive identifiers and are executed by the {@link EventQueue#invokeLater} method. * * @author Martin Desruisseaux (MPO, IRD) * @version 3.00 * * @since 2.0 * @module */ private class Caller implements Runnable { /** For getting or setting the window title. */ static final int TITLE = 1; /** For getting or setting the progress label. */ static final int LABEL = 2; /** For getting or setting the progress bar value. */ static final int PROGRESS = 3; /** For adding a warning message. */ static final int WARNING = 4; /** Notify that an action started. */ static final int STARTED = 5; /** Notify that the process is paused. */ static final int PAUSED = 6; /** Notify that the process is resumed. */ static final int RESUMED = 7; /** Notify that an action is completed. */ static final int COMPLETE = 8; /** Notify that the window can be disposed. */ static final int DISPOSE = 9; /** * The task to execute, as one of the {@link #TITLE}, {@link #LABEL}, <i>etc.</i> * constants or their negative counterpart. */ private final int task; /** * The value to get (negative value {@link #task}) or set (positive value {@link #task}). */ public Object value; /** * Creates an action. {@code task} must be one of {@link #TITLE}, {@link #LABEL} * <i>etc.</i> constants or their negative counterpart. */ public Caller(final int task) { this.task = task; } /** * Run the task. */ @Override public void run() { final BoundedRangeModel model = progressBar.getModel(); switch (task) { case -LABEL: { value = description.getText(); return; } case +LABEL: { description.setText(value.toString()); return; } case PROGRESS: { model.setValue(((Integer) value).intValue()); progressBar.setIndeterminate(false); return; } case STARTED: { model.setRangeProperties(0, 1, 0, 100, false); if (window instanceof Window) { ((Window) window).setLocationRelativeTo(window.getParent()); } window.setVisible(true); break; // Need further action below. } case PAUSED: { progressBar.setIndeterminate(true); return; } case RESUMED: { progressBar.setIndeterminate(false); return; } case COMPLETE: { progressBar.setIndeterminate(false); model.setRangeProperties(100, 1, 0, 100, false); window.setVisible(warningArea != null); cancel.setEnabled(false); break; // Need further action below. } } /* * Some of the tasks above requires an action on the window, which may be a JDialog or * a JInternalFrame. We need to determine the window type before to apply the action. */ synchronized (ProgressWindow.this) { if (window instanceof JDialog) { final JDialog window = (JDialog) ProgressWindow.this.window; switch (task) { case -TITLE: { value = window.getTitle(); return; } case +TITLE: { window.setTitle((String) value); return; } case STARTED: { window.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); return; } case COMPLETE: { window.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE); return; } case DISPOSE: { window.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); if (warningArea==null || !window.isVisible()) { window.dispose(); } return; } } } else { final JInternalFrame window = (JInternalFrame) ProgressWindow.this.window; switch (task) { case -TITLE: { value = window.getTitle(); return; } case +TITLE: { window.setTitle((String) value); return; } case STARTED: { window.setClosable(false); return; } case COMPLETE: { window.setClosable(true); return; } case DISPOSE: { window.setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE); if (warningArea==null || !window.isVisible()) { window.dispose(); } return; } } } if (task != WARNING) { throw new AssertionError(task); // Should never happen. } /* * If the task to execute is not one of the above, we will assume that this * is the WARNING task. If this is the first time we run this task, creates * the panel which will contains the warnings. */ JComponent warningArea = ProgressWindow.this.warningArea; if (warningArea == null) { warningArea = new JPanel(new GridBagLayout()); final JScrollPane scroll = new JScrollPane(warningArea); final JPanel namedArea = new JPanel(new BorderLayout()); ProgressWindow.this.warningArea = warningArea; namedArea.setBorder(BorderFactory.createEmptyBorder(0, HMARGIN, VMARGIN, HMARGIN)); namedArea.add(new JLabel(getString(Vocabulary.Keys.Warning)), BorderLayout.NORTH); namedArea.add(scroll, BorderLayout.CENTER); content.add(namedArea, BorderLayout.CENTER); if (window instanceof JDialog) { final JDialog window = (JDialog) ProgressWindow.this.window; window.setResizable(true); } else { final JInternalFrame window = (JInternalFrame) ProgressWindow.this.window; window.setResizable(true); } window.setSize(WIDTH, HEIGHT+WARNING_HEIGHT); window.setVisible(true); // Seems required in order to force relayout. } /* * Now formats the warning message as 3 new labels. */ final GridBagConstraints c = new GridBagConstraints(); c.weightx = 1; c.gridx = 0; c.gridy = nextWarningRow; c.fill = GridBagConstraints.HORIZONTAL; c.insets.top = 3; final String[] values = (String[]) value; String source = values[0]; if (!Objects.equals(source, lastSource)) { lastSource = source; if (source == null) { source = getString(Vocabulary.Keys.Untitled); } c.gridwidth = 2; c.insets.top += VMARGIN; JXTitledSeparator title = new JXTitledSeparator(source); title.setFont(Font.decode("Dialog-bolditalic-13")); warningArea.add(title, c); c.insets.top = 3; } c.gridy++; c.gridwidth = 1; String location = values[1]; if (location != null) { location = trim(location); if (!location.isEmpty()) { c.weightx = 0; warningArea.add(new JLabel('(' + location + ')'), c); c.weightx = 1; } } c.gridx = 1; c.insets.left = HMARGIN; final JXLabel label = new JXLabel(values[2]); label.setLineWrap(true); warningArea.add(label, c); warningArea.getParent().validate(); // Validates the JScrollPane. nextWarningRow += 2; } } } }