001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.progress.swing;
003
004import java.awt.Component;
005import java.awt.GraphicsEnvironment;
006import java.awt.event.ActionListener;
007import java.awt.event.WindowAdapter;
008import java.awt.event.WindowEvent;
009import java.awt.event.WindowListener;
010
011import javax.swing.SwingUtilities;
012
013import org.openstreetmap.josm.Main;
014import org.openstreetmap.josm.gui.MainApplication;
015import org.openstreetmap.josm.gui.MapFrame;
016import org.openstreetmap.josm.gui.MapStatus.BackgroundProgressMonitor;
017import org.openstreetmap.josm.gui.PleaseWaitDialog;
018import org.openstreetmap.josm.gui.progress.AbstractProgressMonitor;
019import org.openstreetmap.josm.gui.progress.CancelHandler;
020import org.openstreetmap.josm.gui.progress.ProgressException;
021import org.openstreetmap.josm.gui.progress.ProgressTaskId;
022import org.openstreetmap.josm.gui.util.GuiHelper;
023import org.openstreetmap.josm.tools.bugreport.BugReport;
024
025/**
026 * A progress monitor used in {@link org.openstreetmap.josm.gui.PleaseWaitRunnable}.
027 * <p>
028 * Progress is displayed in a dialog window ({@link PleaseWaitDialog}).
029 * @since 12675 (moved from {@code gui.progress} package}
030 */
031public class PleaseWaitProgressMonitor extends AbstractProgressMonitor {
032
033    /**
034     * Implemented by both foreground dialog and background progress dialog (in status bar)
035     */
036    public interface ProgressMonitorDialog {
037        /**
038         * Sets the visibility of this dialog
039         * @param visible The visibility, <code>true</code> to show it, <code>false</code> to hide it
040         */
041        void setVisible(boolean visible);
042
043        /**
044         * Updates the progress value to the specified progress.
045         * @param progress The progress as integer. Between 0 and {@link PleaseWaitProgressMonitor#PROGRESS_BAR_MAX}
046         */
047        void updateProgress(int progress);
048
049        /**
050         * Sets the description of what is done
051         * @param text The description of the task
052         */
053        void setCustomText(String text);
054
055        /**
056         * Sets the current action that is done
057         * @param text The current action
058         */
059        void setCurrentAction(String text);
060
061        /**
062         * Display that the current progress cannot be determined
063         * @param newValue wether the progress cannot be determined
064         */
065        void setIndeterminate(boolean newValue);
066
067        /**
068         * Append a message to the progress log
069         * <p>
070         * TODO Not implemented properly in background monitor, log message will get lost if progress runs in background
071         * @param message The message
072         */
073        void appendLogMessage(String message);
074    }
075
076    /**
077     * The maximum value the progress bar that displays the current progress should have.
078     */
079    public static final int PROGRESS_BAR_MAX = 10_000;
080
081    /**
082     * The progress monitor being currently displayed.
083     */
084    static PleaseWaitProgressMonitor currentProgressMonitor;
085
086    private final Component dialogParent;
087
088    private int currentProgressValue;
089    private String customText;
090    private String title;
091    private boolean indeterminate;
092
093    private boolean isInBackground;
094    private PleaseWaitDialog dialog;
095    private String windowTitle;
096    protected ProgressTaskId taskId;
097
098    private boolean cancelable;
099
100    /**
101     * Returns the progress monitor being currently displayed.
102     * @return the progress monitor being currently displayed
103     * @since 12638
104     */
105    public static PleaseWaitProgressMonitor getCurrent() {
106        return currentProgressMonitor;
107    }
108
109    private void doInEDT(Runnable runnable) {
110        // This must be invoke later even if current thread is EDT because inside there is dialog.setVisible
111        // which freeze current code flow until modal dialog is closed
112        SwingUtilities.invokeLater(() -> {
113            try {
114                runnable.run();
115            } catch (RuntimeException e) { // NOPMD
116                throw BugReport.intercept(e).put("monitor", this);
117            }
118        });
119    }
120
121    private void setDialogVisible(boolean visible) {
122        if (dialog.isVisible() != visible) {
123            dialog.setVisible(visible);
124        }
125    }
126
127    private ProgressMonitorDialog getDialog() {
128
129        BackgroundProgressMonitor backgroundMonitor = null;
130        MapFrame map = MainApplication.getMap();
131        if (map != null) {
132            backgroundMonitor = map.statusLine.progressMonitor;
133        }
134
135        if (backgroundMonitor != null) {
136            backgroundMonitor.setVisible(isInBackground);
137        }
138        if (dialog != null) {
139            setDialogVisible(!isInBackground || backgroundMonitor == null);
140        }
141
142        if (isInBackground && backgroundMonitor != null) {
143            backgroundMonitor.setVisible(true);
144            if (dialog != null) {
145                setDialogVisible(false);
146            }
147            return backgroundMonitor;
148        } else if (backgroundMonitor != null) {
149            backgroundMonitor.setVisible(false);
150            if (dialog != null) {
151                setDialogVisible(true);
152            }
153            return dialog;
154        } else if (dialog != null) {
155            setDialogVisible(true);
156            return dialog;
157        } else
158            return null;
159    }
160
161    /**
162     * Constructs a new {@code PleaseWaitProgressMonitor}.
163     */
164    public PleaseWaitProgressMonitor() {
165        this("");
166    }
167
168    /**
169     * Constructs a new {@code PleaseWaitProgressMonitor}.
170     * @param windowTitle window title
171     */
172    public PleaseWaitProgressMonitor(String windowTitle) {
173        this(Main.parent);
174        this.windowTitle = windowTitle;
175    }
176
177    /**
178     * Constructs a new {@code PleaseWaitProgressMonitor}.
179     * @param dialogParent component to get parent frame from
180     */
181    public PleaseWaitProgressMonitor(Component dialogParent) {
182        super(new CancelHandler());
183        if (GraphicsEnvironment.isHeadless()) {
184            this.dialogParent = dialogParent;
185        } else {
186            this.dialogParent = GuiHelper.getFrameForComponent(dialogParent);
187        }
188        this.cancelable = true;
189    }
190
191    /**
192     * Constructs a new {@code PleaseWaitProgressMonitor}.
193     * @param dialogParent component to get parent frame from
194     * @param windowTitle window title
195     */
196    public PleaseWaitProgressMonitor(Component dialogParent, String windowTitle) {
197        this(GuiHelper.getFrameForComponent(dialogParent));
198        this.windowTitle = windowTitle;
199    }
200
201    private final ActionListener cancelListener = e -> cancel();
202
203    private final ActionListener inBackgroundListener = e -> {
204        isInBackground = true;
205        ProgressMonitorDialog dlg = getDialog();
206        if (dlg != null) {
207            reset();
208            dlg.setVisible(true);
209        }
210    };
211
212    private final WindowListener windowListener = new WindowAdapter() {
213        @Override public void windowClosing(WindowEvent e) {
214            cancel();
215        }
216    };
217
218    /**
219     * See if this task is canceleable
220     * @return <code>true</code> if it can be canceled
221     */
222    public final boolean isCancelable() {
223        return cancelable;
224    }
225
226    /**
227     * Sets this task to be cancelable
228     * @param cancelable Whether it can be canceled
229     */
230    public final void setCancelable(boolean cancelable) {
231        this.cancelable = cancelable;
232    }
233
234    @Override
235    public void doBeginTask() {
236        doInEDT(() -> {
237            currentProgressMonitor = this;
238            if (GraphicsEnvironment.isHeadless()) {
239                return;
240            }
241            if (dialogParent != null && dialog == null) {
242                dialog = new PleaseWaitDialog(dialogParent);
243            } else {
244                throw new ProgressException("PleaseWaitDialog parent must be set");
245            }
246
247            if (windowTitle != null) {
248                dialog.setTitle(windowTitle);
249            }
250            dialog.setCancelEnabled(cancelable);
251            dialog.setCancelCallback(cancelListener);
252            dialog.setInBackgroundCallback(inBackgroundListener);
253            dialog.setCustomText("");
254            dialog.addWindowListener(windowListener);
255            dialog.setMaximumProgress(PROGRESS_BAR_MAX);
256            dialog.setVisible(true);
257        });
258    }
259
260    @Override
261    public void doFinishTask() {
262        // do nothing
263    }
264
265    @Override
266    protected void updateProgress(double progressValue) {
267        final int newValue = (int) (progressValue * PROGRESS_BAR_MAX);
268        if (newValue != currentProgressValue) {
269            currentProgressValue = newValue;
270            doInEDT(() -> {
271                ProgressMonitorDialog dlg = getDialog();
272                if (dlg != null) {
273                    dlg.updateProgress(currentProgressValue);
274                }
275            });
276        }
277    }
278
279    @Override
280    protected void doSetCustomText(final String title) {
281        checkState(State.IN_TASK, State.IN_SUBTASK);
282        this.customText = title;
283        doInEDT(() -> {
284            ProgressMonitorDialog dlg = getDialog();
285            if (dlg != null) {
286                dlg.setCustomText(title);
287            }
288        });
289    }
290
291    @Override
292    protected void doSetTitle(final String title) {
293        checkState(State.IN_TASK, State.IN_SUBTASK);
294        this.title = title;
295        doInEDT(() -> {
296            ProgressMonitorDialog dlg = getDialog();
297            if (dlg != null) {
298                dlg.setCurrentAction(title);
299            }
300        });
301    }
302
303    @Override
304    protected void doSetIntermediate(final boolean value) {
305        this.indeterminate = value;
306        doInEDT(() -> {
307            // Enable only if progress is at the beginning. Doing intermediate progress in the middle
308            // will hide already reached progress
309            ProgressMonitorDialog dlg = getDialog();
310            if (dlg != null) {
311                dlg.setIndeterminate(value && currentProgressValue == 0);
312            }
313        });
314    }
315
316    @Override
317    public void appendLogMessage(final String message) {
318        doInEDT(() -> {
319            ProgressMonitorDialog dlg = getDialog();
320            if (dlg != null) {
321                dlg.appendLogMessage(message);
322            }
323        });
324    }
325
326    /**
327     * Update the dialog values
328     */
329    public void reset() {
330        if (dialog != null) {
331            dialog.setTitle(title);
332            dialog.setCustomText(customText);
333            dialog.updateProgress(currentProgressValue);
334            dialog.setIndeterminate(indeterminate && currentProgressValue == 0);
335        }
336        BackgroundProgressMonitor backgroundMonitor = null;
337        MapFrame map = MainApplication.getMap();
338        if (map != null) {
339            backgroundMonitor = map.statusLine.progressMonitor;
340        }
341        if (backgroundMonitor != null) {
342            backgroundMonitor.setCurrentAction(title);
343            backgroundMonitor.setCustomText(customText);
344            backgroundMonitor.updateProgress(currentProgressValue);
345            backgroundMonitor.setIndeterminate(indeterminate && currentProgressValue == 0);
346        }
347    }
348
349    /**
350     * Close the progress dialog window.
351     */
352    public void close() {
353        doInEDT(() -> {
354            if (dialog != null) {
355                dialog.setVisible(false);
356                dialog.setCancelCallback(null);
357                dialog.setInBackgroundCallback(null);
358                dialog.removeWindowListener(windowListener);
359                dialog.dispose();
360                dialog = null;
361                currentProgressMonitor = null;
362                MapFrame map = MainApplication.getMap();
363                if (map != null) {
364                    map.statusLine.progressMonitor.setVisible(false);
365                }
366            }
367        });
368    }
369
370    /**
371     * Show the progress dialog in foreground
372     */
373    public void showForegroundDialog() {
374        isInBackground = false;
375        doInEDT(() -> {
376            if (dialog != null) {
377                dialog.setInBackgroundPossible(taskId != null && MainApplication.isDisplayingMapView());
378                reset();
379                getDialog();
380            }
381        });
382    }
383
384    @Override
385    public void setProgressTaskId(ProgressTaskId taskId) {
386        this.taskId = taskId;
387        doInEDT(() -> {
388            if (dialog != null) {
389                dialog.setInBackgroundPossible(taskId != null && MainApplication.isDisplayingMapView());
390            }
391        });
392    }
393
394    @Override
395    public ProgressTaskId getProgressTaskId() {
396        return taskId;
397    }
398
399    @Override
400    public Component getWindowParent() {
401        Component parent = dialog;
402        if (isInBackground || parent == null)
403            return Main.parent;
404        else
405            return parent;
406    }
407
408    @Override
409    public String toString() {
410        return "PleaseWaitProgressMonitor [currentProgressValue=" + currentProgressValue + ", customText=" + customText
411                + ", title=" + title + ", indeterminate=" + indeterminate + ", isInBackground=" + isInBackground
412                + ", windowTitle=" + windowTitle + ", taskId=" + taskId + ", cancelable=" + cancelable + ", state="
413                + state + "]";
414    }
415}