// // 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); }