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