JAL-3449 don't assume install4j jar in classpath of getdown
[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.data.Build;
32 import com.threerings.getdown.util.MessageUtil;
33 import com.threerings.getdown.util.Rectangle;
34 import com.threerings.getdown.util.StringUtil;
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 != null && !_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 != null && !_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         if (_ifc == null) {
269                 return;
270         }
271         String status = _status;
272         if (!_displayError) {
273             for (int ii = 0; ii < _statusDots; ii++) {
274                 status += " .";
275             }
276         }
277  
278         StringBuilder labelText = new StringBuilder();
279         if (_ifc.displayVersion) {
280                 labelText.append("launcher version: " + Build.version());
281                 labelText.append("\n");
282                 labelText.append("install4j version: " + i4jVersion);
283                 labelText.append("\n");
284                 labelText.append("installer version: " + System.getProperty("installer_template_version"));
285                 labelText.append("\n");
286         }
287         if (_ifc.displayAppbase) {
288                 labelText.append("appbase: " + _appbase);
289                 labelText.append("\n");
290         }
291         labelText.append(status); 
292         
293         _newlab = createLabel(labelText.toString(), new Color(_ifc.statusText, true));
294         // set the width of the label to the width specified
295         int width = _ifc.status.width;
296         if (width == 0) {
297             // unless we had trouble reading that width, in which case use the entire window
298             width = getWidth();
299         }
300         // but the window itself might not be initialized and have a width of 0
301         if (width > 0) {
302             _newlab.setTargetWidth(width);
303         }
304         repaint();
305     }
306
307     /**
308      * Get the y coordinate of a label in the status area.
309      */
310     protected int getStatusY (Label label)
311     {
312         // if the status region is higher than the progress region, we
313         // want to align the label with the bottom of its region
314         // rather than the top
315         if (_ifc.status.y > _ifc.progress.y) {
316             return _ifc.status.y;
317         }
318         return _ifc.status.y + (_ifc.status.height - label.getSize().height);
319     }
320
321     /**
322      * Create a label, taking care of adding the shadow if needed.
323      */
324     protected Label createLabel (String text, Color color)
325     {
326         Label label = new Label(text, color, FONT);
327         if (_ifc.textShadow != 0) {
328             label.setAlternateColor(new Color(_ifc.textShadow, true));
329             label.setStyle(LabelStyleConstants.SHADOW);
330         }
331         return label;
332     }
333
334     /** Used by {@link #setStatus}. */
335     protected String xlate (String compoundKey)
336     {
337         // to be more efficient about creating unnecessary objects, we
338         // do some checking before splitting
339         int tidx = compoundKey.indexOf('|');
340         if (tidx == -1) {
341             return get(compoundKey);
342
343         } else {
344             String key = compoundKey.substring(0, tidx);
345             String argstr = compoundKey.substring(tidx+1);
346             String[] args = argstr.split("\\|");
347             // unescape and translate the arguments
348             for (int i = 0; i < args.length; i++) {
349                 // if the argument is tainted, do no further translation
350                 // (it might contain |s or other fun stuff)
351                 if (MessageUtil.isTainted(args[i])) {
352                     args[i] = MessageUtil.unescape(MessageUtil.untaint(args[i]));
353                 } else {
354                     args[i] = xlate(MessageUtil.unescape(args[i]));
355                 }
356             }
357             return get(key, args);
358         }
359     }
360
361     /** Used by {@link #setStatus}. */
362     protected String get (String key, String[] args)
363     {
364         String msg = get(key);
365         if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args);
366         return key + String.valueOf(Arrays.asList(args));
367     }
368
369     /** Used by {@link #setStatus}, and {@link #setProgress}. */
370     protected String get (String key)
371     {
372         // if we have no _msgs that means we're probably recovering from a
373         // failure to load the translation messages in the first place, so
374         // just give them their key back because it's probably an english
375         // string; whee!
376         if (_msgs == null) {
377             return key;
378         }
379
380         // if this string is tainted, we don't translate it, instead we
381         // simply remove the taint character and return it to the caller
382         if (MessageUtil.isTainted(key)) {
383             return MessageUtil.untaint(key);
384         }
385         try {
386             return _msgs.getString(key);
387         } catch (MissingResourceException mre) {
388             log.warning("Missing translation message '" + key + "'.");
389             return key;
390         }
391     }
392     
393     public void setAppbase(String appbase) {
394         _appbase = appbase;
395     }
396
397     protected Image _barimg;
398     protected RotatingBackgrounds _bg;
399     protected Dimension _psize;
400
401     protected ResourceBundle _msgs;
402
403     protected int _progress = -1;
404     protected String _status;
405     protected int _statusDots = 1;
406     protected boolean _displayError;
407     protected Label _label, _newlab;
408     protected Label _plabel, _newplab;
409     protected Label _rlabel, _newrlab;
410
411     protected UpdateInterface _ifc;
412     protected Timer _timer;
413
414     protected long[] _remain = new long[4];
415     protected int _ridx;
416     protected Throttle _rthrottle = new Throttle(1, 1000L);
417
418     protected static final Font FONT = new Font("SansSerif", Font.BOLD, 12);
419     
420     public String _appbase;
421     
422     public static String i4jVersion = null;
423 }