1 // 2 // Getdown - application installer, patcher and launcher 3 // Copyright (C) 2004-2018 Getdown authors 4 // https://github.com/threerings/getdown/blob/master/LICENSE 5 6 package com.threerings.getdown.launcher; 7 8 import java.awt.Color; 9 import java.awt.Dimension; 10 import java.awt.Font; 11 import java.awt.Graphics; 12 import java.awt.Graphics2D; 13 import java.awt.Image; 14 import java.awt.event.ActionEvent; 15 import java.awt.event.ActionListener; 16 import java.awt.image.ImageObserver; 17 import java.text.MessageFormat; 18 import java.util.Arrays; 19 import java.util.MissingResourceException; 20 import java.util.ResourceBundle; 21 22 import javax.swing.JComponent; 23 import javax.swing.Timer; 24 25 import com.samskivert.swing.Label; 26 import com.samskivert.swing.LabelStyleConstants; 27 import com.samskivert.swing.util.SwingUtil; 28 import com.samskivert.util.Throttle; 29 30 import com.threerings.getdown.data.Application; 31 import com.threerings.getdown.data.Application.UpdateInterface; 32 import com.threerings.getdown.data.Build; 33 import com.threerings.getdown.util.MessageUtil; 34 import com.threerings.getdown.util.Rectangle; 35 import com.threerings.getdown.util.StringUtil; 36 import static com.threerings.getdown.Log.log; 37 38 /** 39 * Displays download and patching status. 40 */ 41 public final class StatusPanel extends JComponent 42 implements ImageObserver 43 { StatusPanel(ResourceBundle msgs)44 public StatusPanel (ResourceBundle msgs) 45 { 46 _msgs = msgs; 47 48 // Add a bit of "throbbing" to the display by updating the number of dots displayed after 49 // our status. This lets users know things are still working. 50 _timer = new Timer(1000, 51 new ActionListener() { 52 public void actionPerformed (ActionEvent event) { 53 if (_status != null && !_displayError) { 54 _statusDots = (_statusDots % 3) + 1; // 1, 2, 3, 1, 2, 3, etc. 55 updateStatusLabel(); 56 } 57 } 58 }); 59 } 60 init(UpdateInterface ifc, RotatingBackgrounds bg, Image barimg)61 public void init (UpdateInterface ifc, RotatingBackgrounds bg, Image barimg) 62 { 63 _ifc = ifc; 64 _bg = bg; 65 Image img = _bg.getImage(_progress); 66 int width = img == null ? -1 : img.getWidth(this); 67 int height = img == null ? -1 : img.getHeight(this); 68 if (width == -1 || height == -1) { 69 Rectangle bounds = ifc.progress.union(ifc.status); 70 // assume the x inset defines the frame padding; add it on the left, right, and bottom 71 _psize = new Dimension(bounds.x + bounds.width + bounds.x, 72 bounds.y + bounds.height + bounds.x); 73 } else { 74 _psize = new Dimension(width, height); 75 } 76 _barimg = barimg; 77 invalidate(); 78 } 79 80 @Override imageUpdate(Image img, int infoflags, int x, int y, int width, int height)81 public boolean imageUpdate (Image img, int infoflags, int x, int y, int width, int height) 82 { 83 boolean updated = false; 84 if ((infoflags & WIDTH) != 0) { 85 _psize.width = width; 86 updated = true; 87 } 88 if ((infoflags & HEIGHT) != 0) { 89 _psize.height = height; 90 updated = true; 91 } 92 if (updated) { 93 invalidate(); 94 setSize(_psize); 95 getParent().setSize(_psize); 96 } 97 return (infoflags & ALLBITS) == 0; 98 } 99 100 /** 101 * Adjusts the progress display to the specified percentage. 102 */ setProgress(int percent, long remaining)103 public void setProgress (int percent, long remaining) 104 { 105 boolean needsRepaint = false; 106 107 // maybe update the progress label 108 if (_progress != percent) { 109 _progress = percent; 110 if (_ifc != null && !_ifc.hideProgressText) { 111 String msg = MessageFormat.format(get("m.complete"), percent); 112 _newplab = createLabel(msg, new Color(_ifc.progressText, true)); 113 } 114 needsRepaint = true; 115 } 116 117 // maybe update the remaining label 118 if (remaining > 1) { 119 // skip this estimate if it's been less than a second since our last one came in 120 if (!_rthrottle.throttleOp()) { 121 _remain[_ridx++%_remain.length] = remaining; 122 } 123 124 // smooth the remaining time by taking the trailing average of the last four values 125 remaining = 0; 126 int values = Math.min(_ridx, _remain.length); 127 for (int ii = 0; ii < values; ii++) { 128 remaining += _remain[ii]; 129 } 130 remaining /= values; 131 132 if (_ifc != null && !_ifc.hideProgressText) { 133 // now compute our display value 134 int minutes = (int)(remaining / 60), seconds = (int)(remaining % 60); 135 String remstr = minutes + ":" + ((seconds < 10) ? "0" : "") + seconds; 136 String msg = MessageFormat.format(get("m.remain"), remstr); 137 _newrlab = createLabel(msg, new Color(_ifc.statusText, true)); 138 } 139 needsRepaint = true; 140 141 } else if (_rlabel != null || _newrlab != null) { 142 _rthrottle = new Throttle(1, 1000); 143 _ridx = 0; 144 _newrlab = _rlabel = null; 145 needsRepaint = true; 146 } 147 148 if (needsRepaint) { 149 repaint(); 150 } 151 } 152 153 /** 154 * Displays the specified status string. 155 */ setStatus(String status, boolean displayError)156 public void setStatus (String status, boolean displayError) 157 { 158 _status = xlate(status); 159 _displayError = displayError; 160 updateStatusLabel(); 161 } 162 163 /** 164 * Stop the throbbing. 165 */ stopThrob()166 public void stopThrob () 167 { 168 _timer.stop(); 169 _statusDots = 3; 170 updateStatusLabel(); 171 } 172 173 @Override addNotify()174 public void addNotify () 175 { 176 super.addNotify(); 177 _timer.start(); 178 } 179 180 @Override removeNotify()181 public void removeNotify () 182 { 183 _timer.stop(); 184 super.removeNotify(); 185 } 186 187 // documentation inherited 188 @Override paintComponent(Graphics g)189 public void paintComponent (Graphics g) 190 { 191 super.paintComponent(g); 192 Graphics2D gfx = (Graphics2D)g; 193 194 // attempt to draw a background image... 195 Image img; 196 if (_displayError) { 197 img = _bg.getErrorImage(); 198 } else { 199 img = _bg.getImage(_progress); 200 } 201 if (img != null) { 202 gfx.drawImage(img, 0, 0, this); 203 } 204 205 Object oalias = SwingUtil.activateAntiAliasing(gfx); 206 207 // if we have new labels; lay them out 208 if (_newlab != null) { 209 _newlab.layout(gfx); 210 _label = _newlab; 211 _newlab = null; 212 } 213 if (_newplab != null) { 214 _newplab.layout(gfx); 215 _plabel = _newplab; 216 _newplab = null; 217 } 218 if (_newrlab != null) { 219 _newrlab.layout(gfx); 220 _rlabel = _newrlab; 221 _newrlab = null; 222 } 223 224 if (_barimg != null) { 225 gfx.setClip(_ifc.progress.x, _ifc.progress.y, 226 _progress * _ifc.progress.width / 100, 227 _ifc.progress.height); 228 gfx.drawImage(_barimg, _ifc.progress.x, _ifc.progress.y, null); 229 gfx.setClip(null); 230 } else { 231 gfx.setColor(new Color(_ifc.progressBar, true)); 232 gfx.fillRect(_ifc.progress.x, _ifc.progress.y, 233 _progress * _ifc.progress.width / 100, 234 _ifc.progress.height); 235 } 236 237 if (_plabel != null) { 238 int xmarg = (_ifc.progress.width - _plabel.getSize().width)/2; 239 int ymarg = (_ifc.progress.height - _plabel.getSize().height)/2; 240 _plabel.render(gfx, _ifc.progress.x + xmarg, _ifc.progress.y + ymarg); 241 } 242 243 if (_label != null) { 244 _label.render(gfx, _ifc.status.x, getStatusY(_label)); 245 } 246 247 if (_rlabel != null) { 248 // put the remaining label at the end of the status area. This could be dangerous 249 // but I think the only time we would display it is with small statuses. 250 int x = _ifc.status.x + _ifc.status.width - _rlabel.getSize().width; 251 _rlabel.render(gfx, x, getStatusY(_rlabel)); 252 } 253 254 SwingUtil.restoreAntiAliasing(gfx, oalias); 255 } 256 257 // documentation inherited 258 @Override getPreferredSize()259 public Dimension getPreferredSize () 260 { 261 return _psize; 262 } 263 264 /** 265 * Update the status label. 266 */ updateStatusLabel()267 protected void updateStatusLabel () 268 { 269 if (_ifc == null) { 270 return; 271 } 272 String status = _status; 273 if (!_displayError) { 274 for (int ii = 0; ii < _statusDots; ii++) { 275 status += " ."; 276 } 277 } 278 279 StringBuilder labelText = new StringBuilder(); 280 if (_ifc.displayVersion) { 281 labelText.append("launcher version: " + Build.version()); 282 labelText.append("\n"); 283 labelText.append("install4j version: " + Application.i4jVersion); 284 labelText.append("\n"); 285 labelText.append("installer version: " + System.getProperty("installer_template_version")); 286 labelText.append("\n"); 287 } 288 if (_ifc.displayAppbase) { 289 labelText.append("appbase: " + _appbase); 290 labelText.append("\n"); 291 } 292 labelText.append(status); 293 294 _newlab = createLabel(labelText.toString(), new Color(_ifc.statusText, true)); 295 // set the width of the label to the width specified 296 int width = _ifc.status.width; 297 if (width == 0) { 298 // unless we had trouble reading that width, in which case use the entire window 299 width = getWidth(); 300 } 301 // but the window itself might not be initialized and have a width of 0 302 if (width > 0) { 303 _newlab.setTargetWidth(width); 304 } 305 repaint(); 306 } 307 308 /** 309 * Get the y coordinate of a label in the status area. 310 */ getStatusY(Label label)311 protected int getStatusY (Label label) 312 { 313 // if the status region is higher than the progress region, we 314 // want to align the label with the bottom of its region 315 // rather than the top 316 if (_ifc.status.y > _ifc.progress.y) { 317 return _ifc.status.y; 318 } 319 return _ifc.status.y + (_ifc.status.height - label.getSize().height); 320 } 321 322 /** 323 * Create a label, taking care of adding the shadow if needed. 324 */ createLabel(String text, Color color)325 protected Label createLabel (String text, Color color) 326 { 327 Label label = new Label(text, color, FONT); 328 if (_ifc.textShadow != 0) { 329 label.setAlternateColor(new Color(_ifc.textShadow, true)); 330 label.setStyle(LabelStyleConstants.SHADOW); 331 } 332 return label; 333 } 334 335 /** Used by {@link #setStatus}. */ xlate(String compoundKey)336 protected String xlate (String compoundKey) 337 { 338 // to be more efficient about creating unnecessary objects, we 339 // do some checking before splitting 340 int tidx = compoundKey.indexOf('|'); 341 if (tidx == -1) { 342 return get(compoundKey); 343 344 } else { 345 String key = compoundKey.substring(0, tidx); 346 String argstr = compoundKey.substring(tidx+1); 347 String[] args = argstr.split("\\|"); 348 // unescape and translate the arguments 349 for (int i = 0; i < args.length; i++) { 350 // if the argument is tainted, do no further translation 351 // (it might contain |s or other fun stuff) 352 if (MessageUtil.isTainted(args[i])) { 353 args[i] = MessageUtil.unescape(MessageUtil.untaint(args[i])); 354 } else { 355 args[i] = xlate(MessageUtil.unescape(args[i])); 356 } 357 } 358 return get(key, args); 359 } 360 } 361 362 /** Used by {@link #setStatus}. */ get(String key, String[] args)363 protected String get (String key, String[] args) 364 { 365 String msg = get(key); 366 if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args); 367 return key + String.valueOf(Arrays.asList(args)); 368 } 369 370 /** Used by {@link #setStatus}, and {@link #setProgress}. */ get(String key)371 protected String get (String key) 372 { 373 // if we have no _msgs that means we're probably recovering from a 374 // failure to load the translation messages in the first place, so 375 // just give them their key back because it's probably an english 376 // string; whee! 377 if (_msgs == null) { 378 return key; 379 } 380 381 // if this string is tainted, we don't translate it, instead we 382 // simply remove the taint character and return it to the caller 383 if (MessageUtil.isTainted(key)) { 384 return MessageUtil.untaint(key); 385 } 386 try { 387 return _msgs.getString(key); 388 } catch (MissingResourceException mre) { 389 log.warning("Missing translation message '" + key + "'."); 390 return key; 391 } 392 } 393 setAppbase(String appbase)394 public void setAppbase(String appbase) { 395 _appbase = appbase; 396 } 397 398 protected Image _barimg; 399 protected RotatingBackgrounds _bg; 400 protected Dimension _psize; 401 402 protected ResourceBundle _msgs; 403 404 protected int _progress = -1; 405 protected String _status; 406 protected int _statusDots = 1; 407 protected boolean _displayError; 408 protected Label _label, _newlab; 409 protected Label _plabel, _newplab; 410 protected Label _rlabel, _newrlab; 411 412 protected UpdateInterface _ifc; 413 protected Timer _timer; 414 415 protected long[] _remain = new long[4]; 416 protected int _ridx; 417 protected Throttle _rthrottle = new Throttle(1, 1000L); 418 419 protected static final Font FONT = new Font("SansSerif", Font.BOLD, 12); 420 421 public String _appbase; 422 } 423