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