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