2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
6 package com.threerings.getdown.launcher;
9 import java.awt.Dimension;
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;
22 import javax.swing.JComponent;
23 import javax.swing.Timer;
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;
30 import com.threerings.getdown.data.Application.UpdateInterface;
31 import com.threerings.getdown.data.Build;
32 import com.threerings.getdown.util.MessageUtil;
33 import com.threerings.getdown.util.Rectangle;
34 import com.threerings.getdown.util.StringUtil;
35 import static com.threerings.getdown.Log.log;
38 * Displays download and patching status.
40 public final class StatusPanel extends JComponent
41 implements ImageObserver
43 public StatusPanel (ResourceBundle msgs)
47 // Add a bit of "throbbing" to the display by updating the number of dots displayed after
48 // our status. This lets users know things are still working.
49 _timer = new Timer(1000,
50 new ActionListener() {
51 public void actionPerformed (ActionEvent event) {
52 if (_status != null && !_displayError) {
53 _statusDots = (_statusDots % 3) + 1; // 1, 2, 3, 1, 2, 3, etc.
60 public void init (UpdateInterface ifc, RotatingBackgrounds bg, Image barimg)
64 Image img = _bg.getImage(_progress);
65 int width = img == null ? -1 : img.getWidth(this);
66 int height = img == null ? -1 : img.getHeight(this);
67 if (width == -1 || height == -1) {
68 Rectangle bounds = ifc.progress.union(ifc.status);
69 // assume the x inset defines the frame padding; add it on the left, right, and bottom
70 _psize = new Dimension(bounds.x + bounds.width + bounds.x,
71 bounds.y + bounds.height + bounds.x);
73 _psize = new Dimension(width, height);
80 public boolean imageUpdate (Image img, int infoflags, int x, int y, int width, int height)
82 boolean updated = false;
83 if ((infoflags & WIDTH) != 0) {
87 if ((infoflags & HEIGHT) != 0) {
88 _psize.height = height;
94 getParent().setSize(_psize);
96 return (infoflags & ALLBITS) == 0;
100 * Adjusts the progress display to the specified percentage.
102 public void setProgress (int percent, long remaining)
104 boolean needsRepaint = false;
106 // maybe update the progress label
107 if (_progress != percent) {
109 if (_ifc != null && !_ifc.hideProgressText) {
110 String msg = MessageFormat.format(get("m.complete"), percent);
111 _newplab = createLabel(msg, new Color(_ifc.progressText, true));
116 // maybe update the remaining label
118 // skip this estimate if it's been less than a second since our last one came in
119 if (!_rthrottle.throttleOp()) {
120 _remain[_ridx++%_remain.length] = remaining;
123 // smooth the remaining time by taking the trailing average of the last four values
125 int values = Math.min(_ridx, _remain.length);
126 for (int ii = 0; ii < values; ii++) {
127 remaining += _remain[ii];
131 if (_ifc != null && !_ifc.hideProgressText) {
132 // now compute our display value
133 int minutes = (int)(remaining / 60), seconds = (int)(remaining % 60);
134 String remstr = minutes + ":" + ((seconds < 10) ? "0" : "") + seconds;
135 String msg = MessageFormat.format(get("m.remain"), remstr);
136 _newrlab = createLabel(msg, new Color(_ifc.statusText, true));
140 } else if (_rlabel != null || _newrlab != null) {
141 _rthrottle = new Throttle(1, 1000);
143 _newrlab = _rlabel = null;
153 * Displays the specified status string.
155 public void setStatus (String status, boolean displayError)
157 _status = xlate(status);
158 _displayError = displayError;
163 * Stop the throbbing.
165 public void stopThrob ()
173 public void addNotify ()
180 public void removeNotify ()
183 super.removeNotify();
186 // documentation inherited
188 public void paintComponent (Graphics g)
190 super.paintComponent(g);
191 Graphics2D gfx = (Graphics2D)g;
193 // attempt to draw a background image...
196 img = _bg.getErrorImage();
198 img = _bg.getImage(_progress);
201 gfx.drawImage(img, 0, 0, this);
204 Object oalias = SwingUtil.activateAntiAliasing(gfx);
206 // if we have new labels; lay them out
207 if (_newlab != null) {
212 if (_newplab != null) {
213 _newplab.layout(gfx);
217 if (_newrlab != null) {
218 _newrlab.layout(gfx);
223 if (_barimg != null) {
224 gfx.setClip(_ifc.progress.x, _ifc.progress.y,
225 _progress * _ifc.progress.width / 100,
226 _ifc.progress.height);
227 gfx.drawImage(_barimg, _ifc.progress.x, _ifc.progress.y, null);
230 gfx.setColor(new Color(_ifc.progressBar, true));
231 gfx.fillRect(_ifc.progress.x, _ifc.progress.y,
232 _progress * _ifc.progress.width / 100,
233 _ifc.progress.height);
236 if (_plabel != null) {
237 int xmarg = (_ifc.progress.width - _plabel.getSize().width)/2;
238 int ymarg = (_ifc.progress.height - _plabel.getSize().height)/2;
239 _plabel.render(gfx, _ifc.progress.x + xmarg, _ifc.progress.y + ymarg);
242 if (_label != null) {
243 _label.render(gfx, _ifc.status.x, getStatusY(_label));
246 if (_rlabel != null) {
247 // put the remaining label at the end of the status area. This could be dangerous
248 // but I think the only time we would display it is with small statuses.
249 int x = _ifc.status.x + _ifc.status.width - _rlabel.getSize().width;
250 _rlabel.render(gfx, x, getStatusY(_rlabel));
253 SwingUtil.restoreAntiAliasing(gfx, oalias);
256 // documentation inherited
258 public Dimension getPreferredSize ()
264 * Update the status label.
266 protected void updateStatusLabel ()
271 String status = _status;
272 if (!_displayError) {
273 for (int ii = 0; ii < _statusDots; ii++) {
278 StringBuilder labelText = new StringBuilder();
279 if (_ifc.displayVersion) {
280 labelText.append("launcher version: " + Build.version());
281 labelText.append("\n");
282 labelText.append("install4j version: " + i4jVersion);
283 labelText.append("\n");
284 labelText.append("installer version: " + System.getProperty("installer_template_version"));
285 labelText.append("\n");
287 if (_ifc.displayAppbase) {
288 labelText.append("appbase: " + _appbase);
289 labelText.append("\n");
291 labelText.append(status);
293 _newlab = createLabel(labelText.toString(), new Color(_ifc.statusText, true));
294 // set the width of the label to the width specified
295 int width = _ifc.status.width;
297 // unless we had trouble reading that width, in which case use the entire window
300 // but the window itself might not be initialized and have a width of 0
302 _newlab.setTargetWidth(width);
308 * Get the y coordinate of a label in the status area.
310 protected int getStatusY (Label label)
312 // if the status region is higher than the progress region, we
313 // want to align the label with the bottom of its region
314 // rather than the top
315 if (_ifc.status.y > _ifc.progress.y) {
316 return _ifc.status.y;
318 return _ifc.status.y + (_ifc.status.height - label.getSize().height);
322 * Create a label, taking care of adding the shadow if needed.
324 protected Label createLabel (String text, Color color)
326 Label label = new Label(text, color, FONT);
327 if (_ifc.textShadow != 0) {
328 label.setAlternateColor(new Color(_ifc.textShadow, true));
329 label.setStyle(LabelStyleConstants.SHADOW);
334 /** Used by {@link #setStatus}. */
335 protected String xlate (String compoundKey)
337 // to be more efficient about creating unnecessary objects, we
338 // do some checking before splitting
339 int tidx = compoundKey.indexOf('|');
341 return get(compoundKey);
344 String key = compoundKey.substring(0, tidx);
345 String argstr = compoundKey.substring(tidx+1);
346 String[] args = argstr.split("\\|");
347 // unescape and translate the arguments
348 for (int i = 0; i < args.length; i++) {
349 // if the argument is tainted, do no further translation
350 // (it might contain |s or other fun stuff)
351 if (MessageUtil.isTainted(args[i])) {
352 args[i] = MessageUtil.unescape(MessageUtil.untaint(args[i]));
354 args[i] = xlate(MessageUtil.unescape(args[i]));
357 return get(key, args);
361 /** Used by {@link #setStatus}. */
362 protected String get (String key, String[] args)
364 String msg = get(key);
365 if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args);
366 return key + String.valueOf(Arrays.asList(args));
369 /** Used by {@link #setStatus}, and {@link #setProgress}. */
370 protected String get (String key)
372 // if we have no _msgs that means we're probably recovering from a
373 // failure to load the translation messages in the first place, so
374 // just give them their key back because it's probably an english
380 // if this string is tainted, we don't translate it, instead we
381 // simply remove the taint character and return it to the caller
382 if (MessageUtil.isTainted(key)) {
383 return MessageUtil.untaint(key);
386 return _msgs.getString(key);
387 } catch (MissingResourceException mre) {
388 log.warning("Missing translation message '" + key + "'.");
393 public void setAppbase(String appbase) {
397 protected Image _barimg;
398 protected RotatingBackgrounds _bg;
399 protected Dimension _psize;
401 protected ResourceBundle _msgs;
403 protected int _progress = -1;
404 protected String _status;
405 protected int _statusDots = 1;
406 protected boolean _displayError;
407 protected Label _label, _newlab;
408 protected Label _plabel, _newplab;
409 protected Label _rlabel, _newrlab;
411 protected UpdateInterface _ifc;
412 protected Timer _timer;
414 protected long[] _remain = new long[4];
416 protected Throttle _rthrottle = new Throttle(1, 1000L);
418 protected static final Font FONT = new Font("SansSerif", Font.BOLD, 12);
420 public String _appbase;
422 public static String i4jVersion = null;