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