Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / Nasal / canvas / tooltip.nas
1 var Tooltip = {
2   # default delay (in seconds)
3   DELAY: 4.0,
4   # Constructor
5   #
6   # @param size ([width, height])
7   new: func(size, id = nil)
8   {
9     var m = {
10       parents: [Tooltip, PropertyElement.new(["/sim/gui/canvas", "window"], id)],
11       _listener: nil,
12       _property: nil,
13       _mapping: "",
14       _mappingFunc: nil,
15       _width: 0,
16       _height: 0,
17       _tipId: nil,
18       _slice: 17,
19       _measureText: nil,
20       _measureBB: nil,
21       _hideTimer: nil,
22       _hiding: nil
23     };
24
25     m.setInt("size[0]", size[0]);
26     m.setInt("size[1]", size[1]);
27     m.setBool("visible", 0);
28     m.setInt("z-index", gui.STACK_INDEX["tooltip"]);
29
30     m._hideTimer = maketimer(m.DELAY, m, Tooltip._hideTimeout);
31     m._hideTimer.singleShot = 1;
32
33     return m;
34   },
35   # Destructor
36   del: func
37   {
38     me.parents[1].del();
39     if( me["_canvas"] != nil )
40       me._canvas.del();
41   },
42   # Create the canvas to be used for this Tooltip
43   #
44   # @return The new canvas
45   createCanvas: func()
46   {
47     var size = [
48       me.get("size[0]"),
49       me.get("size[1]")
50     ];
51
52     me._canvas = new({
53       size: [2 * size[0], 2 * size[1]],
54       view: size,
55       placement: {
56         type: "window",
57         index: me._node.getIndex()
58       }
59     });
60
61     # don't do anything with mouse events ourselves
62     me.set("capture-events", 0);
63     me.set("fill", "rgba(255,255,255,0.8)");
64
65     # transparent background
66     me._canvas.setColorBackground(0.0, 0.0, 0.0, 0.0);
67
68     var root = me._canvas.createGroup();
69     me._root = root;
70
71     me._frame =
72       root.createChild("image", "background")
73           .set("src", "gui/images/tooltip.png")
74           .set("slice", me._slice ~ " fill")
75           .setSize(size);
76
77     me._text =
78       root.createChild("text", "tooltip-caption")
79           .setText("Aircraft Help")
80           .setAlignment("left-top")
81           .setFontSize(14)
82           .setFont("LiberationFonts/LiberationSans-Bold.ttf")
83           .setColor(1,1,1)
84           .setDrawMode(Text.TEXT)
85           .setTranslation(me._slice, me._slice)
86           .setMaxWidth(size[0]);
87
88     return me._canvas;
89   },
90
91   setLabel: func(msg)
92   {
93     me._label = msg;
94     me._updateText();
95   },
96
97   setProperty: func(prop)
98   {
99     if (me._property != nil)
100       removelistener(me._listener);
101
102     me._property = prop;
103     if (me._property != nil)
104       me._listener = setlistener(me._property, func { me._updateText(); });
105
106     me._updateText();
107   },
108
109   # specify a string used to compute the width of the tooltip
110   setWidthText: func(txt)
111   {
112     me._measureBB = me._text.setText(txt)
113                             .update()
114                             .getBoundingBox();
115     me._updateBounds();
116   },
117
118   _updateText: func
119   {
120     var msg = me._label;
121     if (me._property != nil) {
122       var val = me._property.getValue() or 0;
123
124       # https://code.google.com/p/flightgear-bugs/issues/detail?id=1454
125       # wrap mapping in 'call' to catch conversion errors
126       var val_mapped = call(me._remapValue, [val], me, nil, var err = []);
127       if( size(err) )
128         printlog(
129           "warn",
130           "Tooltip: failed to remap " ~ debug.string(me._property, 0) ~ ":\n"
131           ~ debug.string(err, 0)
132         );
133
134       msg = sprintf(me._label, val_mapped or val);
135     }
136
137     me._text.setText(msg);
138     me._updateBounds();
139   },
140
141   _updateBounds: func
142   {
143     var max_width = me.get("size[0]") - 2 * me._slice;
144     me._text.setMaxWidth(max_width);
145
146     # compute the bounds
147     var text_bb = me._text.update().getBoundingBox();
148     var width = text_bb[2];
149     var height = text_bb[3];
150
151     if( me._measureBB != nil )
152     {
153       width = math.max(width, me._measureBB[2]);
154       height = math.max(height, me._measureBB[3]);
155     }
156
157     if( width > max_width )
158       width = max_width;
159
160     me._width = width + 2 * me._slice;
161     me._height = height + 2 * me._slice;
162     me._frame.setSize(me._width, me._height)
163              .update();
164   },
165
166   _remapValue: func(val)
167   {
168     if (me._mapping == "") return val;
169     if (me._mapping == "percent") return int(val * 100);
170
171     # TODO - translate me!
172     if (me._mapping == "on-off") return (val == 1) ? "ON" : "OFF";
173     if (me._mapping == "arm-disarm") return (val == 1) ? "ARMED" : "DISARMED";
174
175     # provide both 'senses' of the flag here
176     if (me._mapping == "up-down") return (val == 1) ? "UP" : "DOWN";
177     if (me._mapping == "down-up") return (val == 1) ? "DOWN" : "UP";
178     if (me._mapping == "open-close") return (val == 1) ? "OPEN" : "CLOSED";
179     if (me._mapping == "close-open") return (val == 1) ? "CLOSED" : "OPEN";
180
181     if (me._mapping == "heading") return geo.normdeg(val);
182     if (me._mapping == "nasal") return me._mappingFunc(val);
183
184     return val;
185   },
186
187   setMapping: func(mapping, f = nil)
188   {
189     me._mapping = mapping;
190     me._mappingFunc = f;
191     me._updateText();
192   },
193
194   setTooltipId: func(tipId)
195   {
196     me._tipId = tipId;
197     if ((tipId != nil) and me._hiding) {
198       me._hideTimer.stop();
199       me._hiding = 0;
200     }
201   },
202
203   getTooltipId: func { me._tipId; },
204
205   # Get the displayed canvas
206   getCanvas: func()
207   {
208     return me['_canvas'];
209   },
210
211   setPosition: func(x, y)
212   {
213     me.setInt("x", x + 10);
214     me.setInt("y", y + 10);
215   },
216
217   show: func()
218   {
219     # don't show if undefined
220     if (me._tipId == nil) return;
221
222     if (me._hiding) {
223       me._hideTimer.stop();
224       me._hiding = 0;
225     }
226
227     if (!me.isVisible()) {
228       me.setBool("visible", 1);
229     }
230   },
231
232   showMessage: func(timeout = nil, node = nil)
233   {
234     if(var y = me._haveNode(node, 'y') != nil ) {
235       me.setInt("y", y);
236     } else {
237       me.setInt("y", getprop('/sim/startup/ysize') * 0.2);
238     }
239     if(var x = me._haveNode(node, 'x')  != nil) {
240       me.setInt("x", x);
241     } else {
242       var screenW = getprop('/sim/startup/xsize');
243       me.setInt("x", (screenW - me._width) * 0.5);
244     }
245     me.show();
246     # https://code.google.com/p/flightgear-bugs/issues/detail?id=1273
247     # when tooltip is shown for some other reason, ensure it stays for
248     # the full delay (unless replaced). Don't allow the update-hover
249     # code path to hide() with the shorter delay.
250     me._hiding = 1;
251     me._hideTimer.restart(timeout or me.DELAY);
252   },
253
254   _haveNode: func(node, key) {
255     if(node == nil ) return nil;
256     var value = num(node.getValue(key) );
257     return value;
258   },
259
260   hide: func()
261   {
262     # this gets run repeatedly during mouse-moves
263     if (me._hiding) return;
264     me._hiding = 1;
265
266     me._hideTimer.restart(0.5);
267   },
268
269   hideNow: func()
270   {
271     me._hideTimer.stop();
272     me._hideTimeout();
273   },
274
275   isVisible: func
276   {
277     return me.getBool("visible");
278   },
279
280   fadeIn: func()
281   {
282     me.show();
283   },
284
285   fadeOut: func()
286   {
287     me.hide();
288   },
289
290 # private:
291   _hideTimeout: func()
292   {
293     me.setBool("visible", 0);
294     me._hiding = 0;
295   }
296 };
297
298 var tooltip = canvas.Tooltip.new([300, 100]);
299 tooltip.createCanvas();
300
301 var innerSetTooltip = func(node)
302 {
303    tooltip.setLabel(cmdarg().getNode('label').getValue());
304    var measure = cmdarg().getNode('measure-text');
305    if (measure != nil) {
306        tooltip.setWidthText(measure.getValue());
307    } else {
308        tooltip.setWidthText(nil);
309    }
310
311    var propPath = cmdarg().getNode('property');
312    if (propPath != nil) {
313      var n = props.globals.getNode(propPath.getValue());
314      tooltip.setProperty(n);
315    } else {
316       tooltip.setProperty(nil);
317    }
318
319    var mapping = cmdarg().getNode('mapping');
320    if (mapping != nil) {
321      var m = mapping.getValue();
322      var f = nil;
323      if (m == 'nasal') {
324        f = compile(cmdarg().getNode('script').getValue());
325      }
326
327      tooltip.setMapping(m, f);
328    } else {
329      tooltip.setMapping(nil);
330   }
331 }
332
333 var setTooltip = func(node)
334 {
335   var tipId = cmdarg().getNode('tooltip-id').getValue();
336   if (tooltip.getTooltipId() == tipId) {
337     return; # nothing more to do
338   }
339
340   var x = cmdarg().getNode('x').getValue();
341   var y = cmdarg().getNode('y').getValue();
342
343   var screenHeight = getprop('/sim/startup/ysize');
344   tooltip.setPosition(x, screenHeight - y);
345   tooltip.setTooltipId(tipId);
346   innerSetTooltip(node);
347
348   # don't actually show here, we do that response to tooltip-timeout
349   # so this is just getting ready
350 }
351
352 var showTooltip = func(node)
353 {
354   var r = node.getNode("reason");
355   if ((r != nil) and (r.getValue() == "click")) {
356     # click triggering tooltip, show immediately
357     tooltip.show();
358   } else {
359     tooltip.fadeIn();
360   }
361 }
362
363 var updateHover = func(node)
364 {
365   tooltip.setTooltipId(nil);
366
367   # if not shown, nothing to do here
368   if (!tooltip.isVisible()) return;
369
370   # reset cursor to standard
371   tooltip.fadeOut();
372
373 }
374
375 var showMessage = func(node)
376 {
377   var msgId = node.getNode("id");
378   tooltip.setTooltipId((msgId == nil) ? 'msg' : msgId.getValue());
379   innerSetTooltip(node);
380
381   var timeout = node.getNode("delay");
382   tooltip.showMessage( timeout != nil ? timeout.getValue() : nil, node);
383 }
384
385 var clearMessage = func(node)
386 {
387   var msgId = node.getNode("id");
388   if (tooltip.getTooltipId() != msgId.getValue()) return;
389   tooltip.hideNow();
390 }
391
392 addcommand("update-hover", updateHover);
393 addcommand("set-tooltip", setTooltip);
394 addcommand("tooltip-timeout", showTooltip);
395 addcommand("show-message", showMessage);
396 addcommand("clear-message", clearMessage);