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(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 public SplashProgressMonitor(String name, ChangeListener listener) { 200 this.name = name; 201 this.listener = listener; 202 } 203 204 @Override 205 public StringBuilder toHtml(StringBuilder sb) { 206 sb.append(Utils.firstNonNull(name, "")); 207 if (!tasks.isEmpty()) { 208 sb.append("<ul>"); 209 for (Task i : tasks) { 210 sb.append("<li>"); 211 i.toHtml(sb); 212 sb.append("</li>"); 213 } 214 sb.append("</ul>"); 215 } 216 return sb; 217 } 218 219 @Override 220 public void beginTask(String title) { 221 if (title != null) { 222 final MeasurableTask task = new MeasurableTask(title); 223 tasks.add(task); 224 listener.stateChanged(null); 225 } 226 } 227 228 @Override 229 public void beginTask(String title, int ticks) { 230 this.beginTask(title); 231 } 232 233 @Override 234 public void setCustomText(String text) { 235 this.beginTask(text); 236 } 237 238 @Override 239 public void setExtraText(String text) { 240 this.beginTask(text); 241 } 242 243 @Override 244 public void indeterminateSubTask(String title) { 245 this.subTask(title); 246 } 247 248 @Override 249 public void subTask(String title) { 250 latestSubtask = new SplashProgressMonitor(title, listener); 251 tasks.add(latestSubtask); 252 listener.stateChanged(null); 253 } 254 255 @Override 256 public ProgressMonitor createSubTaskMonitor(int ticks, boolean internal) { 257 return latestSubtask; 258 } 259 260 /** 261 * @deprecated Use {@link #finishTask(String)} instead. 262 */ 263 @Override 264 @Deprecated 265 public void finishTask() { 266 // Not used 267 } 268 269 /** 270 * Displays the given task as finished. 271 * @param title the task title 272 */ 273 public void finishTask(String title) { 274 final Task task = Utils.find(tasks, Predicates.<Task>equalTo(new MeasurableTask(title))); 275 if (task instanceof MeasurableTask) { 276 ((MeasurableTask) task).finish(); 277 Main.debug(tr("{0} completed in {1}", title, ((MeasurableTask) task).duration)); 278 listener.stateChanged(null); 279 } 280 } 281 282 @Override 283 public void invalidate() { 284 // Not used 285 } 286 287 @Override 288 public void setTicksCount(int ticks) { 289 // Not used 290 } 291 292 @Override 293 public int getTicksCount() { 294 return 0; 295 } 296 297 @Override 298 public void setTicks(int ticks) { 299 } 300 301 @Override 302 public int getTicks() { 303 return 0; 304 } 305 306 @Override 307 public void worked(int ticks) { 308 // Not used 309 } 310 311 @Override 312 public boolean isCanceled() { 313 return false; 314 } 315 316 @Override 317 public void cancel() { 318 // Not used 319 } 320 321 @Override 322 public void addCancelListener(CancelListener listener) { 323 // Not used 324 } 325 326 @Override 327 public void removeCancelListener(CancelListener listener) { 328 // Not used 329 } 330 331 @Override 332 public void appendLogMessage(String message) { 333 // Not used 334 } 335 336 @Override 337 public void setProgressTaskId(ProgressTaskId taskId) { 338 // Not used 339 } 340 341 @Override 342 public ProgressTaskId getProgressTaskId() { 343 return null; 344 } 345 346 @Override 347 public Component getWindowParent() { 348 return Main.parent; 349 } 350 } 351 352 /** 353 * Returns the progress monitor. 354 * @return The progress monitor 355 */ 356 public SplashProgressMonitor getProgressMonitor() { 357 return progressMonitor; 358 } 359 360 private static class SplashScreenProgressRenderer extends JPanel { 361 private final JosmEditorPane lblTaskTitle = new JosmEditorPane(); 362 private final JProgressBar progressBar = new JProgressBar(JProgressBar.HORIZONTAL); 363 private static final String LABEL_HTML = "<html>" 364 + "<style>ul {margin-top: 0; margin-bottom: 0; padding: 0;} li {margin: 0; padding: 0;}</style>"; 365 366 protected void build() { 367 setLayout(new GridBagLayout()); 368 369 JosmEditorPane.makeJLabelLike(lblTaskTitle, false); 370 lblTaskTitle.setText(LABEL_HTML); 371 final JScrollPane scrollPane = new JScrollPane(lblTaskTitle, 372 ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); 373 scrollPane.setPreferredSize(new Dimension(0, 320)); 374 scrollPane.setBorder(BorderFactory.createEmptyBorder()); 375 add(scrollPane, GBC.eol().insets(5, 5, 0, 0).fill(GridBagConstraints.HORIZONTAL)); 376 377 progressBar.setIndeterminate(true); 378 add(progressBar, GBC.eol().insets(5, 15, 0, 0).fill(GridBagConstraints.HORIZONTAL)); 379 } 380 381 /** 382 * Constructs a new {@code SplashScreenProgressRenderer}. 383 */ 384 SplashScreenProgressRenderer() { 385 build(); 386 } 387 388 /** 389 * Sets the tasks to displayed. A HTML formatted list is expected. 390 * @param tasks HTML formatted list of tasks 391 */ 392 public void setTasks(String tasks) { 393 lblTaskTitle.setText(LABEL_HTML + tasks); 394 lblTaskTitle.setCaretPosition(lblTaskTitle.getDocument().getLength()); 395 } 396 } 397}