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