--- /dev/null
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.launcher;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.image.ImageObserver;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import javax.swing.JComponent;
+import javax.swing.Timer;
+
+import com.samskivert.swing.Label;
+import com.samskivert.swing.LabelStyleConstants;
+import com.samskivert.swing.util.SwingUtil;
+import com.samskivert.util.Throttle;
+
+import com.threerings.getdown.data.Application.UpdateInterface;
+import com.threerings.getdown.util.MessageUtil;
+import com.threerings.getdown.util.Rectangle;
+import com.threerings.getdown.util.StringUtil;
+
+import static com.threerings.getdown.Log.log;
+
+/**
+ * Displays download and patching status.
+ */
+public final class StatusPanel extends JComponent
+ implements ImageObserver
+{
+ public StatusPanel (ResourceBundle msgs)
+ {
+ _msgs = msgs;
+
+ // Add a bit of "throbbing" to the display by updating the number of dots displayed after
+ // our status. This lets users know things are still working.
+ _timer = new Timer(1000,
+ new ActionListener() {
+ public void actionPerformed (ActionEvent event) {
+ if (_status != null && !_displayError) {
+ _statusDots = (_statusDots % 3) + 1; // 1, 2, 3, 1, 2, 3, etc.
+ updateStatusLabel();
+ }
+ }
+ });
+ }
+
+ public void init (UpdateInterface ifc, RotatingBackgrounds bg, Image barimg)
+ {
+ _ifc = ifc;
+ _bg = bg;
+ Image img = _bg.getImage(_progress);
+ int width = img == null ? -1 : img.getWidth(this);
+ int height = img == null ? -1 : img.getHeight(this);
+ if (width == -1 || height == -1) {
+ Rectangle bounds = ifc.progress.union(ifc.status);
+ // assume the x inset defines the frame padding; add it on the left, right, and bottom
+ _psize = new Dimension(bounds.x + bounds.width + bounds.x,
+ bounds.y + bounds.height + bounds.x);
+ } else {
+ _psize = new Dimension(width, height);
+ }
+ _barimg = barimg;
+ invalidate();
+ }
+
+ @Override
+ public boolean imageUpdate (Image img, int infoflags, int x, int y, int width, int height)
+ {
+ boolean updated = false;
+ if ((infoflags & WIDTH) != 0) {
+ _psize.width = width;
+ updated = true;
+ }
+ if ((infoflags & HEIGHT) != 0) {
+ _psize.height = height;
+ updated = true;
+ }
+ if (updated) {
+ invalidate();
+ setSize(_psize);
+ getParent().setSize(_psize);
+ }
+ return (infoflags & ALLBITS) == 0;
+ }
+
+ /**
+ * Adjusts the progress display to the specified percentage.
+ */
+ public void setProgress (int percent, long remaining)
+ {
+ boolean needsRepaint = false;
+
+ // maybe update the progress label
+ if (_progress != percent) {
+ _progress = percent;
+ if (!_ifc.hideProgressText) {
+ String msg = MessageFormat.format(get("m.complete"), percent);
+ _newplab = createLabel(msg, new Color(_ifc.progressText, true));
+ }
+ needsRepaint = true;
+ }
+
+ // maybe update the remaining label
+ if (remaining > 1) {
+ // skip this estimate if it's been less than a second since our last one came in
+ if (!_rthrottle.throttleOp()) {
+ _remain[_ridx++%_remain.length] = remaining;
+ }
+
+ // smooth the remaining time by taking the trailing average of the last four values
+ remaining = 0;
+ int values = Math.min(_ridx, _remain.length);
+ for (int ii = 0; ii < values; ii++) {
+ remaining += _remain[ii];
+ }
+ remaining /= values;
+
+ if (!_ifc.hideProgressText) {
+ // now compute our display value
+ int minutes = (int)(remaining / 60), seconds = (int)(remaining % 60);
+ String remstr = minutes + ":" + ((seconds < 10) ? "0" : "") + seconds;
+ String msg = MessageFormat.format(get("m.remain"), remstr);
+ _newrlab = createLabel(msg, new Color(_ifc.statusText, true));
+ }
+ needsRepaint = true;
+
+ } else if (_rlabel != null || _newrlab != null) {
+ _rthrottle = new Throttle(1, 1000);
+ _ridx = 0;
+ _newrlab = _rlabel = null;
+ needsRepaint = true;
+ }
+
+ if (needsRepaint) {
+ repaint();
+ }
+ }
+
+ /**
+ * Displays the specified status string.
+ */
+ public void setStatus (String status, boolean displayError)
+ {
+ _status = xlate(status);
+ _displayError = displayError;
+ updateStatusLabel();
+ }
+
+ /**
+ * Stop the throbbing.
+ */
+ public void stopThrob ()
+ {
+ _timer.stop();
+ _statusDots = 3;
+ updateStatusLabel();
+ }
+
+ @Override
+ public void addNotify ()
+ {
+ super.addNotify();
+ _timer.start();
+ }
+
+ @Override
+ public void removeNotify ()
+ {
+ _timer.stop();
+ super.removeNotify();
+ }
+
+ // documentation inherited
+ @Override
+ public void paintComponent (Graphics g)
+ {
+ super.paintComponent(g);
+ Graphics2D gfx = (Graphics2D)g;
+
+ // attempt to draw a background image...
+ Image img;
+ if (_displayError) {
+ img = _bg.getErrorImage();
+ } else {
+ img = _bg.getImage(_progress);
+ }
+ if (img != null) {
+ gfx.drawImage(img, 0, 0, this);
+ }
+
+ Object oalias = SwingUtil.activateAntiAliasing(gfx);
+
+ // if we have new labels; lay them out
+ if (_newlab != null) {
+ _newlab.layout(gfx);
+ _label = _newlab;
+ _newlab = null;
+ }
+ if (_newplab != null) {
+ _newplab.layout(gfx);
+ _plabel = _newplab;
+ _newplab = null;
+ }
+ if (_newrlab != null) {
+ _newrlab.layout(gfx);
+ _rlabel = _newrlab;
+ _newrlab = null;
+ }
+
+ if (_barimg != null) {
+ gfx.setClip(_ifc.progress.x, _ifc.progress.y,
+ _progress * _ifc.progress.width / 100,
+ _ifc.progress.height);
+ gfx.drawImage(_barimg, _ifc.progress.x, _ifc.progress.y, null);
+ gfx.setClip(null);
+ } else {
+ gfx.setColor(new Color(_ifc.progressBar, true));
+ gfx.fillRect(_ifc.progress.x, _ifc.progress.y,
+ _progress * _ifc.progress.width / 100,
+ _ifc.progress.height);
+ }
+
+ if (_plabel != null) {
+ int xmarg = (_ifc.progress.width - _plabel.getSize().width)/2;
+ int ymarg = (_ifc.progress.height - _plabel.getSize().height)/2;
+ _plabel.render(gfx, _ifc.progress.x + xmarg, _ifc.progress.y + ymarg);
+ }
+
+ if (_label != null) {
+ _label.render(gfx, _ifc.status.x, getStatusY(_label));
+ }
+
+ if (_rlabel != null) {
+ // put the remaining label at the end of the status area. This could be dangerous
+ // but I think the only time we would display it is with small statuses.
+ int x = _ifc.status.x + _ifc.status.width - _rlabel.getSize().width;
+ _rlabel.render(gfx, x, getStatusY(_rlabel));
+ }
+
+ SwingUtil.restoreAntiAliasing(gfx, oalias);
+ }
+
+ // documentation inherited
+ @Override
+ public Dimension getPreferredSize ()
+ {
+ return _psize;
+ }
+
+ /**
+ * Update the status label.
+ */
+ protected void updateStatusLabel ()
+ {
+ String status = _status;
+ if (!_displayError) {
+ for (int ii = 0; ii < _statusDots; ii++) {
+ status += " .";
+ }
+ }
+ _newlab = createLabel(status, new Color(_ifc.statusText, true));
+ // set the width of the label to the width specified
+ int width = _ifc.status.width;
+ if (width == 0) {
+ // unless we had trouble reading that width, in which case use the entire window
+ width = getWidth();
+ }
+ // but the window itself might not be initialized and have a width of 0
+ if (width > 0) {
+ _newlab.setTargetWidth(width);
+ }
+ repaint();
+ }
+
+ /**
+ * Get the y coordinate of a label in the status area.
+ */
+ protected int getStatusY (Label label)
+ {
+ // if the status region is higher than the progress region, we
+ // want to align the label with the bottom of its region
+ // rather than the top
+ if (_ifc.status.y > _ifc.progress.y) {
+ return _ifc.status.y;
+ }
+ return _ifc.status.y + (_ifc.status.height - label.getSize().height);
+ }
+
+ /**
+ * Create a label, taking care of adding the shadow if needed.
+ */
+ protected Label createLabel (String text, Color color)
+ {
+ Label label = new Label(text, color, FONT);
+ if (_ifc.textShadow != 0) {
+ label.setAlternateColor(new Color(_ifc.textShadow, true));
+ label.setStyle(LabelStyleConstants.SHADOW);
+ }
+ return label;
+ }
+
+ /** Used by {@link #setStatus}. */
+ protected String xlate (String compoundKey)
+ {
+ // to be more efficient about creating unnecessary objects, we
+ // do some checking before splitting
+ int tidx = compoundKey.indexOf('|');
+ if (tidx == -1) {
+ return get(compoundKey);
+
+ } else {
+ String key = compoundKey.substring(0, tidx);
+ String argstr = compoundKey.substring(tidx+1);
+ String[] args = argstr.split("\\|");
+ // unescape and translate the arguments
+ for (int i = 0; i < args.length; i++) {
+ // if the argument is tainted, do no further translation
+ // (it might contain |s or other fun stuff)
+ if (MessageUtil.isTainted(args[i])) {
+ args[i] = MessageUtil.unescape(MessageUtil.untaint(args[i]));
+ } else {
+ args[i] = xlate(MessageUtil.unescape(args[i]));
+ }
+ }
+ return get(key, args);
+ }
+ }
+
+ /** Used by {@link #setStatus}. */
+ protected String get (String key, String[] args)
+ {
+ String msg = get(key);
+ if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args);
+ return key + String.valueOf(Arrays.asList(args));
+ }
+
+ /** Used by {@link #setStatus}, and {@link #setProgress}. */
+ protected String get (String key)
+ {
+ // if we have no _msgs that means we're probably recovering from a
+ // failure to load the translation messages in the first place, so
+ // just give them their key back because it's probably an english
+ // string; whee!
+ if (_msgs == null) {
+ return key;
+ }
+
+ // if this string is tainted, we don't translate it, instead we
+ // simply remove the taint character and return it to the caller
+ if (MessageUtil.isTainted(key)) {
+ return MessageUtil.untaint(key);
+ }
+ try {
+ return _msgs.getString(key);
+ } catch (MissingResourceException mre) {
+ log.warning("Missing translation message '" + key + "'.");
+ return key;
+ }
+ }
+
+ protected Image _barimg;
+ protected RotatingBackgrounds _bg;
+ protected Dimension _psize;
+
+ protected ResourceBundle _msgs;
+
+ protected int _progress = -1;
+ protected String _status;
+ protected int _statusDots = 1;
+ protected boolean _displayError;
+ protected Label _label, _newlab;
+ protected Label _plabel, _newplab;
+ protected Label _rlabel, _newrlab;
+
+ protected UpdateInterface _ifc;
+ protected Timer _timer;
+
+ protected long[] _remain = new long[4];
+ protected int _ridx;
+ protected Throttle _rthrottle = new Throttle(1, 1000L);
+
+ protected static final Font FONT = new Font("SansSerif", Font.BOLD, 12);
+}