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