remove README.Protocol and add a README that refers to the "real"
[fg:toms-fgdata.git] / Nasal / screen.nas
1 # screen log window
2 # =================
3 #
4 #
5 # simple use:
6 #
7 #     foo = screen.window.new()
8 #     foo.write("message in the middle of the screen");
9 #
10 #
11 # advanced use:
12 #
13 #     bar = screen.window.new(nil, -100, 3, 10);
14 #     bar.fg = [1, 1, 1, 1];    # choose white color
15 #     bar.align = "left";
16 #
17 #     bar.write("first line");
18 #     bar.write("second line (red)", 1, 0, 0);
19 #
20 #
21 #
22 # arguments:
23 #            x ... x coordinate
24 #            y ... y coordinate
25 #                  positive coords position relative to the left/lower corner,
26 #                  negative coords from the right/upper corner, nil centers
27 #     maxlines ... max number of displayed lines; if more are pushed into the
28 #                  screen, then the ones on top fall off
29 #   autoscroll ... seconds that each line should be shown; can be less if
30 #                  a message falls off; if 0 then don't scroll at all
31 #
32
33
34 ##
35 # convert string for output; replaces tabs by spaces, and skips
36 # delimiters and the voice part in "{text|voice}" constructions
37 #
38 var sanitize = func(s) {
39         var r = "";
40         var skip = 0;
41         for (var i = 0; i < size(s); i += 1) {
42                 var c = s[i];
43                 if (c == `\t`) {
44                         r ~= ' ';
45                 } elsif (c == `{`) {
46                         #
47                 } elsif (c == `|`) {
48                         skip = 1;
49                 } elsif (c == `}`) {
50                         skip = 0;
51                 } elsif (!skip) {
52                         r ~= chr(c);
53                 }
54         }
55         return r;
56 }
57
58
59
60 var dialog_id = 0;
61 var theme_font = nil;
62
63 var window = {
64         new : func(x = nil, y = nil, maxlines = 10, autoscroll = 10) {
65                 var m = { parents : [window] };
66                 #
67                 # "public"
68                 m.x = x;
69                 m.y = y;
70                 m.maxlines = maxlines;
71                 m.autoscroll = autoscroll;      # display time in seconds
72                 m.sticky = 0;                   # reopens on old place
73                 m.font = nil;
74                 m.bg = [0, 0, 0, 0];            # background color
75                 m.fg = [0.9, 0.4, 0.2, 1];      # default foreground color
76                 m.align = "center";             # "left", "right", "center"
77                 #
78                 # "private"
79                 m.name = "__screen_window_" ~ (dialog_id += 1) ~ "__";
80                 m.lines = [];
81                 m.skiptimer = 0;
82                 m.dialog = nil;
83                 m.namenode = props.Node.new({ "dialog-name" : m.name });
84                 setlistener("/sim/startup/xsize", func { m._redraw_() });
85                 setlistener("/sim/startup/ysize", func { m._redraw_() });
86                 return m;
87         },
88
89         write : func(msg, r = nil, g = nil, b = nil, a = nil) {
90                 if (me.namenode == nil) { return }
91                 if (r == nil) { r = me.fg[0] }
92                 if (g == nil) { g = me.fg[1] }
93                 if (b == nil) { b = me.fg[2] }
94                 if (a == nil) { a = me.fg[3] }
95                 foreach (var line; split("\n", string.trim(msg))) {
96                         line = sanitize(string.trim(line));
97                         append(me.lines, [line, r, g, b, a]);
98                         if (size(me.lines) > me.maxlines) {
99                                 me.lines = subvec(me.lines, 1);
100                                 if (me.autoscroll) {
101                                         me.skiptimer += 1;
102                                 }
103                         }
104                         if (me.autoscroll) {
105                                 settimer(func { me._timeout_() }, me.autoscroll, 1);
106                         }
107                 }
108                 me.show();
109         },
110
111         show : func {
112                 if (me.dialog != nil) {
113                         me.close();
114                 }
115
116                 me.dialog = gui.Widget.new();
117                 me.dialog.set("name", me.name);
118                 if (me.x != nil) { me.dialog.set("x", me.x) }
119                 if (me.y != nil) { me.dialog.set("y", me.y) }
120                 me.dialog.set("layout", "vbox");
121                 me.dialog.set("default-padding", 2);
122                 if (me.font != nil) {
123                         me.dialog.setFont(me.font);
124                 } elsif (theme_font != nil) {
125                         me.dialog.setFont(theme_font);
126                 }
127                 me.dialog.setColor(me.bg[0], me.bg[1], me.bg[2], me.bg[3]);
128
129                 foreach (var line; me.lines) {
130                         var w = me.dialog.addChild("text");
131                         w.set("halign", me.align);
132                         w.set("label", line[0]);
133                         w.setColor(line[1], line[2], line[3], line[4]);
134                 }
135
136                 fgcommand("dialog-new", me.dialog.prop());
137                 fgcommand("dialog-show", me.namenode);
138         },
139
140         close : func {
141                 fgcommand("dialog-close", me.namenode);
142                 if (me.dialog != nil and me.sticky) {
143                         me.x = me.dialog.prop().getNode("lastx").getValue();
144                         me.y = me.dialog.prop().getNode("lasty").getValue();
145                 }
146         },
147
148         _timeout_ : func {
149                 if (me.skiptimer > 0) {
150                         me.skiptimer -= 1;
151                         return;
152                 }
153                 if (size(me.lines) > 1) {
154                         me.lines = subvec(me.lines, 1);
155                         me.show();
156                 } else {
157                         me.close();
158                         dialog = nil;
159                         me.lines = [];
160                 }
161         },
162
163         _redraw_ : func {
164                 if (me.dialog != nil) {
165                         me.close();
166                         me.show();
167                 }
168         },
169 };
170
171
172
173 var log = nil;
174
175 _setlistener("/sim/signals/nasal-dir-initialized", func {
176         setlistener("/sim/gui/current-style", func {
177                 var theme = getprop("/sim/gui/current-style");
178                 theme_font = getprop("/sim/gui/style[" ~ theme ~ "]/fonts/message-display/name");
179         }, 1);
180
181         log = window.new(nil, -30, 10, 10);
182         log.sticky = 0;  # don't turn on; makes scrolling up messages jump left and right
183
184         var b = "/sim/screen/";
185         setlistener(b ~ "black",   func { log.write(cmdarg().getValue(), 0,   0,   0) });
186         setlistener(b ~ "white",   func { log.write(cmdarg().getValue(), 1,   1,   1) });
187         setlistener(b ~ "red",     func { log.write(cmdarg().getValue(), 0.8, 0,   0) });
188         setlistener(b ~ "green",   func { log.write(cmdarg().getValue(), 0,   0.6, 0) });
189         setlistener(b ~ "blue",    func { log.write(cmdarg().getValue(), 0,   0,   0.8) });
190         setlistener(b ~ "yellow",  func { log.write(cmdarg().getValue(), 0.8, 0.8, 0) });
191         setlistener(b ~ "magenta", func { log.write(cmdarg().getValue(), 0.7, 0,   0.7) });
192         setlistener(b ~ "cyan",    func { log.write(cmdarg().getValue(), 0,   0.6, 0.6) });
193 });
194
195
196
197
198
199 ##############################################################################
200 # functions that make use of the window class (and don't belong anywhere else)
201 ##############################################################################
202
203
204 var msg_repeat = func {
205         if (getprop("/sim/tutorials/running")) {
206                 var last = getprop("/sim/tutorials/last-message");
207                 if (last == nil) {
208                         return;
209                 }
210                 setprop("/sim/messages/pilot", "Say again ...");
211                 settimer(func { setprop("/sim/messages/copilot", last) }, 1.5);
212
213         } else {
214                 var last = atc.getValue();
215                 if (last == nil) {
216                         return;
217                 }
218                 setprop("/sim/messages/pilot", "This is " ~ callsign.getValue() ~ ". Say again, over.");
219                 settimer(func {
220                         atc.setValue(atclast.getValue());
221                 }, 6);
222         }
223 }
224
225
226 var atc = nil;
227 var callsign = nil;
228 var atclast = nil;
229 var listener = {};
230
231 _setlistener("/sim/signals/nasal-dir-initialized", func {
232         # set /sim/screen/nomap=true to prevent default message mapping
233         var nomap = getprop("/sim/screen/nomap");
234         if (nomap != nil and nomap) {
235                 return;
236         }
237
238         callsign = props.globals.getNode("/sim/user/callsign", 1);
239         atc = props.globals.getNode("/sim/messages/atc", 1);
240         atclast = props.globals.getNode("/sim/messages/atc-last", 1);
241         atclast.setValue("");
242
243         # map ATC messages to the screen log and to the voice subsystem
244         var map = func(type, msg, r, g, b) {
245                 setprop("/sim/sound/voices/" ~ type, msg);
246                 screen.log.write(msg, r, g, b);
247                 printlog("info", "{", type, "} ", msg);
248
249                 # save last ATC message for user callsign, unless this was already
250                 # a repetition; insert "I say again" appropriately
251                 if (type == "atc") {
252                         var cs = callsign.getValue();
253                         if (find(", I say again: ", atc.getValue()) < 0
254                                         and (var pos = find(cs, msg)) >= 0) {
255                                 var m = substr(msg, 0, pos + size(cs));
256                                 msg = substr(msg, pos + size(cs));
257
258                                 if ((pos = find("Tower, ", msg)) >= 0) {
259                                         m ~= substr(msg, 0, pos + 7);
260                                         msg = substr(msg, pos + 7);
261                                 } else {
262                                         m ~= ", ";
263                                 }
264                                 m ~= "I say again: " ~ msg;
265                                 atclast.setValue(m);
266                                 printlog("debug", "ATC_LAST_MESSAGE: ", m);
267                         }
268                 }
269         }
270
271         var m = "/sim/messages/";
272         listener["atc"] = setlistener(m ~ "atc",
273                         func { map("atc",      cmdarg().getValue(), 0.7, 1.0, 0.7) });
274         listener["approach"] = setlistener(m ~ "approach",
275                         func { map("approach", cmdarg().getValue(), 0.7, 1.0, 0.7) });
276         listener["ground"] = setlistener(m ~ "ground",
277                         func { map("ground",   cmdarg().getValue(), 0.7, 1.0, 0.7) });
278
279         listener["pilot"] = setlistener(m ~ "pilot",
280                         func { map("pilot",    cmdarg().getValue(), 1.0, 0.8, 0.0) });
281         listener["copilot"] = setlistener(m ~ "copilot",
282                         func { map("copilot",  cmdarg().getValue(), 1.0, 1.0, 1.0) });
283         listener["ai-plane"] = setlistener(m ~ "ai-plane",
284                         func { map("ai-plane", cmdarg().getValue(), 0.9, 0.4, 0.2) });
285 });
286
287