001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.GridBagConstraints;
010import java.awt.GridBagLayout;
011import java.awt.Insets;
012import java.awt.event.MouseAdapter;
013import java.awt.event.MouseEvent;
014import java.util.List;
015import java.util.Objects;
016import java.util.concurrent.CopyOnWriteArrayList;
017
018import javax.swing.BorderFactory;
019import javax.swing.JFrame;
020import javax.swing.JLabel;
021import javax.swing.JPanel;
022import javax.swing.JProgressBar;
023import javax.swing.JScrollPane;
024import javax.swing.JSeparator;
025import javax.swing.ScrollPaneConstants;
026import javax.swing.border.Border;
027import javax.swing.border.EmptyBorder;
028import javax.swing.border.EtchedBorder;
029import javax.swing.event.ChangeEvent;
030import javax.swing.event.ChangeListener;
031
032import org.openstreetmap.josm.Main;
033import org.openstreetmap.josm.data.Version;
034import org.openstreetmap.josm.gui.progress.ProgressMonitor;
035import org.openstreetmap.josm.gui.progress.ProgressTaskId;
036import org.openstreetmap.josm.gui.util.GuiHelper;
037import org.openstreetmap.josm.gui.widgets.JosmEditorPane;
038import org.openstreetmap.josm.tools.GBC;
039import org.openstreetmap.josm.tools.ImageProvider;
040import org.openstreetmap.josm.tools.Utils;
041import org.openstreetmap.josm.tools.WindowGeometry;
042
043/**
044 * Show a splash screen so the user knows what is happening during startup.
045 * @since 976
046 */
047public class SplashScreen extends JFrame implements ChangeListener {
048
049    private final transient SplashProgressMonitor progressMonitor;
050    private final SplashScreenProgressRenderer progressRenderer;
051
052    /**
053     * Constructs a new {@code SplashScreen}.
054     */
055    public SplashScreen() {
056        setUndecorated(true);
057
058        // Add a nice border to the main splash screen
059        JPanel contentPane = (JPanel) this.getContentPane();
060        Border margin = new EtchedBorder(1, Color.white, Color.gray);
061        contentPane.setBorder(margin);
062
063        // Add a margin from the border to the content
064        JPanel innerContentPane = new JPanel(new GridBagLayout());
065        innerContentPane.setBorder(new EmptyBorder(10, 10, 2, 10));
066        contentPane.add(innerContentPane);
067
068        // Add the logo
069        JLabel logo = new JLabel(ImageProvider.get("logo.svg", ImageProvider.ImageSizes.SPLASH_LOGO));
070        GridBagConstraints gbc = new GridBagConstraints();
071        gbc.gridheight = 2;
072        gbc.insets = new Insets(0, 0, 0, 70);
073        innerContentPane.add(logo, gbc);
074
075        // Add the name of this application
076        JLabel caption = new JLabel("JOSM – " + tr("Java OpenStreetMap Editor"));
077        caption.setFont(GuiHelper.getTitleFont());
078        gbc.gridheight = 1;
079        gbc.gridx = 1;
080        gbc.insets = new Insets(30, 0, 0, 0);
081        innerContentPane.add(caption, gbc);
082
083        // Add the version number
084        JLabel version = new JLabel(tr("Version {0}", Version.getInstance().getVersionString()));
085        gbc.gridy = 1;
086        gbc.insets = new Insets(0, 0, 0, 0);
087        innerContentPane.add(version, gbc);
088
089        // Add a separator to the status text
090        JSeparator separator = new JSeparator(JSeparator.HORIZONTAL);
091        gbc.gridx = 0;
092        gbc.gridy = 2;
093        gbc.gridwidth = 2;
094        gbc.fill = GridBagConstraints.HORIZONTAL;
095        gbc.insets = new Insets(15, 0, 5, 0);
096        innerContentPane.add(separator, gbc);
097
098        // Add a status message
099        progressRenderer = new SplashScreenProgressRenderer();
100        gbc.gridy = 3;
101        gbc.insets = new Insets(0, 0, 10, 0);
102        innerContentPane.add(progressRenderer, gbc);
103        progressMonitor = new SplashProgressMonitor(null, this);
104
105        pack();
106
107        WindowGeometry.centerOnScreen(this.getSize(), "gui.geometry").applySafe(this);
108
109        // Add ability to hide splash screen by clicking it
110        addMouseListener(new MouseAdapter() {
111            @Override
112            public void mousePressed(MouseEvent event) {
113                setVisible(false);
114            }
115        });
116    }
117
118    @Override
119    public void stateChanged(ChangeEvent ignore) {
120        GuiHelper.runInEDT(() -> progressRenderer.setTasks(progressMonitor.toString()));
121    }
122
123    /**
124     * A task (of a {@link ProgressMonitor}).
125     */
126    private abstract static class Task {
127
128        /**
129         * Returns a HTML representation for this task.
130         * @param sb a {@code StringBuilder} used to build the HTML code
131         * @return {@code sb}
132         */
133        public abstract StringBuilder toHtml(StringBuilder sb);
134
135        @Override
136        public final String toString() {
137            return toHtml(new StringBuilder(1024)).toString();
138        }
139    }
140
141    /**
142     * A single task (of a {@link ProgressMonitor}) which keeps track of its execution duration
143     * (requires a call to {@link #finish()}).
144     */
145    private static class MeasurableTask extends Task {
146        private final String name;
147        private final long start;
148        private String duration = "";
149
150        MeasurableTask(String name) {
151            this.name = name;
152            this.start = System.currentTimeMillis();
153        }
154
155        public void finish() {
156            if (!"".equals(duration)) {
157                throw new IllegalStateException("This tasks has already been finished");
158            }
159            duration = tr(" ({0})", Utils.getDurationString(System.currentTimeMillis() - start));
160        }
161
162        @Override
163        public StringBuilder toHtml(StringBuilder sb) {
164            return sb.append(name).append("<i style='color: #666666;'>").append(duration).append("</i>");
165        }
166
167        @Override
168        public boolean equals(Object o) {
169            if (this == o) return true;
170            if (o == null || getClass() != o.getClass()) return false;
171            MeasurableTask that = (MeasurableTask) o;
172            return Objects.equals(name, that.name);
173        }
174
175        @Override
176        public int hashCode() {
177            return Objects.hashCode(name);
178        }
179    }
180
181    /**
182     * A {@link ProgressMonitor} which stores the (sub)tasks in a tree.
183     */
184    public static class SplashProgressMonitor extends Task implements ProgressMonitor {
185
186        private final String name;
187        private final ChangeListener listener;
188        private final List<Task> tasks = new CopyOnWriteArrayList<>();
189        private SplashProgressMonitor latestSubtask;
190
191        /**
192         * Constructs a new {@code SplashProgressMonitor}.
193         * @param name name
194         * @param listener change listener
195         */
196        public SplashProgressMonitor(String name, ChangeListener listener) {
197            this.name = name;
198            this.listener = listener;
199        }
200
201        @Override
202        public StringBuilder toHtml(StringBuilder sb) {
203            sb.append(Utils.firstNonNull(name, ""));
204            if (!tasks.isEmpty()) {
205                sb.append("<ul>");
206                for (Task i : tasks) {
207                    sb.append("<li>");
208                    i.toHtml(sb);
209                    sb.append("</li>");
210                }
211                sb.append("</ul>");
212            }
213            return sb;
214        }
215
216        @Override
217        public void beginTask(String title) {
218            if (title != null && !title.isEmpty()) {
219                if (Main.isDebugEnabled()) {
220                    Main.debug(title);
221                }
222                final MeasurableTask task = new MeasurableTask(title);
223                tasks.add(task);
224                listener.stateChanged(null);
225            }
226        }
227
228        @Override
229        public void beginTask(String title, int ticks) {
230            this.beginTask(title);
231        }
232
233        @Override
234        public void setCustomText(String text) {
235            this.beginTask(text);
236        }
237
238        @Override
239        public void setExtraText(String text) {
240            this.beginTask(text);
241        }
242
243        @Override
244        public void indeterminateSubTask(String title) {
245            this.subTask(title);
246        }
247
248        @Override
249        public void subTask(String title) {
250            if (Main.isDebugEnabled()) {
251                Main.debug(title);
252            }
253            latestSubtask = new SplashProgressMonitor(title, listener);
254            tasks.add(latestSubtask);
255            listener.stateChanged(null);
256        }
257
258        @Override
259        public ProgressMonitor createSubTaskMonitor(int ticks, boolean internal) {
260            if (latestSubtask != null) {
261                return latestSubtask;
262            } else {
263                // subTask has not been called before, such as for plugin update, #11874
264                return this;
265            }
266        }
267
268        /**
269         * @deprecated Use {@link #finishTask(String)} instead.
270         */
271        @Override
272        @Deprecated
273        public void finishTask() {
274            // Not used
275        }
276
277        /**
278         * Displays the given task as finished.
279         * @param title the task title
280         */
281        public void finishTask(String title) {
282            final Task task = Utils.find(tasks, new MeasurableTask(title)::equals);
283            if (task instanceof MeasurableTask) {
284                ((MeasurableTask) task).finish();
285                if (Main.isDebugEnabled()) {
286                    Main.debug(tr("{0} completed in {1}", title, ((MeasurableTask) task).duration));
287                }
288                listener.stateChanged(null);
289            }
290        }
291
292        @Override
293        public void invalidate() {
294            // Not used
295        }
296
297        @Override
298        public void setTicksCount(int ticks) {
299            // Not used
300        }
301
302        @Override
303        public int getTicksCount() {
304            return 0;
305        }
306
307        @Override
308        public void setTicks(int ticks) {
309            // Not used
310        }
311
312        @Override
313        public int getTicks() {
314            return 0;
315        }
316
317        @Override
318        public void worked(int ticks) {
319            // Not used
320        }
321
322        @Override
323        public boolean isCanceled() {
324            return false;
325        }
326
327        @Override
328        public void cancel() {
329            // Not used
330        }
331
332        @Override
333        public void addCancelListener(CancelListener listener) {
334            // Not used
335        }
336
337        @Override
338        public void removeCancelListener(CancelListener listener) {
339            // Not used
340        }
341
342        @Override
343        public void appendLogMessage(String message) {
344            // Not used
345        }
346
347        @Override
348        public void setProgressTaskId(ProgressTaskId taskId) {
349            // Not used
350        }
351
352        @Override
353        public ProgressTaskId getProgressTaskId() {
354            return null;
355        }
356
357        @Override
358        public Component getWindowParent() {
359            return Main.parent;
360        }
361    }
362
363    /**
364     * Returns the progress monitor.
365     * @return The progress monitor
366     */
367    public SplashProgressMonitor getProgressMonitor() {
368        return progressMonitor;
369    }
370
371    private static class SplashScreenProgressRenderer extends JPanel {
372        private final JosmEditorPane lblTaskTitle = new JosmEditorPane();
373        private final JProgressBar progressBar = new JProgressBar(JProgressBar.HORIZONTAL);
374        private static final String LABEL_HTML = "<html>"
375                + "<style>ul {margin-top: 0; margin-bottom: 0; padding: 0;} li {margin: 0; padding: 0;}</style>";
376
377        protected void build() {
378            setLayout(new GridBagLayout());
379
380            JosmEditorPane.makeJLabelLike(lblTaskTitle, false);
381            lblTaskTitle.setText(LABEL_HTML);
382            final JScrollPane scrollPane = new JScrollPane(lblTaskTitle,
383                    ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
384            scrollPane.setPreferredSize(new Dimension(0, 320));
385            scrollPane.setBorder(BorderFactory.createEmptyBorder());
386            add(scrollPane, GBC.eol().insets(5, 5, 0, 0).fill(GridBagConstraints.HORIZONTAL));
387
388            progressBar.setIndeterminate(true);
389            add(progressBar, GBC.eol().insets(5, 15, 0, 0).fill(GridBagConstraints.HORIZONTAL));
390        }
391
392        /**
393         * Constructs a new {@code SplashScreenProgressRenderer}.
394         */
395        SplashScreenProgressRenderer() {
396            build();
397        }
398
399        /**
400         * Sets the tasks to displayed. A HTML formatted list is expected.
401         * @param tasks HTML formatted list of tasks
402         */
403        public void setTasks(String tasks) {
404            lblTaskTitle.setText(LABEL_HTML + tasks);
405            lblTaskTitle.setCaretPosition(lblTaskTitle.getDocument().getLength());
406        }
407    }
408}