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