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