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}