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