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