001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.progress;
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.MapFrame;
015import org.openstreetmap.josm.gui.MapStatus.BackgroundProgressMonitor;
016import org.openstreetmap.josm.gui.PleaseWaitDialog;
017import org.openstreetmap.josm.gui.util.GuiHelper;
018import org.openstreetmap.josm.tools.bugreport.BugReport;
019
020public class PleaseWaitProgressMonitor extends AbstractProgressMonitor {
021
022    /**
023     * Implemented by both foreground dialog and background progress dialog (in status bar)
024     */
025    public interface ProgressMonitorDialog {
026        void setVisible(boolean visible);
027
028        void updateProgress(int progress);
029
030        void setCustomText(String text);
031
032        void setCurrentAction(String text);
033
034        void setIndeterminate(boolean newValue);
035
036        // TODO Not implemented properly in background monitor, log message will get lost if progress runs in background
037        void appendLogMessage(String message);
038    }
039
040    public static final int PROGRESS_BAR_MAX = 10_000;
041    private final Component dialogParent;
042
043    private int currentProgressValue;
044    private String customText;
045    private String title;
046    private boolean indeterminate;
047
048    private boolean isInBackground;
049    private PleaseWaitDialog dialog;
050    private String windowTitle;
051    protected ProgressTaskId taskId;
052
053    private boolean cancelable;
054
055    private void doInEDT(Runnable runnable) {
056        // This must be invoke later even if current thread is EDT because inside there is dialog.setVisible
057        // which freeze current code flow until modal dialog is closed
058        SwingUtilities.invokeLater(() -> {
059            try {
060                runnable.run();
061            } catch (RuntimeException e) {
062                throw BugReport.intercept(e).put("monitor", this);
063            }
064        });
065    }
066
067    private void setDialogVisible(boolean visible) {
068        if (dialog.isVisible() != visible) {
069            dialog.setVisible(visible);
070        }
071    }
072
073    private ProgressMonitorDialog getDialog() {
074
075        BackgroundProgressMonitor backgroundMonitor = null;
076        MapFrame map = Main.map;
077        if (map != null) {
078            backgroundMonitor = map.statusLine.progressMonitor;
079        }
080
081        if (backgroundMonitor != null) {
082            backgroundMonitor.setVisible(isInBackground);
083        }
084        if (dialog != null) {
085            setDialogVisible(!isInBackground || backgroundMonitor == null);
086        }
087
088        if (isInBackground && backgroundMonitor != null) {
089            backgroundMonitor.setVisible(true);
090            if (dialog != null) {
091                setDialogVisible(false);
092            }
093            return backgroundMonitor;
094        } else if (backgroundMonitor != null) {
095            backgroundMonitor.setVisible(false);
096            if (dialog != null) {
097                setDialogVisible(true);
098            }
099            return dialog;
100        } else if (dialog != null) {
101            setDialogVisible(true);
102            return dialog;
103        } else
104            return null;
105    }
106
107    /**
108     * Constructs a new {@code PleaseWaitProgressMonitor}.
109     */
110    public PleaseWaitProgressMonitor() {
111        this("");
112    }
113
114    /**
115     * Constructs a new {@code PleaseWaitProgressMonitor}.
116     * @param windowTitle window title
117     */
118    public PleaseWaitProgressMonitor(String windowTitle) {
119        this(Main.parent);
120        this.windowTitle = windowTitle;
121    }
122
123    /**
124     * Constructs a new {@code PleaseWaitProgressMonitor}.
125     * @param dialogParent component to get parent frame from
126     */
127    public PleaseWaitProgressMonitor(Component dialogParent) {
128        super(new CancelHandler());
129        if (GraphicsEnvironment.isHeadless()) {
130            this.dialogParent = dialogParent;
131        } else {
132            this.dialogParent = GuiHelper.getFrameForComponent(dialogParent);
133        }
134        this.cancelable = true;
135    }
136
137    /**
138     * Constructs a new {@code PleaseWaitProgressMonitor}.
139     * @param dialogParent component to get parent frame from
140     * @param windowTitle window title
141     */
142    public PleaseWaitProgressMonitor(Component dialogParent, String windowTitle) {
143        this(GuiHelper.getFrameForComponent(dialogParent));
144        this.windowTitle = windowTitle;
145    }
146
147    private final ActionListener cancelListener = e -> cancel();
148
149    private final ActionListener inBackgroundListener = e -> {
150        isInBackground = true;
151        ProgressMonitorDialog dlg = getDialog();
152        if (dlg != null) {
153            reset();
154            dlg.setVisible(true);
155        }
156    };
157
158    private final WindowListener windowListener = new WindowAdapter() {
159        @Override public void windowClosing(WindowEvent e) {
160            cancel();
161        }
162    };
163
164    public final boolean isCancelable() {
165        return cancelable;
166    }
167
168    public final void setCancelable(boolean cancelable) {
169        this.cancelable = cancelable;
170    }
171
172    @Override
173    public void doBeginTask() {
174        doInEDT(() -> {
175            Main.currentProgressMonitor = this;
176            if (GraphicsEnvironment.isHeadless()) {
177                return;
178            }
179            if (dialogParent != null && dialog == null) {
180                dialog = new PleaseWaitDialog(dialogParent);
181            } else {
182                throw new ProgressException("PleaseWaitDialog parent must be set");
183            }
184
185            if (windowTitle != null) {
186                dialog.setTitle(windowTitle);
187            }
188            dialog.setCancelEnabled(cancelable);
189            dialog.setCancelCallback(cancelListener);
190            dialog.setInBackgroundCallback(inBackgroundListener);
191            dialog.setCustomText("");
192            dialog.addWindowListener(windowListener);
193            dialog.progress.setMaximum(PROGRESS_BAR_MAX);
194            dialog.setVisible(true);
195        });
196    }
197
198    @Override
199    public void doFinishTask() {
200        // do nothing
201    }
202
203    @Override
204    protected void updateProgress(double progressValue) {
205        final int newValue = (int) (progressValue * PROGRESS_BAR_MAX);
206        if (newValue != currentProgressValue) {
207            currentProgressValue = newValue;
208            doInEDT(() -> {
209                ProgressMonitorDialog dlg = getDialog();
210                if (dlg != null) {
211                    dlg.updateProgress(currentProgressValue);
212                }
213            });
214        }
215    }
216
217    @Override
218    protected void doSetCustomText(final String title) {
219        checkState(State.IN_TASK, State.IN_SUBTASK);
220        this.customText = title;
221        doInEDT(() -> {
222            ProgressMonitorDialog dlg = getDialog();
223            if (dlg != null) {
224                dlg.setCustomText(title);
225            }
226        });
227    }
228
229    @Override
230    protected void doSetTitle(final String title) {
231        checkState(State.IN_TASK, State.IN_SUBTASK);
232        this.title = title;
233        doInEDT(() -> {
234            ProgressMonitorDialog dlg = getDialog();
235            if (dlg != null) {
236                dlg.setCurrentAction(title);
237            }
238        });
239    }
240
241    @Override
242    protected void doSetIntermediate(final boolean value) {
243        this.indeterminate = value;
244        doInEDT(() -> {
245            // Enable only if progress is at the beginning. Doing intermediate progress in the middle
246            // will hide already reached progress
247            ProgressMonitorDialog dlg = getDialog();
248            if (dlg != null) {
249                dlg.setIndeterminate(value && currentProgressValue == 0);
250            }
251        });
252    }
253
254    @Override
255    public void appendLogMessage(final String message) {
256        doInEDT(() -> {
257            ProgressMonitorDialog dlg = getDialog();
258            if (dlg != null) {
259                dlg.appendLogMessage(message);
260            }
261        });
262    }
263
264    public void reset() {
265        if (dialog != null) {
266            dialog.setTitle(title);
267            dialog.setCustomText(customText);
268            dialog.updateProgress(currentProgressValue);
269            dialog.setIndeterminate(indeterminate && currentProgressValue == 0);
270        }
271        BackgroundProgressMonitor backgroundMonitor = null;
272        MapFrame map = Main.map;
273        if (map != null) {
274            backgroundMonitor = map.statusLine.progressMonitor;
275        }
276        if (backgroundMonitor != null) {
277            backgroundMonitor.setCurrentAction(title);
278            backgroundMonitor.setCustomText(customText);
279            backgroundMonitor.updateProgress(currentProgressValue);
280            backgroundMonitor.setIndeterminate(indeterminate && currentProgressValue == 0);
281        }
282    }
283
284    public void close() {
285        doInEDT(() -> {
286            if (dialog != null) {
287                dialog.setVisible(false);
288                dialog.setCancelCallback(null);
289                dialog.setInBackgroundCallback(null);
290                dialog.removeWindowListener(windowListener);
291                dialog.dispose();
292                dialog = null;
293                Main.currentProgressMonitor = null;
294                MapFrame map = Main.map;
295                if (map != null) {
296                    map.statusLine.progressMonitor.setVisible(false);
297                }
298            }
299        });
300    }
301
302    public void showForegroundDialog() {
303        isInBackground = false;
304        doInEDT(() -> {
305            if (dialog != null) {
306                dialog.setInBackgroundPossible(taskId != null && Main.isDisplayingMapView());
307                reset();
308                getDialog();
309            }
310        });
311    }
312
313    @Override
314    public void setProgressTaskId(ProgressTaskId taskId) {
315        this.taskId = taskId;
316        doInEDT(() -> {
317            if (dialog != null) {
318                dialog.setInBackgroundPossible(taskId != null && Main.isDisplayingMapView());
319            }
320        });
321    }
322
323    @Override
324    public ProgressTaskId getProgressTaskId() {
325        return taskId;
326    }
327
328    @Override
329    public Component getWindowParent() {
330        Component parent = dialog;
331        if (isInBackground || parent == null)
332            return Main.parent;
333        else
334            return parent;
335    }
336
337    @Override
338    public String toString() {
339        return "PleaseWaitProgressMonitor [currentProgressValue=" + currentProgressValue + ", customText=" + customText
340                + ", title=" + title + ", indeterminate=" + indeterminate + ", isInBackground=" + isInBackground
341                + ", windowTitle=" + windowTitle + ", taskId=" + taskId + ", cancelable=" + cancelable + ", state="
342                + state + "]";
343    }
344}