001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: * (C) 1999, Pêches et Océans Canada
007: *
008: * This library is free software; you can redistribute it and/or
009: * modify it under the terms of the GNU Lesser General Public
010: * License as published by the Free Software Foundation; either
011: * version 2.1 of the License, or (at your option) any later version.
012: *
013: * This library is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
016: * Lesser General Public License for more details.
017: */
018: package org.geotools.gui.swing;
019:
020: // J2SE dependencies
021: import java.awt.BorderLayout;
022: import java.awt.Component;
023: import java.awt.Dimension;
024: import java.awt.EventQueue;
025: import java.awt.Font;
026: import java.awt.Frame;
027: import java.awt.GridLayout;
028: import java.awt.Toolkit;
029: import java.awt.event.ActionEvent;
030: import java.awt.event.ActionListener;
031:
032: import javax.swing.BorderFactory;
033: import javax.swing.BoundedRangeModel;
034: import javax.swing.Box;
035: import javax.swing.JButton;
036: import javax.swing.JComponent;
037: import javax.swing.JDesktopPane;
038: import javax.swing.JDialog;
039: import javax.swing.JInternalFrame;
040: import javax.swing.JLabel;
041: import javax.swing.JLayeredPane;
042: import javax.swing.JOptionPane;
043: import javax.swing.JPanel;
044: import javax.swing.JProgressBar;
045: import javax.swing.JScrollPane;
046: import javax.swing.JTextArea;
047:
048: // Geotools dependencies
049: import org.geotools.resources.SwingUtilities;
050: import org.geotools.resources.Utilities;
051: import org.geotools.resources.i18n.Vocabulary;
052: import org.geotools.resources.i18n.VocabularyKeys;
053: import org.geotools.util.ProgressListener;
054: import org.geotools.util.SimpleInternationalString;
055: import org.opengis.util.InternationalString;
056:
057: /**
058: * Reports progress of a lengthly operation in a window. This implementation can also format
059: * warnings. Its method can be invoked from any thread (it doesn't need to be the <cite>Swing</cite>
060: * thread), which make it easier to use it from some background thread. Such background thread
061: * should have a low priority in order to avoid delaying Swing repaint events.
062: *
063: * <p> </p>
064: * <p align="center"><img src="doc-files/ProgressWindow.png"></p>
065: * <p> </p>
066: *
067: * @since 2.0
068: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/ProgressWindow.java $
069: * @version $Id: ProgressWindow.java 22680 2006-11-09 22:32:27Z jgarnett $
070: * @author Martin Desruisseaux
071: */
072: public class ProgressWindow implements ProgressListener {
073: /**
074: * Initial with for the progress window, in pixels.
075: */
076: private static final int WIDTH = 360;
077:
078: /**
079: * Initial height for the progress window, in pixels.
080: * Increase this value is some component (e.g. the "Cancel" button) seems truncated.
081: * The current value has been tested for Metal look and feel.
082: */
083: private static final int HEIGHT = 140;
084:
085: /**
086: * The height of the text area containing the warning messages (if any).
087: */
088: private static final int WARNING_HEIGHT = 120;
089:
090: /**
091: * Horizontal margin width, in pixels.
092: */
093: private static final int HMARGIN = 12;
094:
095: /**
096: * Vertical margin height, in pixels.
097: */
098: private static final int VMARGIN = 9;
099:
100: /**
101: * Amount of spaces to put in the margin of the warning messages window.
102: */
103: private static final int WARNING_MARGIN = 8;
104:
105: /**
106: * The progress window as a {@link JDialog} or a {@link JInternalFrame},
107: * depending of the parent component.
108: */
109: private final Component window;
110:
111: /**
112: * The container where to add components like the progress bar.
113: */
114: private final JComponent content;
115:
116: /**
117: * The progress bar. Values ranges from 0 to 100.
118: */
119: private final JProgressBar progressBar;
120:
121: /**
122: * A description of the undergoing operation. Examples: "Reading header",
123: * "Reading data", <cite>etc.</cite>
124: */
125: private final JLabel description;
126:
127: /**
128: * The cancel button.
129: */
130: private final JButton cancel;
131:
132: /**
133: * Component where to display warnings. The actual component class is {@link JTextArea}.
134: * But we declare {@link JComponent} here in order to avoid class loading before needed.
135: */
136: private JComponent warningArea;
137:
138: /**
139: * The source of the last warning message. Used in order to avoid to repeat the source
140: * for all subsequent warning messages, if the source didn't changed.
141: */
142: private String lastSource;
143:
144: /**
145: * {@code true} if the action has been canceled.
146: */
147: private volatile boolean canceled;
148:
149: /**
150: * Creates a window for reporting progress. The window will not appears immediately.
151: * It will appears only when the {@link #started} method will be invoked.
152: *
153: * @param parent The parent component, or {@code null} if none.
154: */
155: public ProgressWindow(final Component parent) {
156: /*
157: * Creates the window containing the components.
158: */
159: Dimension parentSize;
160: final Vocabulary resources = Vocabulary
161: .getResources(parent != null ? parent.getLocale()
162: : null);
163: final String title = resources
164: .getString(VocabularyKeys.PROGRESSION);
165: final JDesktopPane desktop = JOptionPane
166: .getDesktopPaneForComponent(parent);
167: if (desktop != null) {
168: final JInternalFrame frame;
169: frame = new JInternalFrame(title);
170: window = frame;
171: content = new JPanel(); // Pour avoir un fond opaque
172: parentSize = desktop.getSize();
173: frame.setContentPane(content);
174: frame
175: .setDefaultCloseOperation(JInternalFrame.HIDE_ON_CLOSE);
176: desktop.add(frame, JLayeredPane.PALETTE_LAYER);
177: } else {
178: final Frame frame;
179: final JDialog dialog;
180: frame = JOptionPane.getFrameForComponent(parent);
181: dialog = new JDialog(frame, title);
182: window = dialog;
183: content = (JComponent) dialog.getContentPane();
184: parentSize = frame.getSize();
185: if (parentSize.width == 0 || parentSize.height == 0) {
186: parentSize = Toolkit.getDefaultToolkit()
187: .getScreenSize();
188: }
189: dialog.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
190: dialog.setResizable(false);
191: }
192: window.setBounds((parentSize.width - WIDTH) / 2,
193: (parentSize.height - HEIGHT) / 2, WIDTH, HEIGHT);
194: /*
195: * Creates the label that is going to display the undergoing operation.
196: * This label is initially empty.
197: */
198: description = new JLabel();
199: description.setHorizontalAlignment(JLabel.CENTER);
200: /*
201: * Creates the progress bar.
202: */
203: progressBar = new JProgressBar();
204: progressBar.setIndeterminate(true);
205: progressBar.setBorder(BorderFactory.createCompoundBorder(
206: BorderFactory.createEmptyBorder(6, 9, 6, 9),
207: progressBar.getBorder()));
208: /*
209: * Creates the cancel button.
210: */
211: cancel = new JButton(resources.getString(VocabularyKeys.CANCEL));
212: cancel.addActionListener(new ActionListener() {
213: public void actionPerformed(ActionEvent e) {
214: setCanceled(true);
215: }
216: });
217: final Box cancelBox = Box.createHorizontalBox();
218: cancelBox.add(Box.createGlue());
219: cancelBox.add(cancel);
220: cancelBox.add(Box.createGlue());
221: cancelBox
222: .setBorder(BorderFactory.createEmptyBorder(0, 0, 6, 0));
223: /*
224: * Layout the elements inside the window. An empty border is created in
225: * order to put some space between the window content and the window border.
226: */
227: final JPanel panel = new JPanel(new GridLayout(2, 1));
228: panel.setBorder(BorderFactory.createCompoundBorder(
229: BorderFactory.createEmptyBorder(VMARGIN, HMARGIN,
230: VMARGIN, HMARGIN), BorderFactory
231: .createEtchedBorder()));
232: panel.add(description);
233: panel.add(progressBar);
234: content.setLayout(new BorderLayout());
235: content.add(panel, BorderLayout.NORTH);
236: content.add(cancelBox, BorderLayout.SOUTH);
237: }
238:
239: /**
240: * Returns a localized string for the specified key.
241: */
242: private String getString(final int key) {
243: return Vocabulary.getResources(window.getLocale()).getString(
244: key);
245: }
246:
247: /**
248: * Returns the window title. The default title is "Progress" localized in current locale.
249: */
250: public String getTitle() {
251: return (String) get(Caller.TITLE);
252: }
253:
254: /**
255: * Set the window title. A {@code null} value reset the default title.
256: */
257: public void setTitle(String name) {
258: if (name == null) {
259: name = getString(VocabularyKeys.PROGRESSION);
260: }
261: set(Caller.TITLE, name);
262: }
263:
264: /**
265: * {@inheritDoc}
266: */
267: public String getDescription() {
268: return (String) get(Caller.LABEL);
269: }
270:
271: /**
272: * {@inheritDoc}
273: */
274: public void setDescription(final String description) {
275: set(Caller.LABEL, description);
276: }
277:
278: /**
279: * Notifies that the operation begins. This method display the windows if it was
280: * not already visible.
281: */
282: public void started() {
283: call(Caller.STARTED);
284: }
285:
286: /**
287: * {@inheritDoc}
288: */
289: public void progress(final float percent) {
290: int p = (int) percent; // round toward 0
291: if (p < 0)
292: p = 0;
293: if (p > 100)
294: p = 100;
295: set(Caller.PROGRESS, new Integer(p));
296: }
297:
298: /**
299: * Notifies that the operation has finished. The window will disaspears, except
300: * if it contains warning or exception stack traces.
301: */
302: public void complete() {
303: call(Caller.COMPLETE);
304: }
305:
306: /**
307: * Releases any resource holds by this window. Invoking this method destroy the window.
308: */
309: public void dispose() {
310: call(Caller.DISPOSE);
311: }
312:
313: /**
314: * {@inheritDoc}
315: */
316: public boolean isCanceled() {
317: return canceled;
318: }
319:
320: /**
321: * {@inheritDoc}
322: */
323: public void setCanceled(final boolean stop) {
324: canceled = stop;
325: }
326:
327: /**
328: * Display a warning message under the progress bar. The text area for warning messages
329: * will appears only the first time this method is invoked.
330: */
331: public synchronized void warningOccurred(final String source,
332: String margin, final String warning) {
333: final StringBuffer buffer = new StringBuffer(
334: warning.length() + 16);
335: if (source != lastSource) {
336: lastSource = source;
337: if (warningArea != null) {
338: buffer.append('\n');
339: }
340: buffer.append(source != null ? source
341: : getString(VocabularyKeys.UNTITLED));
342: buffer.append('\n');
343: }
344: int wm = WARNING_MARGIN;
345: if (margin != null) {
346: margin = trim(margin);
347: if (margin.length() != 0) {
348: wm -= (margin.length() + 3);
349: buffer.append(Utilities.spaces(wm));
350: buffer.append('(');
351: buffer.append(margin);
352: buffer.append(')');
353: wm = 1;
354: }
355: }
356: buffer.append(Utilities.spaces(wm));
357: buffer.append(warning);
358: if (buffer.charAt(buffer.length() - 1) != '\n') {
359: buffer.append('\n');
360: }
361: set(Caller.WARNING, buffer.toString());
362: }
363:
364: /**
365: * Display an exception stack trace.
366: */
367: public void exceptionOccurred(final Throwable exception) {
368: ExceptionMonitor.show(window, exception);
369: }
370:
371: /**
372: * Returns the string {@code margin} without the parenthesis (if any).
373: */
374: private static String trim(String margin) {
375: margin = margin.trim();
376: int lower = 0;
377: int upper = margin.length();
378: while (lower < upper && margin.charAt(lower + 0) == '(')
379: lower++;
380: while (lower < upper && margin.charAt(upper - 1) == ')')
381: upper--;
382: return margin.substring(lower, upper);
383: }
384:
385: /**
386: * Queries one of the components in the progress window. This method
387: * doesn't need to be invoked from the <cite>Swing</cite> thread.
388: *
389: * @param task The desired value as one of the {@link Caller#TITLE}
390: * or {@link Caller#LABEL} constants.
391: * @return The value.
392: */
393: private Object get(final int task) {
394: final Caller caller = new Caller(-task);
395: SwingUtilities.invokeAndWait(caller);
396: return caller.value;
397: }
398:
399: /**
400: * Sets the state of one of the components in the progress window.
401: * This method doesn't need to be invoked from the <cite>Swing</cite> thread.
402: *
403: * @param task The value to change as one of the {@link Caller#TITLE}
404: * or {@link Caller#LABEL} constants.
405: * @param value The new value.
406: */
407: private void set(final int task, final Object value) {
408: final Caller caller = new Caller(task);
409: caller.value = value;
410: EventQueue.invokeLater(caller);
411: }
412:
413: /**
414: * Invokes a <cite>Swing</cite> method without arguments.
415: *
416: * @param task The method to invoke: {@link Caller#STARTED} or {@link Caller#DISPOSE}.
417: */
418: private void call(final int task) {
419: EventQueue.invokeLater(new Caller(task));
420: }
421:
422: /**
423: * Task to run in the <cite>Swing</cite> thread. Tasks are identified by a numeric
424: * constant. The {@code get} operations have negative identifiers and are executed
425: * by the {@link EventQueue#invokeAndWait} method. The {@code set} operations have
426: * positive identifiers and are executed by the {@link EventQueue#invokeLater} method.
427: *
428: * @version $Id: ProgressWindow.java 22680 2006-11-09 22:32:27Z jgarnett $
429: * @author Martin Desruisseaux
430: */
431: private class Caller implements Runnable {
432: /** For getting or setting the window title. */
433: public static final int TITLE = 1;
434:
435: /** For getting or setting the progress label. */
436: public static final int LABEL = 2;
437:
438: /** For getting or setting the progress bar value. */
439: public static final int PROGRESS = 3;
440:
441: /** For adding a warning message. */
442: public static final int WARNING = 4;
443:
444: /** Notify that an action started. */
445: public static final int STARTED = 5;
446:
447: /** Notify that an action is completed. */
448: public static final int COMPLETE = 6;
449:
450: /** Notify that the window can be disposed. */
451: public static final int DISPOSE = 7;
452:
453: /**
454: * The task to execute, as one of the {@link #TITLE}, {@link #LABEL}, <cite>etc.</cite>
455: * constants or their negative counterpart.
456: */
457: private final int task;
458:
459: /**
460: * The value to get (negative value {@link #task}) or set (positive value {@link #task}).
461: */
462: public Object value;
463:
464: /**
465: * Creates an action. {@code task} must be one of {@link #TITLE}, {@link #LABEL}
466: * <cite>etc.</cite> constants or their negative counterpart.
467: */
468: public Caller(final int task) {
469: this .task = task;
470: }
471:
472: /**
473: * Run the task.
474: */
475: public void run() {
476: final BoundedRangeModel model = progressBar.getModel();
477: switch (task) {
478: case -LABEL: {
479: value = description.getText();
480: return;
481: }
482: case +LABEL: {
483: description.setText((String) value);
484: return;
485: }
486: case PROGRESS: {
487: model.setValue(((Integer) value).intValue());
488: progressBar.setIndeterminate(false);
489: return;
490: }
491: case STARTED: {
492: model.setRangeProperties(0, 1, 0, 100, false);
493: window.setVisible(true);
494: break; // Need further action below.
495: }
496: case COMPLETE: {
497: model.setRangeProperties(100, 1, 0, 100, false);
498: window.setVisible(warningArea != null);
499: cancel.setEnabled(false);
500: break; // Need further action below.
501: }
502: }
503: /*
504: * Some of the tasks above requires an action on the window, which may be a JDialog or
505: * a JInternalFrame. We need to determine the window type before to apply the action.
506: */
507: synchronized (ProgressWindow.this ) {
508: if (window instanceof JDialog) {
509: final JDialog window = (JDialog) ProgressWindow.this .window;
510: switch (task) {
511: case -TITLE: {
512: value = window.getTitle();
513: return;
514: }
515: case +TITLE: {
516: window.setTitle((String) value);
517: return;
518: }
519: case STARTED: {
520: window
521: .setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
522: return;
523: }
524: case COMPLETE: {
525: window
526: .setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
527: return;
528: }
529: case DISPOSE: {
530: window
531: .setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
532: if (warningArea == null || !window.isVisible()) {
533: window.dispose();
534: }
535: return;
536: }
537: }
538: } else {
539: final JInternalFrame window = (JInternalFrame) ProgressWindow.this .window;
540: switch (task) {
541: case -TITLE: {
542: value = window.getTitle();
543: return;
544: }
545: case +TITLE: {
546: window.setTitle((String) value);
547: return;
548: }
549: case STARTED: {
550: window.setClosable(false);
551: return;
552: }
553: case COMPLETE: {
554: window.setClosable(true);
555: return;
556: }
557: case DISPOSE: {
558: window
559: .setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE);
560: if (warningArea == null || !window.isVisible()) {
561: window.dispose();
562: }
563: return;
564: }
565: }
566: }
567: /*
568: * Si la tâche spécifiée n'est aucune des tâches énumérées ci-haut,
569: * on supposera que l'on voulait afficher un message d'avertissement.
570: */
571: if (warningArea == null) {
572: final JTextArea warningArea = new JTextArea();
573: final JScrollPane scroll = new JScrollPane(
574: warningArea);
575: final JPanel namedArea = new JPanel(
576: new BorderLayout());
577: ProgressWindow.this .warningArea = warningArea;
578: warningArea.setFont(Font.getFont("Monospaced"));
579: warningArea.setEditable(false);
580: namedArea.setBorder(BorderFactory
581: .createEmptyBorder(0, HMARGIN, VMARGIN,
582: HMARGIN));
583: namedArea.add(new JLabel(
584: getString(VocabularyKeys.WARNING)),
585: BorderLayout.NORTH);
586: namedArea.add(scroll, BorderLayout.CENTER);
587: content.add(namedArea, BorderLayout.CENTER);
588: if (window instanceof JDialog) {
589: final JDialog window = (JDialog) ProgressWindow.this .window;
590: window.setResizable(true);
591: } else {
592: final JInternalFrame window = (JInternalFrame) ProgressWindow.this .window;
593: window.setResizable(true);
594: }
595: window.setSize(WIDTH, HEIGHT + WARNING_HEIGHT);
596: window.setVisible(true); // Seems required in order to force relayout.
597: }
598: final JTextArea warningArea = (JTextArea) ProgressWindow.this .warningArea;
599: warningArea.append((String) value);
600: }
601: }
602: }
603:
604: public void setTask(InternationalString task) {
605: setDescription(task.toString());
606: }
607:
608: public InternationalString getTask() {
609: return new SimpleInternationalString(getDescription());
610: }
611: }
|