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