2 #==============================================================================
6 # convert string for output; replaces tabs by spaces, and skips
7 # delimiters and the voice part in "{text|voice}" constructions
9 var sanitize = func(s, newline = 0) {
13 for (var i = 0; i < size(s); i += 1) {
23 elsif (c == `\n` and newline)
38 #------------------------------------------------------------------------------
39 # Class that manages a dialog with fixed number of lines, where you can push in
40 # text at the bottom, which then (optionally) scrolls up after some time.
44 # var window = screen.window.new();
45 # window.write("message in the middle of the screen");
50 # var window = screen.window.new(nil, -100, 3, 10);
51 # window.fg = [1, 1, 1, 1]; # choose white default color
52 # window.align = "left";
54 # window.write("first line");
55 # window.write("second line (red)", 1, 0, 0);
62 # positive coords position relative to the left/lower corner,
63 # negative coords from the right/upper corner, nil centers
64 # maxlines ... max number of displayed lines; if more are pushed into the
65 # screen, then the ones on top fall off
66 # autoscroll ... seconds that each line should be shown; can be less if
67 # a message falls off; if 0 then don't scroll at all
71 new : func(x = nil, y = nil, maxlines = 10, autoscroll = 10) {
72 var m = { parents: [window] };
77 m.maxlines = maxlines;
78 m.autoscroll = autoscroll; # display time in seconds
79 m.sticky = 0; # reopens on old place
81 m.bg = [0, 0, 0, 0]; # background color
82 m.fg = [0.9, 0.4, 0.2, 1]; # default foreground color
83 m.align = "center"; # "left", "right", "center"
86 m.name = "__screen_window_" ~ (window.id += 1) ~ "__";
90 m.namenode = props.Node.new({ "dialog-name": m.name });
92 m.MAX_BUFFER_SIZE = 50;
93 setlistener("/sim/startup/xsize", func m._redraw_());
94 setlistener("/sim/startup/ysize", func m._redraw_());
97 write : func(msg, r = nil, g = nil, b = nil, a = nil) {
98 if (me.namenode == nil)
100 if (size(me.writebuffer) > me.MAX_BUFFER_SIZE)
111 foreach (var line; split("\n", string.trim(msg ~ ""))) {
112 line = sanitize(string.trim(line));
113 append(lines, [line, r, g, b, a]);
115 if (size(me.writebuffer) == 0)
116 settimer(func { me._write_(); } , 0, 1);
117 append(me.writebuffer, lines);
125 if (me.dialog != nil)
128 me.dialog = gui.Widget.new();
129 me.dialog.set("name", me.name);
131 me.dialog.set("x", me.x);
133 me.dialog.set("y", me.y);
134 me.dialog.set("layout", "vbox");
135 me.dialog.set("default-padding", 2);
137 me.dialog.setFont(me.font);
138 elsif (theme_font != nil)
139 me.dialog.setFont(theme_font);
141 me.dialog.setColor(me.bg[0], me.bg[1], me.bg[2], me.bg[3]);
143 foreach (var line; me.lines) {
144 var w = me.dialog.addChild("text");
145 w.set("halign", me.align);
146 w.set("label", line[0]);
147 w.setColor(line[1], line[2], line[3], line[4]);
150 fgcommand("dialog-new", me.dialog.prop());
151 fgcommand("dialog-show", me.namenode);
154 fgcommand("dialog-close", me.namenode);
155 if (me.dialog != nil and me.sticky) {
156 me.x = me.dialog.prop().getNode("lastx").getValue();
157 me.y = me.dialog.prop().getNode("lasty").getValue();
161 if (size(me.writebuffer) == 0)
163 foreach (var msg; me.writebuffer) {
164 foreach (var line; msg) {
165 append(me.lines, line);
166 if (size(me.lines) > me.maxlines) {
167 me.lines = subvec(me.lines, 1);
172 settimer(func me._timeout_(), me.autoscroll, 1);
179 if (me.skiptimer > 0) {
183 if (size(me.lines) > 1) {
184 me.lines = subvec(me.lines, 1);
193 if (me.dialog != nil) {
203 #------------------------------------------------------------------------------
204 # Class that manages a dialog, which displays an arbitrary number of properties
205 # periodically updating the values. Property names are abbreviated to the
206 # shortest possible unique part.
210 # var dpy = screen.display.new(20, 10); # x/y coordinate
211 # dpy.setcolor(1, 0, 1); # magenta (default: white)
212 # dpy.setfont("SANS_12B"); # see $FG_ROOT/gui/styles/*.xml
214 # dpy.add("/position/latitude-deg", "/position/longitude-deg");
215 # dpy.add(props.globals.getNode("/orientation").getChildren());
218 # The add() method takes one or more property paths or props.Nodes, or a vector
219 # containing those, or a hash with properties, or vectors with properties, etc.
220 # Internal "public" parameters may be set directly:
222 # dpy.interval = 0; # update every frame
223 # dpy.format = "%.3g"; # max. 3 digits fractional part
224 # dpy.tagformat = "%-12s"; # align prop names to 12 spaces
225 # dpy.redraw(); # pick up new settings
228 # The open() method should only be used to undo a close() call. In all other
229 # cases this is done implicitly. redraw() is automatically called by an add(),
230 # but can be used to let the dialog pick up new settings of internal variables.
233 # Methods add(), setfont() and setcolor() can be appended to the new()
234 # constructor (-> show big yellow frame rate counter in upper right corner):
236 # screen.display.new(-15, -5, 0).setfont("TIMES_24").setcolor(1, 0.9, 0).add("/sim/frame-rate");
240 new : func(x, y, show_tags = 1) {
241 var m = { parents: [display] };
247 m.font = "HELVETICA_14";
248 m.color = [1, 1, 1, 1];
256 m.name = "__screen_display_" ~ (display.id += 1) ~ "__";
257 m.base = props.globals.getNode("/sim/gui/dialogs/property-display-" ~ display.id, 1);
258 m.namenode = props.Node.new({ "dialog-name": m.name });
259 setlistener("/sim/startup/xsize", func m.redraw());
260 setlistener("/sim/startup/ysize", func m.redraw());
264 setcolor : func(r, g, b, a = 1) {
265 me.color = [r, g, b, a];
269 setfont : func(font) {
275 me.dialog = gui.Widget.new();
276 me.dialog.set("name", me.name);
277 me.dialog.set("x", me.x);
278 me.dialog.set("y", me.y);
279 me.dialog.set("layout", "vbox");
280 me.dialog.set("default-padding", 2);
281 me.dialog.setFont(me.font);
282 me.dialog.setColor(0, 0, 0, 0);
284 foreach (var e; me.entries) {
285 var w = me.dialog.addChild("text");
286 w.set("halign", "left");
287 w.set("label", "M"); # mouse-grab sensitive area
288 w.set("property", e.target.getPath());
289 w.set("format", me.tags ? e.tag ~ " = %s" : "%s");
291 w.setColor(me.color[0], me.color[1], me.color[2], me.color[3]);
293 fgcommand("dialog-new", me.dialog.prop());
295 # add() opens already, so call open() explicitly only after close()!
297 if (me.dialog != nil) {
298 fgcommand("dialog-show", me.namenode);
299 me._loop_(me.loopid += 1);
303 if (me.dialog != nil) {
304 fgcommand("dialog-close", me.namenode);
310 me.dialog == nil ? me.redraw() : me.close();
323 foreach (nextprop; var n; props.nodeList(p)) {
324 var path = n.getPath();
325 foreach (var e; me.entries) {
326 if (e.node.getPath() == path)
329 e.tag = sprintf(me.tagformat, me.nameof(e.node));
331 append(me.entries, { node: n, parent: n,
332 tag: sprintf(me.tagformat, me.nameof(n)),
333 target: me.base.getChild("entry", size(me.entries), 1) });
336 # extend names to the left until they are unique
339 foreach (var e; me.entries) {
340 if (contains(uniq, e.tag))
341 append(uniq[e.tag], e);
347 foreach (var u; keys(uniq)) {
348 if (size(uniq[u]) == 1)
351 foreach (var e; uniq[u]) {
352 e.parent = e.parent.getParent();
354 e.tag = me.nameof(e.parent) ~ '/' ~ e.tag;
364 foreach (var e; me.entries) {
365 var type = e.node.getType();
368 elsif (type == "BOOL")
369 var val = e.node.getValue() ? "true" : "false";
370 elsif (type == "STRING" or type == "UNSPECIFIED")
371 var val = "'" ~ sanitize(e.node.getValue(), 1) ~ "'";
373 var val = sprintf(me.format, e.node.getValue());
374 e.target.setValue(val);
378 id != me.loopid and return;
380 settimer(func me._loop_(id), me.interval);
383 var name = n.getName();
384 if (var i = n.getIndex())
385 name ~= '[' ~ i ~ ']';
395 var property_display = nil;
399 # Shift-click in the property browser adds the selected property to the property display
400 # Shift-Alt-click adds all children of the selected property to the property display
401 # Shift-Ctrl-click removes all properties from the display
403 _setlistener("/sim/signals/nasal-dir-initialized", func {
404 property_display = display.new(5, -25);
405 listener.display = setlistener("/sim/gui/dialogs/property-browser/selected", func(n) {
406 var n = n.getValue();
407 if (n != "" and getprop("/devices/status/keyboard/shift")) {
408 if (getprop("/devices/status/keyboard/ctrl"))
409 return property_display.reset();
410 n = props.globals.getNode(n);
411 if (!n.getAttribute("children"))
412 property_display.add(n);
413 elsif (getprop("/devices/status/keyboard/alt"))
414 property_display.add(n.getChildren());
418 setlistener("/sim/gui/current-style", func {
419 var theme = getprop("/sim/gui/current-style");
420 theme_font = getprop("/sim/gui/style[" ~ theme ~ "]/fonts/message-display/name");
423 log = window.new(nil, -30, 10, 10);
424 log.sticky = 0; # don't turn on; makes scrolling up messages jump left and right
426 var b = "/sim/screen/";
427 setlistener(b ~ "black", func(n) log.write(n.getValue(), 0, 0, 0));
428 setlistener(b ~ "white", func(n) log.write(n.getValue(), 1, 1, 1));
429 setlistener(b ~ "red", func(n) log.write(n.getValue(), 0.8, 0, 0));
430 setlistener(b ~ "green", func(n) log.write(n.getValue(), 0, 0.6, 0));
431 setlistener(b ~ "blue", func(n) log.write(n.getValue(), 0, 0, 0.8));
432 setlistener(b ~ "yellow", func(n) log.write(n.getValue(), 0.8, 0.8, 0));
433 setlistener(b ~ "magenta", func(n) log.write(n.getValue(), 0.7, 0, 0.7));
434 setlistener(b ~ "cyan", func(n) log.write(n.getValue(), 0, 0.6, 0.6));
439 # --prop:display=sim/frame-rate ... adds this property to the property display
440 # --prop:display=position/ ... adds all properties under /position/ (ends with slash!)
441 # --prop:display=position/,orientation/ ... separate multiple properties with comma
443 var fdm_init_listener = _setlistener("/sim/signals/fdm-initialized", func {
444 removelistener(fdm_init_listener); # uninstall, so we're only called once
445 foreach (var n; props.globals.getChildren("display")) {
446 foreach (var p; split(",", n.getValue())) {
449 if (find('%', p) >= 0)
450 property_display.format = p;
452 property_display.add(props.globals.getNode(p, 1).getChildren());
454 property_display.add(p);
457 props.globals.removeChildren("display");
462 var search_name_in_msg = func(msg, call) {
465 for(var i = 0; i < size(msg); i = i + 1) {
466 if (msg[i] == ` ` or msg[i] == `,` or msg[i] == `.` or msg[i] == `;` or msg[i] == `:` or msg[i] == `>`) {
467 if (matching == size(call)) {
474 if (matching >= size(call)) {
475 matching = matching + 1;
478 if (call[matching] == msg[i]) {
479 matching = matching + 1;
484 if (found == 1 or matching == size(call))
489 ##############################################################################
490 # functions that make use of the window class (and don't belong anywhere else)
491 ##############################################################################
493 # highlights messages with the multiplayer callsign in the text
494 var msg_mp = func (n) {
495 if (!getprop("/sim/multiplay/chat-display"))
497 var msg = string.lc(n.getValue());
498 var call = string.lc(getprop("/sim/multiplay/callsign"));
499 var highlight = getprop("/sim/multiplay/chat_highlight");
500 if (search_name_in_msg(msg, call) or (highlight != nil and search_name_in_msg(msg, string.lc(highlight))))
501 screen.log.write(n.getValue(), 1.0, 0.5, 0.5);
503 screen.log.write(n.getValue(), 0.5, 0.0, 0.8);
506 var msg_repeat = func {
507 if (getprop("/sim/tutorials/running")) {
508 var last = getprop("/sim/tutorials/last-message");
512 setprop("/sim/messages/pilot", "Say again ...");
513 settimer(func setprop("/sim/messages/copilot", last), 1.5);
516 var last = atc.getValue();
520 setprop("/sim/messages/pilot", "This is " ~ callsign.getValue() ~ ". Say again, over.");
521 settimer(func atc.setValue(atclast.getValue()), 6);
530 _setlistener("/sim/signals/nasal-dir-initialized", func {
531 # set /sim/screen/nomap=true to prevent default message mapping
532 var nomap = getprop("/sim/screen/nomap");
533 if (nomap != nil and nomap)
536 callsign = props.globals.getNode("/sim/user/callsign", 1);
537 atc = props.globals.getNode("/sim/messages/atc", 1);
538 atclast = props.globals.getNode("/sim/messages/atc-last", 1);
539 atclast.setValue("");
541 # let ATC tell which runway was automatically chosen after startup/teleportation
543 setlistener("/sim/atc/runway", func(n) { # set in src/Main/fg_init.cxx
544 var rwy = n.getValue();
547 if (getprop("/sim/presets/airport-id") == "KSFO" and rwy == "28R")
549 if ((var agl = getprop("/position/altitude-agl-ft")) != nil and agl > 100)
551 screen.log.write("You are on runway " ~ rwy, 0.7, 1.0, 0.7);
555 setlistener("/gear/launchbar/state", func(n) {
556 if (n.getValue() == "Engaged")
557 setprop("/sim/messages/copilot", "Engaged!");
560 # map ATC messages to the screen log and to the voice subsystem
561 var map = func(type, msg, r, g, b, cond = nil) {
562 printlog("info", "{", type, "} ", msg);
563 setprop("/sim/sound/voices/" ~ type, msg);
565 if (cond == nil or cond())
566 screen.log.write(msg, r, g, b);
568 # save last ATC message for user callsign, unless this was already
569 # a repetition; insert "I say again" appropriately
571 var cs = callsign.getValue();
572 if (find(", I say again: ", atc.getValue()) < 0
573 and (var pos = find(cs, msg)) >= 0) {
574 var m = substr(msg, 0, pos + size(cs));
575 msg = substr(msg, pos + size(cs));
577 if ((pos = find("Tower, ", msg)) >= 0) {
578 m ~= substr(msg, 0, pos + 7);
579 msg = substr(msg, pos + 7);
583 m ~= "I say again: " ~ msg;
585 printlog("debug", "ATC_LAST_MESSAGE: ", m);
590 var m = "/sim/messages/";
591 listener.atc = setlistener(m ~ "atc",
592 func(n) map("atc", n.getValue(), 0.7, 1.0, 0.7));
593 listener.approach = setlistener(m ~ "approach",
594 func(n) map("approach", n.getValue(), 0.7, 1.0, 0.7));
595 listener.ground = setlistener(m ~ "ground",
596 func(n) map("ground", n.getValue(), 0.7, 1.0, 0.7));
598 listener.pilot = setlistener(m ~ "pilot",
599 func(n) map("pilot", n.getValue(), 1.0, 0.8, 0.0));
600 listener.copilot = setlistener(m ~ "copilot",
601 func(n) map("copilot", n.getValue(), 1.0, 1.0, 1.0));
602 listener.ai_plane = setlistener(m ~ "ai-plane",
603 func(n) map("ai-plane", n.getValue(), 0.9, 0.4, 0.2));
604 listener.mp_plane = setlistener(m ~ "mp-plane", msg_mp);