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}