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;
36 import static com.threerings.getdown.Log.log;
39 * Displays download and patching status.
41 public final class StatusPanel extends JComponent
42 implements ImageObserver
44 public StatusPanel (ResourceBundle msgs)
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.
61 public void init (UpdateInterface ifc, RotatingBackgrounds bg, Image barimg)
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);
74 _psize = new Dimension(width, height);
81 public boolean imageUpdate (Image img, int infoflags, int x, int y, int width, int height)
83 boolean updated = false;
84 if ((infoflags & WIDTH) != 0) {
88 if ((infoflags & HEIGHT) != 0) {
89 _psize.height = height;
95 getParent().setSize(_psize);
97 return (infoflags & ALLBITS) == 0;
101 * Adjusts the progress display to the specified percentage.
103 public void setProgress (int percent, long remaining)
105 boolean needsRepaint = false;
107 // maybe update the progress label
108 if (_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));
117 // maybe update the remaining label
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;
124 // smooth the remaining time by taking the trailing average of the last four values
126 int values = Math.min(_ridx, _remain.length);
127 for (int ii = 0; ii < values; ii++) {
128 remaining += _remain[ii];
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));
141 } else if (_rlabel != null || _newrlab != null) {
142 _rthrottle = new Throttle(1, 1000);
144 _newrlab = _rlabel = null;
154 * Displays the specified status string.
156 public void setStatus (String status, boolean displayError)
158 _status = xlate(status);
159 _displayError = displayError;
164 * Stop the throbbing.
166 public void stopThrob ()
174 public void addNotify ()
181 public void removeNotify ()
184 super.removeNotify();
187 // documentation inherited
189 public void paintComponent (Graphics g)
191 super.paintComponent(g);
192 Graphics2D gfx = (Graphics2D)g;
194 // attempt to draw a background image...
197 img = _bg.getErrorImage();
199 img = _bg.getImage(_progress);
202 gfx.drawImage(img, 0, 0, this);
205 Object oalias = SwingUtil.activateAntiAliasing(gfx);
207 // if we have new labels; lay them out
208 if (_newlab != null) {
213 if (_newplab != null) {
214 _newplab.layout(gfx);
218 if (_newrlab != null) {
219 _newrlab.layout(gfx);
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);
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);
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);
243 if (_label != null) {
244 _label.render(gfx, _ifc.status.x, getStatusY(_label));
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));
254 SwingUtil.restoreAntiAliasing(gfx, oalias);
257 // documentation inherited
259 public Dimension getPreferredSize ()
265 * Update the status label.
267 protected void updateStatusLabel ()
272 String status = _status;
273 if (!_displayError) {
274 for (int ii = 0; ii < _statusDots; ii++) {
279 StringBuilder labelText = new StringBuilder();
280 if (_ifc.displayVersion) {
281 labelText.append("launcher version: "+Build.version());
282 labelText.append("\n");
284 if (_ifc.displayAppbase) {
285 labelText.append("appbase: "+_appbase);
286 labelText.append("\n");
288 labelText.append(status);
290 _newlab = createLabel(labelText.toString(), new Color(_ifc.statusText, true));
291 // set the width of the label to the width specified
292 int width = _ifc.status.width;
294 // unless we had trouble reading that width, in which case use the entire window
297 // but the window itself might not be initialized and have a width of 0
299 _newlab.setTargetWidth(width);
305 * Get the y coordinate of a label in the status area.
307 protected int getStatusY (Label label)
309 // if the status region is higher than the progress region, we
310 // want to align the label with the bottom of its region
311 // rather than the top
312 if (_ifc.status.y > _ifc.progress.y) {
313 return _ifc.status.y;
315 return _ifc.status.y + (_ifc.status.height - label.getSize().height);
319 * Create a label, taking care of adding the shadow if needed.
321 protected Label createLabel (String text, Color color)
323 Label label = new Label(text, color, FONT);
324 if (_ifc.textShadow != 0) {
325 label.setAlternateColor(new Color(_ifc.textShadow, true));
326 label.setStyle(LabelStyleConstants.SHADOW);
331 /** Used by {@link #setStatus}. */
332 protected String xlate (String compoundKey)
334 // to be more efficient about creating unnecessary objects, we
335 // do some checking before splitting
336 int tidx = compoundKey.indexOf('|');
338 return get(compoundKey);
341 String key = compoundKey.substring(0, tidx);
342 String argstr = compoundKey.substring(tidx+1);
343 String[] args = argstr.split("\\|");
344 // unescape and translate the arguments
345 for (int i = 0; i < args.length; i++) {
346 // if the argument is tainted, do no further translation
347 // (it might contain |s or other fun stuff)
348 if (MessageUtil.isTainted(args[i])) {
349 args[i] = MessageUtil.unescape(MessageUtil.untaint(args[i]));
351 args[i] = xlate(MessageUtil.unescape(args[i]));
354 return get(key, args);
358 /** Used by {@link #setStatus}. */
359 protected String get (String key, String[] args)
361 String msg = get(key);
362 if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args);
363 return key + String.valueOf(Arrays.asList(args));
366 /** Used by {@link #setStatus}, and {@link #setProgress}. */
367 protected String get (String key)
369 // if we have no _msgs that means we're probably recovering from a
370 // failure to load the translation messages in the first place, so
371 // just give them their key back because it's probably an english
377 // if this string is tainted, we don't translate it, instead we
378 // simply remove the taint character and return it to the caller
379 if (MessageUtil.isTainted(key)) {
380 return MessageUtil.untaint(key);
383 return _msgs.getString(key);
384 } catch (MissingResourceException mre) {
385 log.warning("Missing translation message '" + key + "'.");
390 public void setAppbase(String appbase) {
394 protected Image _barimg;
395 protected RotatingBackgrounds _bg;
396 protected Dimension _psize;
398 protected ResourceBundle _msgs;
400 protected int _progress = -1;
401 protected String _status;
402 protected int _statusDots = 1;
403 protected boolean _displayError;
404 protected Label _label, _newlab;
405 protected Label _plabel, _newplab;
406 protected Label _rlabel, _newrlab;
408 protected UpdateInterface _ifc;
409 protected Timer _timer;
411 protected long[] _remain = new long[4];
413 protected Throttle _rthrottle = new Throttle(1, 1000L);
415 protected static final Font FONT = new Font("SansSerif", Font.BOLD, 12);
417 public String _appbase;