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