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 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(); 068 innerContentPane.setBorder(new EmptyBorder(10, 10, 2, 10)); 069 contentPane.add(innerContentPane); 070 innerContentPane.setLayout(new GridBagLayout()); 071 072 // Add the logo 073 JLabel logo = new JLabel(new ImageIcon(ImageProvider.get("logo.svg").getImage().getScaledInstance(128, 129, Image.SCALE_SMOOTH))); 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(new Runnable() { 125 @Override 126 public void run() { 127 progressRenderer.setTasks(progressMonitor.toString()); 128 } 129 }); 130 } 131 132 /** 133 * A task (of a {@link ProgressMonitor}). 134 */ 135 private abstract static class Task { 136 137 /** 138 * Returns a HTML representation for this task. 139 * @param sb a {@code StringBuilder} used to build the HTML code 140 * @return {@code sb} 141 */ 142 public abstract StringBuilder toHtml(StringBuilder sb); 143 144 @Override 145 public final String toString() { 146 return toHtml(new StringBuilder(1024)).toString(); 147 } 148 } 149 150 /** 151 * A single task (of a {@link ProgressMonitor}) which keeps track of its execution duration 152 * (requires a call to {@link #finish()}). 153 */ 154 private static class MeasurableTask extends Task { 155 private final String name; 156 private final long start; 157 private String duration = ""; 158 159 MeasurableTask(String name) { 160 this.name = name; 161 this.start = System.currentTimeMillis(); 162 } 163 164 public void finish() { 165 if (!"".equals(duration)) { 166 throw new IllegalStateException("This tasks has already been finished"); 167 } 168 duration = tr(" ({0})", Utils.getDurationString(System.currentTimeMillis() - start)); 169 } 170 171 @Override 172 public StringBuilder toHtml(StringBuilder sb) { 173 return sb.append(name).append("<i style='color: #666666;'>").append(duration).append("</i>"); 174 } 175 176 @Override 177 public boolean equals(Object o) { 178 if (this == o) return true; 179 if (o == null || getClass() != o.getClass()) return false; 180 MeasurableTask that = (MeasurableTask) o; 181 return Objects.equals(name, that.name); 182 } 183 184 @Override 185 public int hashCode() { 186 return Objects.hashCode(name); 187 } 188 } 189 190 /** 191 * A {@link ProgressMonitor} which stores the (sub)tasks in a tree. 192 */ 193 public static class SplashProgressMonitor extends Task implements ProgressMonitor { 194 195 private final String name; 196 private final ChangeListener listener; 197 private final List<Task> tasks = new CopyOnWriteArrayList<>(); 198 private SplashProgressMonitor latestSubtask; 199 200 public SplashProgressMonitor(String name, ChangeListener listener) { 201 this.name = name; 202 this.listener = listener; 203 } 204 205 @Override 206 public StringBuilder toHtml(StringBuilder sb) { 207 sb.append(Utils.firstNonNull(name, "")); 208 if (!tasks.isEmpty()) { 209 sb.append("<ul>"); 210 for (Task i : tasks) { 211 sb.append("<li>"); 212 i.toHtml(sb); 213 sb.append("</li>"); 214 } 215 sb.append("</ul>"); 216 } 217 return sb; 218 } 219 220 @Override 221 public void beginTask(String title) { 222 if (title != null) { 223 final MeasurableTask task = new MeasurableTask(title); 224 tasks.add(task); 225 listener.stateChanged(null); 226 } 227 } 228 229 @Override 230 public void beginTask(String title, int ticks) { 231 this.beginTask(title); 232 } 233 234 @Override 235 public void setCustomText(String text) { 236 this.beginTask(text); 237 } 238 239 @Override 240 public void setExtraText(String text) { 241 this.beginTask(text); 242 } 243 244 @Override 245 public void indeterminateSubTask(String title) { 246 this.subTask(title); 247 } 248 249 @Override 250 public void subTask(String title) { 251 latestSubtask = new SplashProgressMonitor(title, listener); 252 tasks.add(latestSubtask); 253 listener.stateChanged(null); 254 } 255 256 @Override 257 public ProgressMonitor createSubTaskMonitor(int ticks, boolean internal) { 258 return latestSubtask; 259 } 260 261 /** 262 * @deprecated Use {@link #finishTask(String)} instead. 263 */ 264 @Override 265 @Deprecated 266 public void finishTask() { 267 // Not used 268 } 269 270 /** 271 * Displays the given task as finished. 272 * @param title the task title 273 */ 274 public void finishTask(String title) { 275 final Task task = Utils.find(tasks, Predicates.<Task>equalTo(new MeasurableTask(title))); 276 if (task instanceof MeasurableTask) { 277 ((MeasurableTask) task).finish(); 278 Main.debug(tr("{0} completed in {1}", title, ((MeasurableTask) task).duration)); 279 listener.stateChanged(null); 280 } 281 } 282 283 @Override 284 public void invalidate() { 285 // Not used 286 } 287 288 @Override 289 public void setTicksCount(int ticks) { 290 // Not used 291 } 292 293 @Override 294 public int getTicksCount() { 295 return 0; 296 } 297 298 @Override 299 public void setTicks(int ticks) { 300 } 301 302 @Override 303 public int getTicks() { 304 return 0; 305 } 306 307 @Override 308 public void worked(int ticks) { 309 // Not used 310 } 311 312 @Override 313 public boolean isCanceled() { 314 return false; 315 } 316 317 @Override 318 public void cancel() { 319 // Not used 320 } 321 322 @Override 323 public void addCancelListener(CancelListener listener) { 324 // Not used 325 } 326 327 @Override 328 public void removeCancelListener(CancelListener listener) { 329 // Not used 330 } 331 332 @Override 333 public void appendLogMessage(String message) { 334 // Not used 335 } 336 337 @Override 338 public void setProgressTaskId(ProgressTaskId taskId) { 339 // Not used 340 } 341 342 @Override 343 public ProgressTaskId getProgressTaskId() { 344 return null; 345 } 346 347 @Override 348 public Component getWindowParent() { 349 return Main.parent; 350 } 351 } 352 353 /** 354 * Returns the progress monitor. 355 * @return The progress monitor 356 */ 357 public SplashProgressMonitor getProgressMonitor() { 358 return progressMonitor; 359 } 360 361 private static class SplashScreenProgressRenderer extends JPanel { 362 private final JosmEditorPane lblTaskTitle = new JosmEditorPane(); 363 private final JProgressBar progressBar = new JProgressBar(JProgressBar.HORIZONTAL); 364 private static final String LABEL_HTML = "<html>" 365 + "<style>ul {margin-top: 0; margin-bottom: 0; padding: 0;} li {margin: 0; padding: 0;}</style>"; 366 367 protected void build() { 368 setLayout(new GridBagLayout()); 369 370 JosmEditorPane.makeJLabelLike(lblTaskTitle, false); 371 lblTaskTitle.setText(LABEL_HTML); 372 final JScrollPane scrollPane = new JScrollPane(lblTaskTitle, 373 ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); 374 scrollPane.setPreferredSize(new Dimension(0, 320)); 375 scrollPane.setBorder(BorderFactory.createEmptyBorder()); 376 add(scrollPane, GBC.eol().insets(5, 5, 0, 0).fill(GridBagConstraints.HORIZONTAL)); 377 378 progressBar.setIndeterminate(true); 379 add(progressBar, GBC.eol().insets(5, 15, 0, 0).fill(GridBagConstraints.HORIZONTAL)); 380 } 381 382 /** 383 * Constructs a new {@code SplashScreenProgressRenderer}. 384 */ 385 SplashScreenProgressRenderer() { 386 build(); 387 } 388 389 /** 390 * Sets the tasks to displayed. A HTML formatted list is expected. 391 */ 392 public void setTasks(String tasks) { 393 lblTaskTitle.setText(LABEL_HTML + tasks); 394 lblTaskTitle.setCaretPosition(lblTaskTitle.getDocument().getLength()); 395 } 396 } 397}