JAL-3322 New progress bar. Added option to report appbase in launcher window. Added...
[jalview.git] / getdown / src / getdown / launcher / src / main / java / com / threerings / getdown / launcher / StatusPanel.java
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.UpdateInterface;
31 import com.threerings.getdown.util.MessageUtil;
32 import com.threerings.getdown.util.Rectangle;
33 import com.threerings.getdown.util.StringUtil;
34
35 import static com.threerings.getdown.Log.log;
36
37 /**
38  * Displays download and patching status.
39  */
40 public final class StatusPanel extends JComponent
41     implements ImageObserver
42 {
43     public StatusPanel (ResourceBundle msgs)
44     {
45         _msgs = msgs;
46
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.
54                         updateStatusLabel();
55                     }
56                 }
57             });
58     }
59
60     public void init (UpdateInterface ifc, RotatingBackgrounds bg, Image barimg)
61     {
62         _ifc = ifc;
63         _bg = bg;
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);
72         } else {
73             _psize = new Dimension(width, height);
74         }
75         _barimg = barimg;
76         invalidate();
77     }
78
79     @Override
80     public boolean imageUpdate (Image img, int infoflags, int x, int y, int width, int height)
81     {
82         boolean updated = false;
83         if ((infoflags & WIDTH) != 0) {
84             _psize.width = width;
85             updated = true;
86         }
87         if ((infoflags & HEIGHT) != 0) {
88             _psize.height = height;
89             updated = true;
90         }
91         if (updated) {
92             invalidate();
93             setSize(_psize);
94             getParent().setSize(_psize);
95         }
96         return (infoflags & ALLBITS) == 0;
97     }
98
99     /**
100      * Adjusts the progress display to the specified percentage.
101      */
102     public void setProgress (int percent, long remaining)
103     {
104         boolean needsRepaint = false;
105
106         // maybe update the progress label
107         if (_progress != percent) {
108             _progress = percent;
109             if (!_ifc.hideProgressText) {
110                 String msg = MessageFormat.format(get("m.complete"), percent);
111                 _newplab = createLabel(msg, new Color(_ifc.progressText, true));
112             }
113             needsRepaint = true;
114         }
115
116         // maybe update the remaining label
117         if (remaining > 1) {
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;
121             }
122
123             // smooth the remaining time by taking the trailing average of the last four values
124             remaining = 0;
125             int values = Math.min(_ridx, _remain.length);
126             for (int ii = 0; ii < values; ii++) {
127                 remaining += _remain[ii];
128             }
129             remaining /= values;
130
131             if (!_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));
137             }
138             needsRepaint = true;
139
140         } else if (_rlabel != null || _newrlab != null) {
141             _rthrottle = new Throttle(1, 1000);
142             _ridx = 0;
143             _newrlab = _rlabel = null;
144             needsRepaint = true;
145         }
146
147         if (needsRepaint) {
148             repaint();
149         }
150     }
151
152     /**
153      * Displays the specified status string.
154      */
155     public void setStatus (String status, boolean displayError)
156     {
157         _status = xlate(status);
158         _displayError = displayError;
159         updateStatusLabel();
160     }
161
162     /**
163      * Stop the throbbing.
164      */
165     public void stopThrob ()
166     {
167         _timer.stop();
168         _statusDots = 3;
169         updateStatusLabel();
170     }
171
172     @Override
173     public void addNotify ()
174     {
175         super.addNotify();
176         _timer.start();
177     }
178
179     @Override
180     public void removeNotify ()
181     {
182         _timer.stop();
183         super.removeNotify();
184     }
185
186     // documentation inherited
187     @Override
188     public void paintComponent (Graphics g)
189     {
190         super.paintComponent(g);
191         Graphics2D gfx = (Graphics2D)g;
192
193         // attempt to draw a background image...
194         Image img;
195         if (_displayError) {
196             img = _bg.getErrorImage();
197         } else {
198             img = _bg.getImage(_progress);
199         }
200         if (img != null) {
201             gfx.drawImage(img, 0, 0, this);
202         }
203
204         Object oalias = SwingUtil.activateAntiAliasing(gfx);
205
206         // if we have new labels; lay them out
207         if (_newlab != null) {
208             _newlab.layout(gfx);
209             _label = _newlab;
210             _newlab = null;
211         }
212         if (_newplab != null) {
213             _newplab.layout(gfx);
214             _plabel = _newplab;
215             _newplab = null;
216         }
217         if (_newrlab != null) {
218             _newrlab.layout(gfx);
219             _rlabel = _newrlab;
220             _newrlab = null;
221         }
222
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);
228             gfx.setClip(null);
229         } else {
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);
234         }
235
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);
240         }
241
242         if (_label != null) {
243             _label.render(gfx, _ifc.status.x, getStatusY(_label));
244         }
245
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));
251         }
252
253         SwingUtil.restoreAntiAliasing(gfx, oalias);
254     }
255
256     // documentation inherited
257     @Override
258     public Dimension getPreferredSize ()
259     {
260         return _psize;
261     }
262
263     /**
264      * Update the status label.
265      */
266     protected void updateStatusLabel ()
267     {
268         String status = _status;
269         if (!_displayError) {
270             for (int ii = 0; ii < _statusDots; ii++) {
271                 status += " .";
272             }
273         }
274         
275         StringBuilder labelText = new StringBuilder();
276         if (_ifc.displayAppbase) {
277                 labelText.append(" appbase: "+_appbase);
278                 labelText.append("\n");
279         }
280         labelText.append(status); 
281         
282         _newlab = createLabel(labelText.toString(), new Color(_ifc.statusText, true));
283         // set the width of the label to the width specified
284         int width = _ifc.status.width;
285         if (width == 0) {
286             // unless we had trouble reading that width, in which case use the entire window
287             width = getWidth();
288         }
289         // but the window itself might not be initialized and have a width of 0
290         if (width > 0) {
291             _newlab.setTargetWidth(width);
292         }
293         repaint();
294     }
295
296     /**
297      * Get the y coordinate of a label in the status area.
298      */
299     protected int getStatusY (Label label)
300     {
301         // if the status region is higher than the progress region, we
302         // want to align the label with the bottom of its region
303         // rather than the top
304         if (_ifc.status.y > _ifc.progress.y) {
305             return _ifc.status.y;
306         }
307         return _ifc.status.y + (_ifc.status.height - label.getSize().height);
308     }
309
310     /**
311      * Create a label, taking care of adding the shadow if needed.
312      */
313     protected Label createLabel (String text, Color color)
314     {
315         Label label = new Label(text, color, FONT);
316         if (_ifc.textShadow != 0) {
317             label.setAlternateColor(new Color(_ifc.textShadow, true));
318             label.setStyle(LabelStyleConstants.SHADOW);
319         }
320         return label;
321     }
322
323     /** Used by {@link #setStatus}. */
324     protected String xlate (String compoundKey)
325     {
326         // to be more efficient about creating unnecessary objects, we
327         // do some checking before splitting
328         int tidx = compoundKey.indexOf('|');
329         if (tidx == -1) {
330             return get(compoundKey);
331
332         } else {
333             String key = compoundKey.substring(0, tidx);
334             String argstr = compoundKey.substring(tidx+1);
335             String[] args = argstr.split("\\|");
336             // unescape and translate the arguments
337             for (int i = 0; i < args.length; i++) {
338                 // if the argument is tainted, do no further translation
339                 // (it might contain |s or other fun stuff)
340                 if (MessageUtil.isTainted(args[i])) {
341                     args[i] = MessageUtil.unescape(MessageUtil.untaint(args[i]));
342                 } else {
343                     args[i] = xlate(MessageUtil.unescape(args[i]));
344                 }
345             }
346             return get(key, args);
347         }
348     }
349
350     /** Used by {@link #setStatus}. */
351     protected String get (String key, String[] args)
352     {
353         String msg = get(key);
354         if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args);
355         return key + String.valueOf(Arrays.asList(args));
356     }
357
358     /** Used by {@link #setStatus}, and {@link #setProgress}. */
359     protected String get (String key)
360     {
361         // if we have no _msgs that means we're probably recovering from a
362         // failure to load the translation messages in the first place, so
363         // just give them their key back because it's probably an english
364         // string; whee!
365         if (_msgs == null) {
366             return key;
367         }
368
369         // if this string is tainted, we don't translate it, instead we
370         // simply remove the taint character and return it to the caller
371         if (MessageUtil.isTainted(key)) {
372             return MessageUtil.untaint(key);
373         }
374         try {
375             return _msgs.getString(key);
376         } catch (MissingResourceException mre) {
377             log.warning("Missing translation message '" + key + "'.");
378             return key;
379         }
380     }
381     
382     public void setAppbase(String appbase) {
383         _appbase = appbase;
384     }
385
386     protected Image _barimg;
387     protected RotatingBackgrounds _bg;
388     protected Dimension _psize;
389
390     protected ResourceBundle _msgs;
391
392     protected int _progress = -1;
393     protected String _status;
394     protected int _statusDots = 1;
395     protected boolean _displayError;
396     protected Label _label, _newlab;
397     protected Label _plabel, _newplab;
398     protected Label _rlabel, _newrlab;
399
400     protected UpdateInterface _ifc;
401     protected Timer _timer;
402
403     protected long[] _remain = new long[4];
404     protected int _ridx;
405     protected Throttle _rthrottle = new Throttle(1, 1000L);
406
407     protected static final Font FONT = new Font("SansSerif", Font.BOLD, 12);
408     
409     public String _appbase;
410 }