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}